status-desktop/ui/app/AppLayouts/CommunitiesPortal/popups/CreateCommunityPopup.qml

530 lines
21 KiB
QML
Raw Normal View History

import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
2020-12-11 20:29:46 +00:00
import QtQuick.Dialogs 1.3
import utils 1.0
import shared.panels 1.0
import shared.popups 1.0
2020-12-11 20:29:46 +00:00
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Popups 0.1
import "../../Chat/controls/community"
import "../controls"
import "../panels"
StatusStackModal {
id: root
2020-12-17 13:24:33 +00:00
property var store
property bool isDiscordImport // creating new or importing from discord?
2020-12-11 20:29:46 +00:00
stackTitle: isDiscordImport ? qsTr("Import a community from Discord into Status") :
qsTr("Create New Community")
width: 640
nextButton: StatusButton {
objectName: "createCommunityNextBtn"
font.weight: Font.Medium
text: typeof currentItem.nextButtonText !== "undefined" ? currentItem.nextButtonText : qsTr("Next")
enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext
loading: root.store.discordDataExtractionInProgress
onClicked: {
let nextAction = currentItem.nextAction
if (typeof(nextAction) == "function") {
return nextAction()
}
root.currentIndex++
}
}
finishButton: StatusButton {
objectName: "createCommunityFinalBtn"
font.weight: Font.Medium
text: root.isDiscordImport ? qsTr("Start Discord import") : qsTr("Create Community")
enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext
onClicked: {
let nextAction = currentItem.nextAction
if (typeof (nextAction) == "function") {
return nextAction()
}
if (!root.isDiscordImport)
d.createCommunity()
}
}
2020-12-11 20:29:46 +00:00
readonly property var clearFilesButton: StatusButton {
font.weight: Font.Medium
text: qsTr("Clear all")
type: StatusBaseButton.Type.Danger
visible: root.currentItem.objectName === "discordFileListView" // no better way to address the current item in the stack :/
enabled: !fileListView.fileListModelEmpty && !root.store.discordDataExtractionInProgress
onClicked: root.store.clearFileList()
}
rightButtons: [clearFilesButton, nextButton, finishButton]
onAboutToShow: {
nameInput.input.edit.forceActiveFocus()
if (root.isDiscordImport) {
for (let i = 0; i < discordPages.length; i++) {
stackItems.push(discordPages[i])
}
}
}
onClosed: {
if (root.isDiscordImport) {
root.store.clearFileList()
root.store.clearDiscordCategoriesAndChannels()
}
}
readonly property list<Item> discordPages: [
ColumnLayout {
id: fileListView
objectName: "discordFileListView" // !!! DON'T CHANGE, clearFilesButton depends on this
spacing: 24
readonly property var fileListModel: root.store.discordFileList
readonly property bool fileListModelEmpty: !fileListModel.count
readonly property bool canGoNext: fileListModel.selectedCount
|| (fileListModel.selectedCount && fileListModel.selectedFilesValid)
readonly property string nextButtonText:
fileListModel.selectedCount && fileListModel.selectedFilesValid ? qsTr("Proceed with (%1/%2) files").arg(fileListModel.selectedCount).arg(fileListModel.count) :
fileListModel.selectedCount ? qsTr("Validate (%1/%2) files").arg(fileListModel.selectedCount).arg(fileListModel.count)
: qsTr("Import files")
readonly property var nextAction: function () {
if (!fileListView.fileListModel.selectedFilesValid) {
return root.store.requestExtractChannelsAndCategories()
}
root.currentIndex++
}
RowLayout {
Layout.fillWidth: true
spacing: 12
StatusBaseText {
font.pixelSize: 15
text: fileListView.fileListModelEmpty ? qsTr("Select Discord JSON files to import") :
root.store.discordImportErrorsCount ? qsTr("Some of your community files cannot be used") :
qsTr("Uncheck any files you would like to exclude from the import")
}
StatusBaseText {
visible: fileListView.fileListModelEmpty
font.pixelSize: 12
color: Theme.palette.baseColor1
text: qsTr("(JSON file format only)")
}
IssuePill {
type: root.store.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning
count: {
if (root.store.discordImportErrorsCount > 0) {
return root.store.discordImportErrorsCount
}
if (root.store.discordImportWarningsCount > 0) {
return root.store.discordImportWarningsCount
}
return 0
}
visible: !!count
}
Item { Layout.fillWidth: true }
StatusButton {
text: qsTr("Browse files")
type: StatusBaseButton.Type.Primary
onClicked: fileDialog.open()
enabled: !root.store.discordDataExtractionInProgress
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.palette.baseColor4
ColumnLayout {
visible: fileListView.fileListModelEmpty
anchors.top: parent.top
anchors.topMargin: 60
anchors.horizontalCenter: parent.horizontalCenter
spacing: 8
StatusRoundIcon {
Layout.alignment: Qt.AlignHCenter
asset.name: "info"
}
StatusBaseText {
Layout.topMargin: 8
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Qt.AlignHCenter
linkColor: hoveredLink ? Qt.lighter(Theme.palette.primaryColor1) : Theme.palette.primaryColor1
text: qsTr("Export your Discord JSON data using %1")
.arg("<a href='https://github.com/Tyrrrz/DiscordChatExporter'>DiscordChatExporter</a>")
onLinkActivated: Global.openLink(link)
HoverHandler {
id: handler1
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: handler1.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
StatusBaseText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Qt.AlignHCenter
linkColor: hoveredLink ? Qt.lighter(Theme.palette.primaryColor1) : Theme.palette.primaryColor1
text: qsTr("Refer to this <a href='https://github.com/Tyrrrz/DiscordChatExporter/wiki'>wiki</a> if you have any queries")
onLinkActivated: Global.openLink(link)
HoverHandler {
id: handler2
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: handler2.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
StatusListView {
visible: !fileListView.fileListModelEmpty
enabled: !root.store.discordDataExtractionInProgress
anchors.fill: parent
anchors.margins: 16
model: fileListView.fileListModel
delegate: ColumnLayout {
width: ListView.view.width
StatusCheckBox {
id: fileCheckBox
Layout.fillWidth: true
text: model.filePath
font.pixelSize: 13
checked: model.selected
enabled: model.errorMessage === "" // TODO distinguish between error/warning
onToggled: model.selected = checked
}
StatusBaseText {
Layout.fillWidth: true
Layout.leftMargin: fileCheckBox.leftPadding + fileCheckBox.spacing + fileCheckBox.indicator.width
text: "%1 %2".arg("⚠").arg(model.errorMessage)
visible: model.errorMessage
font.pixelSize: 13
font.weight: Font.Medium
elide: Text.ElideMiddle
color: Theme.palette.dangerColor1
verticalAlignment: Qt.AlignTop
}
}
}
}
FileDialog {
id: fileDialog
title: qsTr("Choose files to import")
selectMultiple: true
nameFilters: [qsTr("JSON files (%1)").arg("*.json")]
onAccepted: {
if (fileDialog.fileUrls.length > 0) {
let files = []
for (let i = 0; i < fileDialog.fileUrls.length; i++)
files.push(decodeURI(fileDialog.fileUrls[i].toString()))
root.store.setFileListItems(files)
}
}
}
},
ColumnLayout {
id: categoriesAndChannelsView
spacing: 24
readonly property bool canGoNext: root.store.discordChannelsModel.hasSelectedItems
readonly property var nextAction: function () {
d.requestImportDiscordCommunity()
// replace ourselves with the progress dialog, no way back
root.leftButtons[0].visible = false
root.backgroundColor = Theme.palette.baseColor4
root.replace(progressComponent)
}
Component {
id: progressComponent
DiscordImportProgressContents {
width: root.availableWidth
store: root.store
onClose: root.close()
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !root.store.discordChannelsModel.count
Loader {
anchors.centerIn: parent
active: parent.visible
sourceComponent: StatusLoadingIndicator {
width: 50
height: 50
}
}
}
ColumnLayout {
spacing: 12
visible: root.store.discordChannelsModel.count
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Please select the categories and channels you would like to import")
wrapMode: Text.WordWrap
}
RowLayout {
spacing: 20
Layout.fillWidth: true
StatusRadioButton {
text: qsTr("Import all history")
checked: true
}
StatusRadioButton {
id: startDateRadio
text: qsTr("Start date")
}
StatusDatePicker {
id: datePicker
Layout.fillWidth: true
selectedDate: new Date(root.store.discordOldestMessageTimestamp * 1000)
enabled: startDateRadio.checked
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.palette.baseColor4
StatusListView {
anchors.fill: parent
anchors.margins: 16
model: root.store.discordCategoriesModel
delegate: ColumnLayout {
width: ListView.view.width
spacing: 8
StatusCheckBox {
readonly property string categoryId: model.id
id: categoryCheckbox
checked: model.selected
text: model.name
onToggled: root.store.toggleDiscordCategory(categoryId, checked)
}
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Layout.leftMargin: 24
Repeater {
Layout.fillWidth: true
model: root.store.discordChannelsModel
delegate: StatusCheckBox {
width: parent.width
text: model.name
checked: model.selected
visible: model.categoryId === categoryCheckbox.categoryId
onToggled: root.store.toggleDiscordChannel(model.id, checked)
}
}
}
}
}
}
}
}
]
stackItems: [
StatusScrollView {
id: generalView
2020-12-11 20:29:46 +00:00
readonly property bool canGoNext: nameInput.valid && descriptionTextInput.valid
clip: false
ColumnLayout {
id: generalViewLayout
width: generalView.availableWidth
spacing: 16
CommunityNameInput {
id: nameInput
input.edit.objectName: "createCommunityNameInput"
Layout.fillWidth: true
input.tabNavItem: descriptionTextInput.input.edit
}
CommunityDescriptionInput {
id: descriptionTextInput
input.edit.objectName: "createCommunityDescriptionInput"
Layout.fillWidth: true
input.tabNavItem: nameInput.input.edit
}
CommunityLogoPicker {
id: logoPicker
Layout.fillWidth: true
}
CommunityBannerPicker {
id: bannerPicker
Layout.fillWidth: true
}
CommunityColorPicker {
id: colorPicker
onPick: root.replace(colorPanel)
Layout.fillWidth: true
Component {
id: colorPanel
CommunityColorPanel {
Component.onCompleted: color = colorPicker.color
onAccepted: {
colorPicker.color = color;
root.replace(null);
}
}
}
}
2020-12-11 20:29:46 +00:00
CommunityTagsPicker {
id: communityTagsPicker
tags: root.store.communityTags
onPick: root.replace(tagsPanel)
Layout.fillWidth: true
Component {
id: tagsPanel
CommunityTagsPanel {
Component.onCompleted: {
tags = communityTagsPicker.tags;
selectedTags = communityTagsPicker.selectedTags;
}
onAccepted: {
communityTagsPicker.selectedTags = selectedTags;
root.replace(null);
}
}
}
}
StatusModalDivider {
Layout.fillWidth: true
2020-12-11 20:29:46 +00:00
}
CommunityOptions {
id: options
archiveSupportOptionVisible: root.store.isCommunityHistoryArchiveSupportEnabled
archiveSupportEnabled: archiveSupportOptionVisible
}
Item {
Layout.fillHeight: true
}
}
},
ColumnLayout {
id: introOutroMessageView
spacing: 11
readonly property bool canGoNext: introMessageInput.valid && outroMessageInput.valid
CommunityIntroMessageInput {
id: introMessageInput
input.edit.objectName: "createCommunityIntroMessageInput"
input.tabNavItem: outroMessageInput.input.edit
2020-12-17 13:24:33 +00:00
Layout.fillWidth: true
Layout.fillHeight: true
2020-12-11 20:29:46 +00:00
minimumHeight: height
maximumHeight: (height - Style.current.xlPadding)
}
CommunityOutroMessageInput {
id: outroMessageInput
input.edit.objectName: "createCommunityOutroMessageInput"
input.tabNavItem: introMessageInput.input.edit
Layout.fillWidth: true
}
2020-12-11 20:29:46 +00:00
}
]
QtObject {
id: d
function _getCommunityConfig() {
return {
name: StatusQUtils.Utils.filterXSS(nameInput.input.text),
description: StatusQUtils.Utils.filterXSS(descriptionTextInput.input.text),
introMessage: StatusQUtils.Utils.filterXSS(introMessageInput.input.text),
outroMessage: StatusQUtils.Utils.filterXSS(outroMessageInput.input.text),
color: colorPicker.color.toString().toUpperCase(),
tags: communityTagsPicker.selectedTags,
image: {
src: logoPicker.source,
AX: logoPicker.cropRect.x,
AY: logoPicker.cropRect.y,
BX: logoPicker.cropRect.x + logoPicker.cropRect.width,
BY: logoPicker.cropRect.y + logoPicker.cropRect.height,
},
options: {
historyArchiveSupportEnabled: options.archiveSupportEnabled,
checkedMembership: options.requestToJoinEnabled ? Constants.communityChatOnRequestAccess : Constants.communityChatPublicAccess,
pinMessagesAllowedForMembers: options.pinMessagesEnabled
},
bannerJsonStr: JSON.stringify({imagePath: String(bannerPicker.source).replace("file://", ""), cropRect: bannerPicker.cropRect})
}
}
function createCommunity() {
const error = root.store.createCommunity(_getCommunityConfig())
if (error) {
errorDialog.text = error.error
errorDialog.open()
}
root.close()
}
function requestImportDiscordCommunity() {
const error = root.store.requestImportDiscordCommunity(_getCommunityConfig(), datePicker.selectedDate.valueOf()/1000)
if (error) {
errorDialog.text = error.error
errorDialog.open()
}
}
}
MessageDialog {
id: errorDialog
title: qsTr("Error creating the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
2020-12-11 20:29:46 +00:00
}