diff --git a/app/icons/Download.svg b/app/icons/Download.svg
new file mode 100644
index 000000000..b97db411c
--- /dev/null
+++ b/app/icons/Download.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/icons/Info.svg b/app/icons/Info.svg
new file mode 100644
index 000000000..ecf9df01d
--- /dev/null
+++ b/app/icons/Info.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/icons/ProjectButtonMore.svg b/app/icons/ProjectButtonMore.svg
new file mode 100644
index 000000000..d8773b76f
--- /dev/null
+++ b/app/icons/ProjectButtonMore.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/icons/Stop.svg b/app/icons/Stop.svg
new file mode 100644
index 000000000..d022fd4ae
--- /dev/null
+++ b/app/icons/Stop.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/icons/Sync.svg b/app/icons/Sync.svg
new file mode 100644
index 000000000..9fa37fa99
--- /dev/null
+++ b/app/icons/Sync.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/icons/icons.qrc b/app/icons/icons.qrc
index abc0f7b21..8c0b4cec4 100644
--- a/app/icons/icons.qrc
+++ b/app/icons/icons.qrc
@@ -20,6 +20,11 @@
Done.svg
Edit.svg
More.svg
+ ProjectButtonMore.svg
+ Stop.svg
+ Download.svg
+ Info.svg
+ Sync.svg
MorePhotos.svg
diff --git a/app/mmstyle.h b/app/mmstyle.h
index 473d586c4..aa4a7669c 100644
--- a/app/mmstyle.h
+++ b/app/mmstyle.h
@@ -93,6 +93,11 @@ class MMStyle: public QObject
Q_PROPERTY( QUrl doneIcon READ doneIcon CONSTANT )
Q_PROPERTY( QUrl editIcon READ editIcon CONSTANT )
Q_PROPERTY( QUrl moreIcon READ moreIcon CONSTANT )
+ Q_PROPERTY( QUrl projectButtonMoreIcon READ projectButtonMoreIcon CONSTANT )
+ Q_PROPERTY( QUrl stopIcon READ stopIcon CONSTANT )
+ Q_PROPERTY( QUrl syncIcon READ syncIcon CONSTANT )
+ Q_PROPERTY( QUrl infoIcon READ infoIcon CONSTANT )
+ Q_PROPERTY( QUrl downloadIcon READ downloadIcon CONSTANT )
Q_PROPERTY( QUrl morePhotosIcon READ morePhotosIcon CONSTANT )
// Images
@@ -177,6 +182,11 @@ class MMStyle: public QObject
QUrl doneIcon() {return QUrl( "qrc:/Done.svg" );}
QUrl editIcon() {return QUrl( "qrc:/Edit.svg" );}
QUrl moreIcon() {return QUrl( "qrc:/More.svg" );}
+ QUrl projectButtonMoreIcon() {return QUrl( "qrc:/ProjectButtonMore.svg" );}
+ QUrl stopIcon() {return QUrl( "qrc:/Stop.svg" );}
+ QUrl syncIcon() {return QUrl( "qrc:/Sync.svg" );}
+ QUrl infoIcon() {return QUrl( "qrc:/Info.svg" );}
+ QUrl downloadIcon() {return QUrl( "qrc:/Download.svg" );}
QUrl morePhotosIcon() {return QUrl( "qrc:/MorePhotos.svg" );}
QUrl uploadImage() {return QUrl( "qrc:/UploadImage.svg" );}
diff --git a/app/qml/components/MMListDrawer.qml b/app/qml/components/MMListDrawer.qml
new file mode 100644
index 000000000..2ac2bbf1c
--- /dev/null
+++ b/app/qml/components/MMListDrawer.qml
@@ -0,0 +1,107 @@
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Controls.Basic
+
+Drawer {
+ id: control
+
+ property alias title: title.text
+ property alias model: listView.model
+
+ padding: 20 * __dp
+
+ signal clicked(type: string)
+
+ width: window.width
+ height: mainColumn.height
+ edge: Qt.BottomEdge
+
+ Rectangle {
+ color: roundedRect.color
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 2 * radius
+ anchors.topMargin: -radius
+ radius: 20 * __dp
+ }
+
+ Rectangle {
+ id: roundedRect
+
+ anchors.fill: parent
+ color: __style.whiteColor
+
+ Column {
+ id: mainColumn
+
+ width: parent.width
+ spacing: 20 * __dp
+ leftPadding: control.padding
+ rightPadding: control.padding
+ bottomPadding: control.padding
+
+ Row {
+ width: parent.width - 2 * control.padding
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Item { width: closeButton.width; height: 1 }
+
+ Text {
+ id: title
+
+ anchors.verticalCenter: parent.verticalCenter
+ font: __style.t2
+ width: parent.width - closeButton.width * 2
+ color: __style.forestColor
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ }
+
+ Image {
+ id: closeButton
+
+ source: __style.closeButtonIcon
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: control.visible = false
+ }
+ }
+ }
+
+ ListView {
+ id: listView
+
+ width: parent.width - 2 * control.padding
+ height: control.model ? control.model.count * __style.menuDrawerHeight : 0
+ interactive: false
+
+ delegate: Item {
+ width: listView.width
+ height: __style.menuDrawerHeight
+
+ MMListDrawerItem {
+ width: listView.width
+
+ type: model.type
+ text: model.name
+ iconSource: model.iconSource
+
+ onClicked: function(type) { control.clicked(type); control.visible = false }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/qml/components/MMListDrawerItem.qml b/app/qml/components/MMListDrawerItem.qml
new file mode 100644
index 000000000..1e4b1b7fd
--- /dev/null
+++ b/app/qml/components/MMListDrawerItem.qml
@@ -0,0 +1,58 @@
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Controls.Basic
+
+Item {
+ id: control
+
+ signal clicked(type: string)
+
+ required property string type
+ required property string text
+ required property var iconSource
+
+ width: control.width
+ height: __style.menuDrawerHeight
+
+ Rectangle {
+ anchors.top: parent.top
+ width: parent.width
+ height: 1 * __dp
+ color: __style.grayColor
+ }
+
+ Row {
+ height: parent.height
+ width: parent.width
+ spacing: 10 * __dp
+
+ MMIcon {
+ height: parent.height
+ width: 20 * __dp
+ color: __style.forestColor
+ source: control.iconSource ?? ""
+ }
+
+ Text {
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ text: control.text
+ color: __style.nightColor
+ font: __style.t3
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: control.clicked(control.type)
+ }
+}
diff --git a/app/qml/components/MMProjectItem.qml b/app/qml/components/MMProjectItem.qml
new file mode 100644
index 000000000..126394590
--- /dev/null
+++ b/app/qml/components/MMProjectItem.qml
@@ -0,0 +1,257 @@
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ id: root
+
+ required property string projectDisplayName
+ required property string projectId
+ required property string projectDescription
+ required property int projectStatus
+ required property bool projectIsValid
+ required property bool projectIsLocal
+ required property bool projectIsMergin
+ property bool projectIsPending: false
+ property real projectSyncProgress: 0.0
+ property bool highlight: false
+
+ signal openRequested()
+ signal syncRequested()
+ signal migrateRequested()
+ signal removeRequested()
+ signal stopSyncRequested()
+ signal showChangesRequested()
+
+ color: root.highlight ? __style.forestColor : __style.whiteColor
+ radius: 12 * __dp
+ height: mainColumn.height
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.projectIsValid
+ onClicked: openRequested()
+ }
+
+ Column {
+ id: mainColumn
+
+ width: parent.width
+ padding: 20 * __dp
+ spacing: 12 * __dp
+
+ Row {
+ id: row
+
+ spacing: 6 * __dp
+
+ Column {
+ id: column
+
+ spacing: 6 * __dp
+
+ Text {
+ width: mainColumn.width - 2 * mainColumn.padding - icon.width - row.spacing
+
+ text: root.projectDisplayName
+ font: __style.t3
+ color: root.highlight ? __style.whiteColor : __style.nightColor
+ elide: Text.ElideRight
+ opacity: root.projectIsValid ? 1.0 : 0.4
+ }
+
+ Row {
+ width: mainColumn.width - 2 * mainColumn.padding - icon.width - row.spacing
+ spacing: 4 * __dp
+
+ MMIcon {
+ id: errorIcon
+
+ source: visible ? __style.errorIcon : ""
+ color: __style.negativeColor
+ visible: !root.projectIsValid
+ }
+
+ Text {
+ width: parent.width - errorIcon.width
+
+ text: root.projectDescription
+ font: __style.p6
+ elide: Text.ElideRight
+ color: {
+ if(root.projectIsValid) {
+ if(root.highlight) {
+ return __style.whiteColor
+ }
+ return __style.nightColor
+ }
+ return __style.grapeColor
+ }
+ }
+ }
+ }
+
+ Image {
+ id: icon
+
+ height: column.height
+ width: height
+
+ source: __style.projectButtonMoreIcon
+ fillMode: Image.PreserveAspectFit
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ root.fillMoreMenu()
+ listDrawer.visible = true
+ }
+ }
+ }
+ }
+
+ Item {
+ height: root.projectIsPending ? syncColumn.height : 0
+ width: parent.width
+ clip: true
+
+ Behavior on height {
+ NumberAnimation { duration: 250 }
+ }
+
+ Column {
+ id: syncColumn
+
+ spacing: mainColumn.spacing
+
+ MMProgressBar {
+ position: root.projectSyncProgress
+ width: mainColumn.width - 2 * mainColumn.padding
+ }
+
+ Row {
+ id: syncRow
+
+ spacing: 6 * __dp
+
+ Text {
+ width: mainColumn.width - 2 * mainColumn.padding - stopIcon.width - syncRow.spacing * 2 - stopText.width
+ anchors.verticalCenter: parent.verticalCenter
+
+ text: qsTr("Synchronising...")
+ font: __style.p6
+ color: root.highlight ? __style.whiteColor : __style.nightColor
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ MMIcon {
+ id: stopIcon
+
+ anchors.verticalCenter: parent.verticalCenter
+
+ source: __style.stopIcon
+ color: root.highlight ? __style.whiteColor : __style.forestColor
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.stopSyncRequested()
+ }
+ }
+
+ Text {
+ id: stopText
+
+ anchors.verticalCenter: parent.verticalCenter
+
+ text: qsTr("Stop")
+ font: __style.t4
+ color: root.highlight ? __style.whiteColor : __style.nightColor
+ verticalAlignment: Text.AlignVCenter
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.stopSyncRequested()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ MMListDrawer {
+ id: listDrawer
+
+ title: qsTr("More options")
+ model: ListModel {
+ id: menuModel
+ }
+
+ onClicked: function(type) {
+ console.log(type)
+ switch(type) {
+ case "download": root.syncRequested(); break
+ case "sync": root.syncRequested(); break
+ case "changes": root.showChangesRequested(); break
+ case "remove": root.removeRequested(); break
+ case "upload": root.migrateRequested(); break
+ }
+ }
+ }
+
+ function getMoreMenuItems() {
+ if ( projectIsMergin && projectIsLocal )
+ {
+ if ( ( projectStatus === 2 /*ProjectStatus.NeedsSync*/ ) ) { // uncomment when using this component
+ return "sync,changes,remove"
+ }
+ return "changes,remove"
+ }
+ else if ( !projectIsMergin && projectIsLocal ) {
+ return "upload,remove"
+ }
+ return "download"
+ }
+
+ function fillMoreMenu() {
+ // fill more menu with corresponding items
+ let itemsMap = {
+ "download": {
+ "name": qsTr("Download from Mergin"),
+ "iconSource": __style.downloadIcon
+ },
+ "sync": {
+ "name": qsTr("Synchronize project"),
+ "iconSource": __style.syncIcon
+ },
+ "changes": {
+ "name": qsTr("Local changes"),
+ "iconSource": __style.infoIcon
+ },
+ "remove": {
+ "name": qsTr("Remove from device"),
+ "iconSource": __style.deleteIcon
+ },
+ "upload": {
+ "name": qsTr("Upload to Mergin"),
+ "iconSource": __style.uploadIcon
+ }
+ }
+
+ let items = getMoreMenuItems().split(',')
+ menuModel.clear()
+ items.forEach( function(item) {
+ var json = itemsMap[item]
+ json.type = item
+ menuModel.append( json )
+ } )
+ }
+}
diff --git a/gallery/qml.qrc b/gallery/qml.qrc
index 3746020d5..8127f6ea2 100644
--- a/gallery/qml.qrc
+++ b/gallery/qml.qrc
@@ -37,6 +37,10 @@
../app/qml/components/MMMenuDrawer.qml
../app/qml/components/MMToolbarMenuButton.qml
../app/qml/components/MMToolbarLongButton.qml
+ qml/pages/ProjectItemsPage.qml
+ ../app/qml/components/MMListDrawer.qml
+ ../app/qml/components/MMListDrawerItem.qml
+ ../app/qml/components/MMProjectItem.qml
../app/qml/components/MMMorePhoto.qml
../app/qml/components/MMPhoto.qml
../app/qml/components/MMPhotoGallery.qml
diff --git a/gallery/qml/Main.qml b/gallery/qml/Main.qml
index 4819ecfb1..2bf7d479c 100644
--- a/gallery/qml/Main.qml
+++ b/gallery/qml/Main.qml
@@ -148,6 +148,10 @@ ApplicationWindow {
title: "Toolbars"
source: "ToolbarPage.qml"
}
+ ListElement {
+ title: "Project items"
+ source: "ProjectItemsPage.qml"
+ }
}
ScrollIndicator.vertical: ScrollIndicator {}
diff --git a/gallery/qml/pages/ProjectItemsPage.qml b/gallery/qml/pages/ProjectItemsPage.qml
new file mode 100644
index 000000000..f7fa27708
--- /dev/null
+++ b/gallery/qml/pages/ProjectItemsPage.qml
@@ -0,0 +1,107 @@
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import QtQuick.Controls
+import "../../app/qml/components"
+
+Page {
+ id: pane
+
+ Column {
+ width: parent.width
+ anchors.top: parent.top
+ anchors.topMargin: 20
+ anchors.left: parent.left
+ anchors.leftMargin: 20
+ spacing: 20
+
+ MMProjectItem {
+ width: 350
+
+ highlight: true
+ projectId: "1"
+ projectStatus: 2
+ projectDisplayName: "Mergin local project"
+ projectDescription: "Highlighted"
+ projectIsValid: true
+ projectIsLocal: true
+ projectIsMergin: true
+ projectIsPending: true
+
+ onOpenRequested: console.log("onOpenRequested")
+ onStopSyncRequested: projectIsPending = false
+ onShowChangesRequested: console.log("onShowChangesRequested")
+ onSyncRequested: projectIsPending = true
+ onRemoveRequested: console.log("onRemoveRequested")
+ onMigrateRequested: console.log("onMigrateRequested")
+ }
+
+ MMProjectItem {
+ width: 350
+
+ highlight: false
+ projectId: "1"
+ projectStatus: 2
+ projectDisplayName: "Mergin local project"
+ projectDescription: "Highlighted"
+ projectIsValid: true
+ projectIsLocal: true
+ projectIsMergin: true
+
+ onOpenRequested: console.log("onOpenRequested")
+ onStopSyncRequested: projectIsPending = false
+ onShowChangesRequested: console.log("onShowChangesRequested")
+ onSyncRequested: projectIsPending = true
+ onRemoveRequested: console.log("onRemoveRequested")
+ onMigrateRequested: console.log("onMigrateRequested")
+ }
+
+ MMProjectItem {
+ width: 350
+
+ highlight: false
+ projectId: "1"
+ projectStatus: 2
+ projectDisplayName: "Mergin local project Long Long Long Long Long Long Long"
+ projectDescription: "Description Description Description Description Description"
+ projectIsValid: true
+ projectIsLocal: true
+ projectIsMergin: true
+
+ onOpenRequested: console.log("onOpenRequested")
+ onStopSyncRequested: projectIsPending = false
+ onShowChangesRequested: console.log("onShowChangesRequested")
+ onSyncRequested: projectIsPending = true
+ onRemoveRequested: console.log("onRemoveRequested")
+ onMigrateRequested: console.log("onMigrateRequested")
+ }
+
+ MMProjectItem {
+ width: 350
+
+ highlight: false
+ projectId: "2"
+ projectStatus: 0
+ projectDisplayName: "Invalid project"
+ projectDescription: "A project error. A project error. A project error."
+ projectIsValid: false
+ projectIsLocal: false
+ projectIsMergin: false
+
+ onOpenRequested: console.log("onOpenRequested")
+ onStopSyncRequested: console.log("onStopSyncRequested")
+ onShowChangesRequested: console.log("onShowChangesRequested")
+ onSyncRequested: { console.log("onSyncRequested"); projectIsPending = true }
+ onRemoveRequested: console.log("onRemoveRequested")
+ onMigrateRequested: console.log("onMigrateRequested")
+ }
+
+ }
+}