Warning, /network/tokodon/src/content/ui/Main.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu> 0002 // SPDX-FileCopyrightText: 2020 Han Young <hanyoung@protonmail.com> 0003 // SPDX-FileCopyrightText: 2020 Devin Lin <espidev@gmail.com> 0004 // SPDX-License-Identifier: GPL-3.0-only 0005 0006 import QtQuick 0007 import org.kde.kirigami 2 as Kirigami 0008 import QtQuick.Controls 2 as QQC2 0009 import QtQuick.Layouts 0010 import QtQml.Models 0011 import org.kde.tokodon 0012 import org.kde.kirigamiaddons.delegates 1 as Delegates 0013 0014 import "./StatusComposer" 0015 import "./StatusDelegate" 0016 0017 Kirigami.ApplicationWindow { 0018 id: appwindow 0019 0020 property bool isShowingFullScreenImage: false 0021 0022 // A new post was created by us. Currently used by ThreadPages to update themselves when we reply. 0023 signal newPost() 0024 0025 minimumWidth: Kirigami.Units.gridUnit * 21 0026 minimumHeight: Kirigami.Units.gridUnit * 20 0027 0028 pageStack { 0029 defaultColumnWidth: appwindow.width 0030 0031 globalToolBar { 0032 canContainHandles: true 0033 style: Kirigami.ApplicationHeaderStyle.ToolBar 0034 showNavigationButtons: if (applicationWindow().pageStack.currentIndex > 0 0035 || applicationWindow().pageStack.currentIndex > 0) { 0036 Kirigami.ApplicationHeaderStyle.ShowBackButton 0037 } else { 0038 0 0039 } 0040 } 0041 } 0042 0043 function decideDefaultPage() { 0044 globalDrawer.drawerOpen = true; 0045 pageStack.clear(); 0046 0047 if (InitialSetupFlow.isSetupNeeded()) { 0048 globalDrawer.drawerOpen = false; 0049 pageStack.push(Qt.createComponent("org.kde.tokodon", "SetupWelcome")); 0050 return; 0051 } 0052 0053 if (AccountManager.selectedAccountHasIssue) { 0054 pageStack.push(Qt.createComponent("org.kde.tokodon", "LoginIssuePage")); 0055 } else { 0056 pageStack.push(mainTimeline, { 0057 name: 'home', 0058 }); 0059 } 0060 } 0061 0062 function startupAccountCheck() { 0063 if (AccountManager.hasAccounts) { 0064 decideDefaultPage(); 0065 } else { 0066 pageStack.push(Qt.createComponent("org.kde.tokodon", "WelcomePage")); 0067 } 0068 } 0069 0070 function navigateLink(link, shouldOpenInternalLinks) { 0071 if (link.startsWith('hashtag:/') && shouldOpenInternalLinks) { 0072 pageStack.push(tagModelComponent, { 0073 hashtag: link.substring(9), 0074 }) 0075 } else if (link.startsWith('account:/') && shouldOpenInternalLinks) { 0076 Navigation.openAccount(link.substring(9)) 0077 } else if (link.startsWith('web+ap:/') && shouldOpenInternalLinks) { 0078 Controller.openWebApLink(link.substring(8)) 0079 } else { 0080 Qt.openUrlExternally(link) 0081 } 0082 } 0083 0084 function requestCrossAction(action, url) { 0085 console.log("Requesting " + action + " of " + url); 0086 crossActionDialog.action = action; 0087 crossActionDialog.url = url; 0088 crossActionDialog.open(); 0089 } 0090 0091 ReportDialog { 0092 id: reportDialog 0093 visible: false 0094 } 0095 0096 Kirigami.PromptDialog { 0097 id: crossActionDialog 0098 0099 property string url 0100 property string action 0101 0102 title: { 0103 if (action === 'open') { 0104 return i18nc("@title", "Open As…") 0105 } else if (action === 'reply') { 0106 return i18nc("@title", "Reply As…") 0107 } else if (action === 'favourite') { 0108 return i18nc("@title", "Favorite As…") 0109 } else if (action === 'reblog') { 0110 return i18nc("@title", "Boost As…") 0111 } else if (action === 'bookmark') { 0112 return i18nc("@title", "Bookmark As…") 0113 } else { 0114 return i18nc("@title", "Unknown Action") 0115 } 0116 } 0117 0118 standardButtons: Kirigami.Dialog.NoButton 0119 0120 mainItem: ColumnLayout { 0121 Repeater { 0122 id: accounts 0123 0124 model: AccountManager 0125 0126 delegate: Delegates.RoundedItemDelegate { 0127 required property int index 0128 required property string displayName 0129 required property string instance 0130 required property var account 0131 0132 text: displayName 0133 0134 Layout.fillWidth: true 0135 0136 onClicked: crossActionDialog.takeAction(account) 0137 } 0138 } 0139 } 0140 0141 function takeAction(account): void { 0142 if (action === 'open' || action === 'reply') { 0143 AccountManager.selectedAccount = account; 0144 } 0145 0146 Qt.callLater(() => { 0147 if (action === 'open') { 0148 Controller.openWebApLink(url); 0149 } else { 0150 account.mutateRemotePost(url, action); 0151 } 0152 }); 0153 0154 close(); 0155 } 0156 } 0157 0158 Component.onCompleted: { 0159 if (AccountManager.isReady) { 0160 startupAccountCheck(); 0161 } 0162 0163 saveWindowGeometryConnections.enabled = true; 0164 homeAction.checked = true; 0165 } 0166 0167 Connections { 0168 target: AccountManager 0169 0170 function onAccountSelected() { 0171 decideDefaultPage(); 0172 } 0173 0174 function onAccountRemoved() { 0175 if (!AccountManager.hasAccounts) { 0176 pageStack.replace(Qt.createComponent("org.kde.tokodon", "WelcomePage")); 0177 globalDrawer.drawerOpen = false; 0178 } 0179 } 0180 0181 function onAccountsReloaded() { 0182 pageStack.replace(mainTimeline, { 0183 name: "home" 0184 }); 0185 } 0186 0187 function onAccountsReady() { 0188 appwindow.startupAccountCheck(); 0189 } 0190 } 0191 0192 Connections { 0193 target: Controller 0194 0195 function onOpenPost(id) { 0196 Navigation.openThread(id) 0197 } 0198 0199 function onOpenAccount(id) { 0200 Navigation.openAccount(id) 0201 } 0202 0203 function onOpenComposer(text) { 0204 pageStack.layers.push("./StatusComposer/StatusComposer.qml", { 0205 purpose: StatusComposer.New, 0206 initialText: text, 0207 onPostFinished: applicationWindow().pageStack.layers.pop() 0208 }); 0209 } 0210 } 0211 0212 Connections { 0213 target: Navigation 0214 0215 function onOpenStatusComposer() { 0216 pageStack.layers.push("./StatusComposer/StatusComposer.qml", { 0217 purpose: StatusComposer.New 0218 }); 0219 } 0220 0221 function onReplyTo(inReplyTo, mentions, visibility, authorIdentity, post) { 0222 if (!mentions.includes(`@${authorIdentity.account}`)) { 0223 mentions.push(`@${authorIdentity.account}`); 0224 } 0225 pageStack.layers.push("./StatusComposer/StatusComposer.qml", { 0226 purpose: StatusComposer.Reply, 0227 inReplyTo: inReplyTo, 0228 mentions: mentions, 0229 visibility: visibility, 0230 previewPost: post 0231 }); 0232 } 0233 0234 function onOpenThread(postId) { 0235 if (!pageStack.currentItem.postId || pageStack.currentItem.postId !== postId) { 0236 pageStack.push(Qt.createComponent("org.kde.tokodon", "ThreadPage"), { 0237 postId: postId, 0238 }); 0239 } 0240 } 0241 0242 function onOpenAccount(accountId) { 0243 if (!pageStack.currentItem.accountId || pageStack.currentItem.accountId !== accountId) { 0244 pageStack.push(Qt.createComponent("org.kde.tokodon", "AccountInfo"), { 0245 accountId: accountId, 0246 }); 0247 } 0248 } 0249 0250 function onOpenTag(tag) { 0251 pageStack.push(tagModelComponent, { 0252 hashtag: tag, 0253 }) 0254 } 0255 0256 function onReportPost(identity, postId) { 0257 applicationWindow().pageStack.pushDialogLayer(Qt.createComponent("org.kde.tokodon", "ReportDialog"), 0258 { 0259 type: ReportDialog.Post, 0260 identity: identity, 0261 postId: postId 0262 }); 0263 } 0264 0265 function onReportUser(identity) { 0266 applicationWindow().pageStack.pushDialogLayer(Qt.createComponent("org.kde.tokodon", "ReportDialog"), 0267 { 0268 type: ReportDialog.User, 0269 identity: identity 0270 }); 0271 } 0272 0273 function onOpenList(listId, name) { 0274 pageStack.push(Qt.createComponent("org.kde.tokodon", "ListPage"), { 0275 name, 0276 listId 0277 }); 0278 } 0279 } 0280 0281 globalDrawer: Kirigami.OverlayDrawer { 0282 id: drawer 0283 enabled: AccountManager.hasAccounts && AccountManager.isReady 0284 edge: Qt.application.layoutDirection === Qt.RightToLeft ? Qt.RightEdge : Qt.LeftEdge 0285 modal: !enabled || Kirigami.Settings.isMobile || Kirigami.Settings.tabletMode || (applicationWindow().width < Kirigami.Units.gridUnit * 50 && !collapsed) // Only modal when not collapsed, otherwise collapsed won't show. 0286 z: modal ? Math.round(position * 10000000) : 100 0287 drawerOpen: !Kirigami.Settings.isMobile && enabled 0288 width: Kirigami.Units.gridUnit * 16 0289 Behavior on width { 0290 NumberAnimation { 0291 duration: Kirigami.Units.longDuration 0292 easing.type: Easing.InOutQuad 0293 } 0294 } 0295 Kirigami.Theme.colorSet: Kirigami.Theme.View 0296 Kirigami.Theme.inherit: false 0297 0298 handleClosedIcon.source: modal ? null : "sidebar-expand-left" 0299 handleOpenIcon.source: modal ? null : "sidebar-collapse-left" 0300 handleVisible: modal && !isShowingFullScreenImage && enabled 0301 onModalChanged: drawerOpen = !modal; 0302 0303 leftPadding: 0 0304 rightPadding: 0 0305 topPadding: 0 0306 bottomPadding: 0 0307 0308 contentItem: ColumnLayout { 0309 spacing: 0 0310 0311 QQC2.ToolBar { 0312 Layout.fillWidth: true 0313 Layout.preferredHeight: pageStack.globalToolBar.preferredHeight 0314 Layout.bottomMargin: Kirigami.Units.smallSpacing / 2 0315 0316 leftPadding: 3 0317 rightPadding: 3 0318 topPadding: 3 0319 bottomPadding: 3 0320 0321 visible: !Kirigami.Settings.isMobile 0322 0323 contentItem: SearchField {} 0324 } 0325 0326 UserInfo { 0327 Layout.fillWidth: true 0328 } 0329 0330 Kirigami.Separator { 0331 Layout.fillWidth: true 0332 Layout.margins: Kirigami.Units.smallSpacing 0333 } 0334 0335 QQC2.ButtonGroup { 0336 id: pageButtonGroup 0337 } 0338 0339 Repeater { 0340 model: Kirigami.Settings.isMobile ? 0341 [searchAction, announcementsAction, followRequestAction, exploreAction, conversationAction, favouritesAction, bookmarksAction, listsAction] : 0342 [homeAction, notificationAction, searchAction, announcementsAction, followRequestAction, localTimelineAction, globalTimelineAction, exploreAction, conversationAction, favouritesAction, bookmarksAction, listsAction] 0343 Delegates.RoundedItemDelegate { 0344 required property var modelData 0345 QQC2.ButtonGroup.group: pageButtonGroup 0346 0347 padding: Kirigami.Units.largeSpacing 0348 action: modelData 0349 Layout.fillWidth: true 0350 visible: modelData.visible 0351 enabled: !AccountManager.selectedAccountHasIssue 0352 } 0353 } 0354 0355 Item { 0356 Layout.fillHeight: true 0357 } 0358 0359 Delegates.RoundedItemDelegate { 0360 icon.name: "debug-run" 0361 onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.tokodon", "DebugPage")) 0362 text: i18nc("@action:button Open debug page", "Debug") 0363 visible: AccountManager.testMode 0364 padding: Kirigami.Units.largeSpacing 0365 0366 Layout.fillWidth: true 0367 } 0368 0369 Delegates.RoundedItemDelegate { 0370 icon.name: "lock" 0371 onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.tokodon", "ModerationToolPage"), {}, {title: i18n("Moderation Tools")}) 0372 text: i18nc("@action:button Open moderation tools", "Moderation Tools") 0373 visible: AccountManager.selectedAccount && (AccountManager.selectedAccount.identity.permission & AdminAccountInfo.ManageUsers) 0374 padding: Kirigami.Units.largeSpacing 0375 0376 Layout.fillWidth: true 0377 } 0378 0379 Delegates.RoundedItemDelegate { 0380 icon.name: "settings-configure" 0381 onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.tokodon", "SettingsPage"), {}, {title: i18n("Configure")}) 0382 text: i18nc("@action:button Open settings dialog", "Settings") 0383 padding: Kirigami.Units.largeSpacing 0384 0385 Layout.fillWidth: true 0386 Layout.bottomMargin: Kirigami.Units.smallSpacing 0387 } 0388 } 0389 } 0390 0391 property Kirigami.Action homeAction: Kirigami.Action { 0392 icon.name: "go-home-large" 0393 text: i18n("Home") 0394 checkable: true 0395 onTriggered: { 0396 pageStack.clear(); 0397 pageStack.push(mainTimeline, { 0398 name: "home" 0399 }); 0400 checked = true; 0401 if (Kirigami.Settings.isMobile || drawer.modal) { 0402 drawer.drawerOpen = false; 0403 } 0404 } 0405 } 0406 property Kirigami.Action notificationAction: Kirigami.Action { 0407 icon.name: "notifications" 0408 text: i18n("Notifications") 0409 checkable: true 0410 onTriggered: { 0411 pageStack.clear(); 0412 pageStack.push(notificationTimeline); 0413 checked = true; 0414 if (Kirigami.Settings.isMobile || drawer.modal) { 0415 drawer.drawerOpen = false; 0416 } 0417 } 0418 } 0419 property Kirigami.Action followRequestAction: Kirigami.Action { 0420 icon.name: "list-add-user" 0421 text: i18n("Follow Requests") 0422 checkable: true 0423 visible: AccountManager.hasAccounts && AccountManager.selectedAccount && AccountManager.selectedAccount.hasFollowRequests 0424 onTriggered: { 0425 pageStack.clear(); 0426 pageStack.push(socialGraphComponent, { 0427 name: "request" 0428 }); 0429 checked = true; 0430 if (Kirigami.Settings.isMobile || drawer.modal) { 0431 drawer.drawerOpen = false; 0432 } 0433 } 0434 } 0435 property Kirigami.Action localTimelineAction: Kirigami.Action { 0436 icon.name: "system-users" 0437 text: i18n("Local") 0438 checkable: true 0439 onTriggered: { 0440 pageStack.clear(); 0441 pageStack.push(mainTimeline, { 0442 name: "public", 0443 }); 0444 checked = true; 0445 if (Kirigami.Settings.isMobile || drawer.modal) { 0446 drawer.drawerOpen = false; 0447 } 0448 } 0449 } 0450 property Kirigami.Action globalTimelineAction: Kirigami.Action { 0451 icon.name: "kstars_xplanet" 0452 text: i18n("Global") 0453 checkable: true 0454 onTriggered: { 0455 pageStack.clear(); 0456 pageStack.push(mainTimeline, { 0457 name: "federated", 0458 }); 0459 checked = true; 0460 if (Kirigami.Settings.isMobile || drawer.modal) { 0461 drawer.drawerOpen = false; 0462 } 0463 } 0464 } 0465 0466 property Kirigami.Action conversationAction: Kirigami.Action { 0467 icon.name: "tokodon-chat-reply" 0468 text: i18n("Conversation") 0469 checkable: true 0470 onTriggered: { 0471 pageStack.clear(); 0472 pageStack.push(Qt.createComponent("org.kde.tokodon", "ConversationPage")); 0473 checked = true; 0474 if (Kirigami.Settings.isMobile || drawer.modal) { 0475 drawer.drawerOpen = false; 0476 } 0477 } 0478 } 0479 0480 property Kirigami.Action favouritesAction: Kirigami.Action { 0481 icon.name: "tokodon-post-favorite" 0482 text: i18n("Favourites") 0483 checkable: true 0484 onTriggered: { 0485 pageStack.clear(); 0486 pageStack.push(mainTimeline, { 0487 name: "favourites", 0488 }); 0489 checked = true; 0490 if (Kirigami.Settings.isMobile || drawer.modal) { 0491 drawer.drawerOpen = false; 0492 } 0493 } 0494 } 0495 0496 property Kirigami.Action bookmarksAction: Kirigami.Action { 0497 icon.name: "bookmarks" 0498 text: i18n("Bookmarks") 0499 checkable: true 0500 onTriggered: { 0501 pageStack.clear(); 0502 pageStack.push(mainTimeline, { 0503 name: "bookmarks", 0504 }); 0505 checked = true; 0506 if (Kirigami.Settings.isMobile || drawer.modal) { 0507 drawer.drawerOpen = false; 0508 } 0509 } 0510 } 0511 0512 property Kirigami.Action exploreAction: Kirigami.Action { 0513 icon.name: "kstars_planets" 0514 text: i18n("Explore") 0515 checkable: true 0516 onTriggered: { 0517 pageStack.clear(); 0518 pageStack.push(exploreTimeline); 0519 checked = true; 0520 if (Kirigami.Settings.isMobile || drawer.modal) { 0521 drawer.drawerOpen = false; 0522 } 0523 } 0524 } 0525 0526 property Kirigami.Action searchAction: Kirigami.Action { 0527 icon.name: "search" 0528 text: i18n("Search") 0529 checkable: true 0530 visible: Kirigami.Settings.isMobile 0531 onTriggered: { 0532 pageStack.clear(); 0533 pageStack.push(Qt.createComponent("org.kde.tokodon", "SearchPage")); 0534 checked = true; 0535 if (Kirigami.Settings.isMobile) { 0536 drawer.drawerOpen = false; 0537 } 0538 } 0539 } 0540 0541 property Kirigami.Action announcementsAction: Kirigami.Action { 0542 icon.name: "note" 0543 text: i18nc("@action:button Server Announcements", "Announcements") 0544 checkable: true 0545 visible: AccountManager.hasAccounts && AccountManager.selectedAccount 0546 onTriggered: { 0547 pageStack.clear(); 0548 pageStack.push(Qt.createComponent("org.kde.tokodon", "AnnouncementsPage")); 0549 checked = true; 0550 if (Kirigami.Settings.isMobile || drawer.modal) { 0551 drawer.drawerOpen = false; 0552 } 0553 } 0554 } 0555 0556 property Kirigami.Action listsAction: Kirigami.Action { 0557 icon.name: "view-list-text" 0558 text: i18n("Lists") 0559 checkable: true 0560 onTriggered: { 0561 pageStack.clear(); 0562 pageStack.push(Qt.createComponent("org.kde.tokodon", "ListsPage")); 0563 checked = true; 0564 if (Kirigami.Settings.isMobile || drawer.modal) { 0565 drawer.drawerOpen = false; 0566 } 0567 } 0568 } 0569 0570 property Kirigami.NavigationTabBar tabBar: Kirigami.NavigationTabBar { 0571 // Make sure we take in count drawer width 0572 visible: pageStack.layers.depth <= 1 && AccountManager.hasAccounts && !appwindow.wideScreen 0573 actions: [homeAction, notificationAction, localTimelineAction, globalTimelineAction] 0574 enabled: !AccountManager.selectedAccountHasIssue 0575 } 0576 0577 footer: Kirigami.Settings.isMobile ? tabBar : null 0578 0579 contextDrawer: Kirigami.ContextDrawer { 0580 id: contextDrawer 0581 } 0582 0583 Component { 0584 id: mainTimeline 0585 TimelinePage { 0586 id: timelinePage 0587 property alias name: timelineModel.name 0588 model: MainTimelineModel { 0589 id: timelineModel 0590 showReplies: timelinePage.showReplies 0591 showBoosts: timelinePage.showBoosts 0592 } 0593 } 0594 } 0595 0596 Component { 0597 id: socialGraphComponent 0598 SocialGraphPage { 0599 id: socialGraphPage 0600 property alias name: socialGraphModel.name 0601 property alias accountId: socialGraphModel.accountId 0602 property alias statusId: socialGraphModel.statusId 0603 property alias count: socialGraphModel.count 0604 model: SocialGraphModel { 0605 id: socialGraphModel 0606 name: socialGraphPage.name 0607 accountId: socialGraphPage.accountId 0608 statusId: socialGraphPage.statusId 0609 count: socialGraphPage.count 0610 } 0611 } 0612 } 0613 0614 Component { 0615 id: notificationTimeline 0616 NotificationPage { } 0617 } 0618 0619 Component { 0620 id: exploreTimeline 0621 ExplorePage { } 0622 } 0623 0624 property Item hoverLinkIndicator: QQC2.Control { 0625 parent: overlay.parent 0626 property alias text: linkText.text 0627 opacity: text.length > 0 ? 1 : 0 0628 visible: !Kirigami.Settings.isMobile && !text.startsWith("hashtag:") && !text.startsWith("account:") 0629 0630 z: 999990 0631 x: 0 0632 y: parent.height - implicitHeight 0633 contentItem: QQC2.Label { 0634 id: linkText 0635 } 0636 Kirigami.Theme.colorSet: Kirigami.Theme.View 0637 background: Rectangle { 0638 color: Kirigami.Theme.backgroundColor 0639 } 0640 } 0641 0642 Component { 0643 id: tagModelComponent 0644 TimelinePage { 0645 id: tagPage 0646 property string hashtag 0647 model: TagsTimelineModel { 0648 hashtag: tagPage.hashtag 0649 showReplies: tagPage.showReplies 0650 showBoosts: tagPage.showBoosts 0651 } 0652 } 0653 } 0654 0655 Rectangle { 0656 anchors.fill: parent 0657 visible: !AccountManager.isReady 0658 color: Kirigami.Theme.backgroundColor 0659 0660 Kirigami.LoadingPlaceholder { 0661 anchors.centerIn: parent 0662 } 0663 } 0664 0665 // This timer allows to batch update the window size change to reduce 0666 // the io load and also work around the fact that x/y/width/height are 0667 // changed when loading the page and overwrite the saved geometry from 0668 // the previous session. 0669 Timer { 0670 id: saveWindowGeometryTimer 0671 interval: 1000 0672 onTriggered: WindowController.saveGeometry() 0673 } 0674 0675 Connections { 0676 id: saveWindowGeometryConnections 0677 enabled: false // Disable on startup to avoid writing wrong values if the window is hidden 0678 target: appwindow 0679 0680 function onClosing() { WindowController.saveGeometry(); } 0681 function onWidthChanged() { saveWindowGeometryTimer.restart(); } 0682 function onHeightChanged() { saveWindowGeometryTimer.restart(); } 0683 function onXChanged() { saveWindowGeometryTimer.restart(); } 0684 function onYChanged() { saveWindowGeometryTimer.restart(); } 0685 } 0686 0687 Connections { 0688 target: AccountManager.selectedAccount 0689 0690 function onFetchedOEmbed(html) { 0691 embedDialog.active = true; 0692 embedDialog.item.html = html; 0693 embedDialog.item.open() 0694 } 0695 } 0696 0697 Loader { 0698 id: embedDialog 0699 0700 active: false 0701 sourceComponent: Kirigami.PromptDialog { 0702 property alias html: textArea.text 0703 0704 title: i18nc("@title", "Embed Information") 0705 mainItem: QQC2.TextArea { 0706 id: textArea 0707 0708 readOnly: true 0709 wrapMode: TextEdit.Wrap 0710 0711 Kirigami.SpellCheck.enabled: false 0712 } 0713 standardButtons: Kirigami.Dialog.Ok 0714 showCloseButton: false 0715 } 0716 } 0717 }