619 lines
26 KiB
QML
619 lines
26 KiB
QML
import QtQuick 2.15
|
||
import QtQuick.Layouts 1.15
|
||
import QtQuick.Controls 2.15
|
||
import QtQuick.Dialogs 1.3
|
||
import QtQml.Models 2.15
|
||
|
||
import utils 1.0
|
||
import shared.panels 1.0
|
||
|
||
import StatusQ.Core 0.1
|
||
import StatusQ.Core.Theme 0.1
|
||
import StatusQ.Core.Utils 0.1 as StatusQUtils
|
||
import StatusQ.Controls 0.1
|
||
import StatusQ.Controls.Validators 0.1
|
||
import StatusQ.Components 0.1
|
||
import StatusQ.Popups 0.1
|
||
|
||
import AppLayouts.Communities.controls 1.0
|
||
import AppLayouts.Communities.panels 1.0
|
||
|
||
StatusStackModal {
|
||
id: root
|
||
|
||
property var communitiesStore
|
||
|
||
property bool isDiscordImport // creating new or importing from discord?
|
||
property bool isEdit: false
|
||
property bool isDeleteable: false
|
||
|
||
property string chatId: ""
|
||
property string categoryId: ""
|
||
property string channelName: ""
|
||
property string channelDescription: ""
|
||
property string channelEmoji: ""
|
||
property string channelColor: ""
|
||
property bool emojiPopupOpened: false
|
||
property var emojiPopup: null
|
||
readonly property int communityColorValidator: Utils.Validate.NoEmpty
|
||
| Utils.Validate.TextHexColor
|
||
|
||
readonly property int maxChannelNameLength: 24
|
||
readonly property int maxChannelDescLength: 140
|
||
|
||
signal createCommunityChannel(string chName, string chDescription, string chEmoji, string chColor, string chCategoryId)
|
||
signal editCommunityChannel(string chName, string chDescription, string chEmoji, string chColor, string chCategoryId)
|
||
signal deleteCommunityChannel()
|
||
|
||
width: 640
|
||
|
||
QtObject {
|
||
id: d
|
||
function isFormValid() {
|
||
return nameInput.valid && descriptionTextArea.valid &&
|
||
Utils.validateAndReturnError(colorDialog.color.toString().toUpperCase(), communityColorValidator) === ""
|
||
}
|
||
|
||
function openEmojiPopup(leftSide = false) {
|
||
root.emojiPopupOpened = true;
|
||
root.emojiPopup.open();
|
||
root.emojiPopup.emojiSize = StatusQUtils.Emoji.size.verySmall;
|
||
root.emojiPopup.x = leftSide ? root.x + Style.current.padding : (root.x + (root.width - root.emojiPopup.width - Style.current.padding));
|
||
root.emojiPopup.y = root.y + root.header.height + root.topPadding + nameInput.height + Style.current.smallPadding;
|
||
}
|
||
|
||
function _getChannelConfig() {
|
||
return {
|
||
discordChannelId: root.communitiesStore.discordImportChannelId,
|
||
categoryId: root.categoryId,
|
||
name: StatusQUtils.Utils.filterXSS(nameInput.input.text),
|
||
description: StatusQUtils.Utils.filterXSS(descriptionTextArea.text),
|
||
color: colorDialog.color.toString().toUpperCase(),
|
||
emoji: StatusQUtils.Emoji.deparse(nameInput.input.asset.emoji),
|
||
options: {
|
||
// TODO
|
||
}
|
||
}
|
||
}
|
||
|
||
function requestImportDiscordChannel() {
|
||
const error = root.communitiesStore.requestImportDiscordChannel(_getChannelConfig(), datePicker.selectedDate.valueOf()/1000)
|
||
if (error) {
|
||
creatingError.text = error.error
|
||
creatingError.open()
|
||
}
|
||
}
|
||
}
|
||
|
||
stackTitle: isDiscordImport ? qsTr("New Channel With Imported Chat History") :
|
||
isEdit ? qsTr("Edit #%1").arg(root.channelName)
|
||
: qsTr("New channel")
|
||
|
||
nextButton: StatusButton {
|
||
objectName: "createChannelNextBtn"
|
||
font.weight: Font.Medium
|
||
text: typeof currentItem.nextButtonText !== "undefined" ? currentItem.nextButtonText : qsTr("Import chat history")
|
||
enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext
|
||
loading: root.communitiesStore.discordDataExtractionInProgress
|
||
onClicked: {
|
||
const nextAction = currentItem.nextAction
|
||
if (typeof(nextAction) == "function") {
|
||
return nextAction()
|
||
}
|
||
root.currentIndex++
|
||
}
|
||
}
|
||
|
||
finishButton: StatusButton {
|
||
objectName: "createOrEditCommunityChannelBtn"
|
||
font.weight: Font.Medium
|
||
text: isDiscordImport ? qsTr("Import chat history") : isEdit ? qsTr("Save changes") : qsTr("Create channel")
|
||
enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext
|
||
onClicked: {
|
||
let nextAction = currentItem.nextAction
|
||
if (typeof (nextAction) == "function") {
|
||
return nextAction()
|
||
}
|
||
if (!root.isDiscordImport) {
|
||
if (!d.isFormValid()) {
|
||
scrollView.scrollBackUp()
|
||
return
|
||
}
|
||
let emoji = StatusQUtils.Emoji.deparse(nameInput.input.asset.emoji)
|
||
|
||
if (!isEdit) {
|
||
root.createCommunityChannel(StatusQUtils.Utils.filterXSS(nameInput.input.text),
|
||
StatusQUtils.Utils.filterXSS(descriptionTextArea.text),
|
||
emoji,
|
||
colorDialog.color.toString().toUpperCase(),
|
||
root.categoryId)
|
||
} else {
|
||
root.editCommunityChannel(StatusQUtils.Utils.filterXSS(nameInput.input.text),
|
||
StatusQUtils.Utils.filterXSS(descriptionTextArea.text),
|
||
emoji,
|
||
colorDialog.color.toString().toUpperCase(),
|
||
root.categoryId)
|
||
}
|
||
// TODO Open the channel once we have designs for it
|
||
root.close()
|
||
}
|
||
}
|
||
}
|
||
|
||
readonly property StatusButton clearFilesButton: StatusButton {
|
||
font.weight: Font.Medium
|
||
text: qsTr("Clear all")
|
||
type: StatusBaseButton.Type.Danger
|
||
visible: typeof currentItem.isFileListView !== "undefined" && currentItem.isFileListView
|
||
enabled: !fileListView.fileListModelEmpty && !root.communitiesStore.discordDataExtractionInProgress
|
||
onClicked: root.communitiesStore.clearFileList()
|
||
}
|
||
|
||
readonly property StatusButton deleteChannelButton: StatusButton {
|
||
objectName: "deleteCommunityChannelBtn"
|
||
visible: isEdit && isDeleteable && !isDiscordImport && typeof(replaceItem) === "undefined"
|
||
text: qsTr("Delete channel")
|
||
type: StatusBaseButton.Type.Danger
|
||
onClicked: root.deleteCommunityChannel()
|
||
}
|
||
|
||
rightButtons: [clearFilesButton, deleteChannelButton, nextButton, finishButton]
|
||
|
||
onAboutToShow: {
|
||
if (root.isDiscordImport) {
|
||
if (!root.communitiesStore.discordImportInProgress) {
|
||
root.communitiesStore.clearFileList()
|
||
root.communitiesStore.clearDiscordCategoriesAndChannels()
|
||
}
|
||
for (let i = 0; i < discordPages.length; i++) {
|
||
stackItems.push(discordPages[i])
|
||
}
|
||
}
|
||
|
||
nameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason)
|
||
if (isEdit) {
|
||
nameInput.text = root.channelName
|
||
descriptionTextArea.text = root.channelDescription
|
||
if (root.channelEmoji) {
|
||
nameInput.input.asset.emoji = root.channelEmoji
|
||
}
|
||
colorDialog.color = root.channelColor
|
||
} else {
|
||
nameInput.input.asset.isLetterIdenticon = true;
|
||
}
|
||
|
||
updateRightButtons()
|
||
}
|
||
|
||
readonly property list<Item> discordPages: [
|
||
ColumnLayout {
|
||
id: fileListView
|
||
spacing: 24
|
||
|
||
readonly property bool isFileListView: true
|
||
|
||
readonly property var fileListModel: root.communitiesStore.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 && fileListModel.selectedCount === fileListModel.count ? qsTr("Validate %n file(s)", "", fileListModel.selectedCount)
|
||
: fileListModel.selectedCount ? qsTr("Validate (%1/%2) files").arg(fileListModel.selectedCount).arg(fileListModel.count)
|
||
: qsTr("Start channel import")
|
||
readonly property var nextAction: function () {
|
||
if (!fileListView.fileListModel.selectedFilesValid)
|
||
return root.communitiesStore.requestExtractChannelsAndCategories()
|
||
|
||
root.currentIndex++
|
||
}
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 12
|
||
StatusBaseText {
|
||
Layout.fillWidth: true
|
||
maximumLineCount: 2
|
||
wrapMode: Text.Wrap
|
||
elide: Text.ElideRight
|
||
text: fileListView.fileListModelEmpty ? qsTr("Select Discord channel JSON files to import") :
|
||
root.communitiesStore.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 && !issuePill.visible
|
||
font.pixelSize: 12
|
||
color: Theme.palette.baseColor1
|
||
text: qsTr("(JSON file format only)")
|
||
}
|
||
IssuePill {
|
||
id: issuePill
|
||
type: root.communitiesStore.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning
|
||
count: root.communitiesStore.discordImportErrorsCount || root.communitiesStore.discordImportWarningsCount || 0
|
||
visible: !!count && !fileListView.fileListModelEmpty
|
||
}
|
||
StatusButton {
|
||
Layout.alignment: Qt.AlignRight
|
||
text: qsTr("Browse files")
|
||
type: StatusBaseButton.Type.Primary
|
||
onClicked: fileDialog.open()
|
||
enabled: !root.communitiesStore.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
|
||
text: qsTr("Export the Discord channel’s chat history data using %1").arg("<a href='https://github.com/Tyrrrz/DiscordChatExporter/releases/tag/2.40.4'>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
|
||
text: qsTr("Refer to this <a href='https://github.com/Tyrrrz/DiscordChatExporter/blob/master/.docs/Readme.md'>documentation</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
|
||
}
|
||
}
|
||
}
|
||
|
||
Component {
|
||
id: floatingDivComp
|
||
Rectangle {
|
||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||
width: ListView.view ? ListView.view.width : 0
|
||
height: 4
|
||
color: Theme.palette.directColor8
|
||
}
|
||
}
|
||
|
||
StatusListView {
|
||
visible: !fileListView.fileListModelEmpty
|
||
enabled: !root.communitiesStore.discordDataExtractionInProgress
|
||
anchors.fill: parent
|
||
leftMargin: 8
|
||
rightMargin: 8
|
||
model: fileListView.fileListModel
|
||
header: !atYBeginning ? floatingDivComp : null
|
||
headerPositioning: ListView.OverlayHeader
|
||
footer: !atYEnd ? floatingDivComp : null
|
||
footerPositioning: ListView.OverlayHeader
|
||
delegate: ColumnLayout {
|
||
width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin
|
||
RowLayout {
|
||
spacing: 20
|
||
Layout.fillWidth: true
|
||
Layout.topMargin: 8
|
||
StatusBaseText {
|
||
Layout.fillWidth: true
|
||
text: model.filePath
|
||
font.pixelSize: 13
|
||
elide: Text.ElideRight
|
||
wrapMode: Text.WordWrap
|
||
maximumLineCount: 2
|
||
}
|
||
|
||
StatusFlatRoundButton {
|
||
Layout.preferredWidth: 32
|
||
Layout.preferredHeight: 32
|
||
type: StatusFlatRoundButton.Type.Secondary
|
||
icon.name: "close"
|
||
icon.color: Theme.palette.directColor1
|
||
icon.width: 24
|
||
icon.height: 24
|
||
onClicked: root.communitiesStore.removeFileListItem(model.filePath)
|
||
}
|
||
}
|
||
|
||
StatusBaseText {
|
||
Layout.fillWidth: true
|
||
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 *.JSON")]
|
||
onAccepted: {
|
||
if (fileDialog.fileUrls.length > 0) {
|
||
const files = []
|
||
for (let i = 0; i < fileDialog.fileUrls.length; i++)
|
||
files.push(decodeURI(fileDialog.fileUrls[i].toString()))
|
||
root.communitiesStore.setFileListItems(files)
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
ColumnLayout {
|
||
id: categoriesAndChannelsView
|
||
spacing: 24
|
||
|
||
readonly property bool canGoNext: root.communitiesStore.discordChannelsModel.hasSelectedItems
|
||
readonly property var nextAction: function () {
|
||
d.requestImportDiscordChannel()
|
||
// 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.communitiesStore
|
||
importingSingleChannel: true
|
||
onClose: root.close()
|
||
}
|
||
}
|
||
|
||
Item {
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
visible: !root.communitiesStore.discordChannelsModel.count
|
||
Loader {
|
||
anchors.centerIn: parent
|
||
active: parent.visible
|
||
sourceComponent: StatusLoadingIndicator {
|
||
width: 50
|
||
height: 50
|
||
}
|
||
}
|
||
}
|
||
|
||
ColumnLayout {
|
||
spacing: 12
|
||
visible: root.communitiesStore.discordChannelsModel.count
|
||
|
||
StatusBaseText {
|
||
Layout.fillWidth: true
|
||
text: qsTr("Select the chat history you would like to import into #%1...").arg(StatusQUtils.Utils.filterXSS(nameInput.input.text))
|
||
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.communitiesStore.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.communitiesStore.discordCategoriesModel
|
||
delegate: ColumnLayout {
|
||
width: ListView.view.width
|
||
spacing: 8
|
||
|
||
StatusBaseText {
|
||
readonly property string categoryId: model.id
|
||
id: categoryCheckbox
|
||
text: model.name
|
||
}
|
||
|
||
ColumnLayout {
|
||
spacing: 8
|
||
Layout.fillWidth: true
|
||
Layout.leftMargin: 24
|
||
Repeater {
|
||
Layout.fillWidth: true
|
||
model: root.communitiesStore.discordChannelsModel
|
||
delegate: StatusRadioButton {
|
||
width: parent.width
|
||
text: model.name
|
||
checked: model.selected
|
||
visible: model.categoryId === categoryCheckbox.categoryId
|
||
onToggled: root.communitiesStore.toggleOneDiscordChannel(model.id)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
]
|
||
|
||
Connections {
|
||
enabled: root.opened && root.emojiPopupOpened
|
||
target: emojiPopup
|
||
|
||
function onEmojiSelected(emojiText: string, atCursor: bool) {
|
||
nameInput.input.asset.isLetterIdenticon = false;
|
||
nameInput.input.asset.emoji = emojiText
|
||
}
|
||
function onClosed() {
|
||
root.emojiPopupOpened = false
|
||
}
|
||
}
|
||
|
||
stackItems: [
|
||
StatusScrollView {
|
||
id: scrollView
|
||
|
||
readonly property bool canGoNext: d.isFormValid()
|
||
|
||
property ScrollBar vScrollBar: ScrollBar.vertical
|
||
|
||
contentWidth: availableWidth
|
||
padding: 0
|
||
|
||
function scrollBackUp() {
|
||
vScrollBar.setPosition(0)
|
||
}
|
||
|
||
ColumnLayout {
|
||
id: content
|
||
|
||
width: scrollView.availableWidth
|
||
spacing: 0
|
||
|
||
StatusInput {
|
||
id: nameInput
|
||
Layout.fillWidth: true
|
||
input.edit.objectName: "createOrEditCommunityChannelNameInput"
|
||
label: qsTr("Channel name")
|
||
charLimit: root.maxChannelNameLength
|
||
placeholderText: qsTr("# Name the channel")
|
||
|
||
input.onTextChanged: {
|
||
const cursorPosition = input.cursorPosition
|
||
input.text = Utils.convertSpacesToDashes(input.text)
|
||
input.cursorPosition = cursorPosition
|
||
if (root.channelEmoji === "") {
|
||
input.letterIconName = text
|
||
}
|
||
}
|
||
input.asset.color: colorDialog.color.toString()
|
||
input.rightComponent: StatusRoundButton {
|
||
objectName: "StatusChannelPopup_emojiButton"
|
||
implicitWidth: 32
|
||
implicitHeight: 32
|
||
icon.width: 20
|
||
icon.height: 20
|
||
icon.name: "smiley"
|
||
onClicked: d.openEmojiPopup()
|
||
}
|
||
onIconClicked: {
|
||
d.openEmojiPopup(true);
|
||
}
|
||
|
||
validators: [
|
||
StatusMinLengthValidator {
|
||
minLength: 1
|
||
errorMessage: Utils.getErrorMessage(nameInput.errors, qsTr("channel name"))
|
||
},
|
||
StatusRegularExpressionValidator {
|
||
regularExpression: Constants.regularExpressions.alphanumericalExpanded
|
||
errorMessage: Constants.errorMessages.alphanumericalExpandedRegExp
|
||
}
|
||
]
|
||
}
|
||
|
||
Item {
|
||
Layout.preferredHeight: 16
|
||
Layout.fillWidth: true
|
||
}
|
||
|
||
ColorPicker {
|
||
id: colorDialog
|
||
Layout.fillWidth: true
|
||
title: qsTr("Channel colour")
|
||
color: root.isEdit && root.channelColor ? root.channelColor : Theme.palette.primaryColor1
|
||
onPick: root.replace(colorPanel)
|
||
|
||
Component {
|
||
id: colorPanel
|
||
ColorPanel {
|
||
title: qsTr("Channel colour")
|
||
buttonText: qsTr("Select Colour")
|
||
Component.onCompleted: color = colorDialog.color
|
||
onAccepted: {
|
||
colorDialog.color = color
|
||
root.replaceItem = undefined
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Item {
|
||
Layout.preferredHeight: 16
|
||
Layout.fillWidth: true
|
||
}
|
||
|
||
StatusInput {
|
||
id: descriptionTextArea
|
||
Layout.fillWidth: true
|
||
input.edit.objectName: "createOrEditCommunityChannelDescriptionInput"
|
||
input.verticalAlignment: TextEdit.AlignTop
|
||
label: qsTr("Description")
|
||
charLimit: 140
|
||
|
||
placeholderText: qsTr("Describe the channel")
|
||
input.multiline: true
|
||
minimumHeight: 88
|
||
maximumHeight: 88
|
||
validators: [
|
||
StatusMinLengthValidator {
|
||
minLength: 1
|
||
errorMessage: Utils.getErrorMessage(descriptionTextArea.errors, qsTr("channel description"))
|
||
},
|
||
StatusRegularExpressionValidator {
|
||
regularExpression: Constants.regularExpressions.alphanumericalExpanded
|
||
errorMessage: Constants.errorMessages.alphanumericalExpandedRegExp
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
|
||
MessageDialog {
|
||
id: creatingError
|
||
title: qsTr("Error creating the channel")
|
||
icon: StandardIcon.Critical
|
||
standardButtons: StandardButton.Ok
|
||
}
|
||
}
|