diff --git a/Mergin/configure_sync_wizard.py b/Mergin/configure_sync_wizard.py
new file mode 100644
index 00000000..fd1d0ef9
--- /dev/null
+++ b/Mergin/configure_sync_wizard.py
@@ -0,0 +1,239 @@
+import os
+
+from qgis.PyQt import uic
+from qgis.PyQt.QtCore import QSettings
+from qgis.PyQt.QtWidgets import QWizard, QFileDialog
+
+from qgis.gui import QgsFileWidget
+from qgis.core import QgsProject, QgsProviderRegistry, QgsApplication, QgsAuthMethodConfig
+
+from .utils import get_mergin_auth
+
+base_dir = os.path.dirname(__file__)
+ui_direction_page, base_direction_page = uic.loadUiType(os.path.join(base_dir, "ui", "ui_sync_direction_page.ui"))
+ui_gpkg_select_page, base_gpkg_select_page = uic.loadUiType(os.path.join(base_dir, "ui", "ui_gpkg_selection_page.ui"))
+ui_db_select_page, base_db_select_page = uic.loadUiType(os.path.join(base_dir, "ui", "ui_db_selection_page.ui"))
+ui_config_page, base_config_page = uic.loadUiType(os.path.join(base_dir, "ui", "ui_config_file_page.ui"))
+
+SYNC_DIRECTION_PAGE = 0
+GPKG_SELECT_PAGE = 1
+DB_SELECT_PAGE = 2
+CONFIG_PAGE = 3
+
+
+class SyncDirectionPage(ui_direction_page, base_direction_page):
+ """Initial wizard page with sync direction selector."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setupUi(self)
+ self.parent = parent
+
+ self.ledit_sync_direction.hide()
+ self.registerField("init_from*", self.ledit_sync_direction)
+
+ self.radio_from_project.toggled.connect(self.update_direction)
+ self.radio_from_db.toggled.connect(self.update_direction)
+
+ def update_direction(self, checked):
+ if self.radio_from_project.isChecked():
+ self.ledit_sync_direction.setText("gpkg")
+ else:
+ self.ledit_sync_direction.setText("db")
+
+ def nextId(self):
+ """Decide about the next page based on selected sync direction."""
+ if self.radio_from_project.isChecked():
+ return GPKG_SELECT_PAGE
+ return DB_SELECT_PAGE
+
+
+class GpkgSelectionPage(ui_gpkg_select_page, base_gpkg_select_page):
+ """Wizard page for selecting GPKG file."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setupUi(self)
+ self.parent = parent
+
+ self.ledit_gpkg_file.hide()
+ self.registerField("sync_file", self.ledit_gpkg_file)
+
+ def initializePage(self):
+ direction = self.field("init_from")
+ if direction == "gpkg":
+ self.label.setText("Pick a GeoPackage file that contains data to be synchronized")
+ self.file_edit_gpkg.setStorageMode(QgsFileWidget.GetFile)
+ else:
+ self.label.setText("Pick a GeoPackage file that will contain synchronized data")
+ self.file_edit_gpkg.setStorageMode(QgsFileWidget.SaveFile)
+
+ self.file_edit_gpkg.setDialogTitle(self.tr('Select file'))
+ settings = QSettings()
+ self.file_edit_gpkg.setDefaultRoot(settings.value('Mergin/lastUsedDirectory', QgsProject.instance().homePath(), str))
+ self.file_edit_gpkg.setFilter('GeoPackage files (*.gpkg *.GPKG)')
+ self.file_edit_gpkg.fileChanged.connect(self.ledit_gpkg_file.setText)
+
+ def nextId(self):
+ """Decide about the next page based on selected sync direction."""
+ direction = self.field("init_from")
+ if direction == "gpkg":
+ return DB_SELECT_PAGE
+ return CONFIG_PAGE
+
+
+class DatabaseSelectionPage(ui_db_select_page, base_db_select_page):
+ """Wizard page for selecting database and schema."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setupUi(self)
+ self.parent = parent
+
+ self.ledit_sync_schema.hide()
+ self.populate_connections()
+
+ self.registerField("connection*", self.cmb_db_conn, "currentText", self.cmb_db_conn.currentTextChanged)
+ self.registerField("sync_schema*", self.ledit_sync_schema)
+ self.registerField("internal_schema*", self.line_edit_internal_schema)
+ self.cmb_db_conn.currentTextChanged.connect(self.populate_schemas)
+
+ def initializePage(self):
+ self.direction = self.field("init_from")
+ if self.direction == "gpkg":
+ self.label_sync_schema.setText("Schema name for sync (will be created)")
+ # use line edit for schema name
+ self.stackedWidget.setCurrentIndex(0)
+ self.line_edit_sync_schema.textChanged.connect(self.schema_changed)
+ else:
+ self.label_sync_schema.setText("Existing schema name for sync")
+ # use combobox to select existing schema
+ self.stackedWidget.setCurrentIndex(1)
+ self.cmb_sync_schema.currentTextChanged.connect(self.schema_changed)
+
+ # pre-fill internal schema name
+ self.line_edit_internal_schema.setText("merginmaps_db_sync")
+
+ def cleanupPage(self):
+ if self.direction == "gpkg":
+ self.line_edit_sync_schema.textChanged.disconnect()
+ else:
+ self.cmb_sync_schema.currentTextChanged.disconnect()
+
+ def schema_changed(self, schema_name):
+ self.line_edit_internal_schema.setText(f"{schema_name}_db_sync")
+ self.ledit_sync_schema.setText(schema_name)
+
+ def populate_connections(self):
+ metadata = QgsProviderRegistry.instance().providerMetadata("postgres")
+ connections = metadata.dbConnections()
+ for k, v in connections.items():
+ self.cmb_db_conn.addItem(k, v)
+
+ self.cmb_db_conn.setCurrentIndex(-1)
+
+ def populate_schemas(self):
+ connection = self.cmb_db_conn.currentData()
+ if connection:
+ self.cmb_sync_schema.clear()
+ self.cmb_sync_schema.addItems(connection.schemas())
+
+ def nextId(self):
+ """Decide about the next page based on selected sync direction."""
+ if self.direction == "gpkg":
+ return CONFIG_PAGE
+ return GPKG_SELECT_PAGE
+
+
+class ConfigFilePage(ui_config_page, base_config_page):
+ """Wizard page with generated config file."""
+
+ def __init__(self, project_name, parent=None):
+ super().__init__(parent)
+ self.setupUi(self)
+ self.parent = parent
+
+ self.project_name = project_name
+
+ self.btn_save_config.clicked.connect(self.save_config)
+
+ def initializePage(self):
+ self.text_config_file.setPlainText(self.generate_config())
+
+ def save_config(self):
+ file_path, _ = QFileDialog.getSaveFileName(self, "Save file", os.path.expanduser('~'), "YAML files (*.yml *.YML)")
+ if file_path:
+ if not file_path.lower().endswith(".yml"):
+ file_path += ".yml"
+
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(self.text_config_file.toPlainText())
+
+ def generate_config(self):
+ url, user, password = get_mergin_auth()
+ conn_name = self.field("connection")
+ metadata = QgsProviderRegistry.instance().providerMetadata("postgres")
+ conn = metadata.dbConnections()[conn_name]
+ decoded_uri = metadata.decodeUri(conn.uri())
+ conn_string = []
+ if "host" in decoded_uri:
+ conn_string.append(f"host={decoded_uri['host']}")
+ if "dbname" in decoded_uri:
+ conn_string.append(f"dbname={decoded_uri['dbname']}")
+
+ if "authcfg" in decoded_uri:
+ auth_id = decoded_uri["authcfg"]
+ auth_manager = QgsApplication.authManager()
+ auth_config = QgsAuthMethodConfig()
+ auth_manager.loadAuthenticationConfig(auth_id, auth_config, True)
+ conn_string.append(f"user={auth_config.config('username')}")
+ conn_string.append(f"password={auth_config.config('password')}")
+ else:
+ if "username" in decoded_uri:
+ user_name = decoded_uri['username'].strip("'")
+ conn_string.append(f"user={user_name}")
+ if "password" in decoded_uri:
+ password = decoded_uri['password'].strip("'")
+ conn_string.append(f"password={password}")
+
+ cfg = ("mergin:\n"
+ f" url: {url}\n"
+ f" username: {user}\n"
+ f" password: {password}\n"
+ f"init_from: {self.field('init_from')}\n"
+ "connections:\n"
+ f" - driver: postgres\n"
+ f" conn_info: \"{' '.join(conn_string)}\"\n"
+ f" modified: {self.field('sync_schema')}\n"
+ f" base: {self.field('internal_schema')}\n"
+ f" mergin_project: {self.project_name}\n"
+ f" sync_file: {self.field('sync_file')}\n"
+ f"daemon:\n"
+ f" sleep_time: 10\n"
+ )
+
+ return cfg
+
+
+class DbSyncConfigWizard(QWizard):
+ """Wizard for configuring db-sync."""
+
+ def __init__(self, project_name, parent=None):
+ """Create a wizard"""
+ super().__init__(parent)
+
+ self.setWindowTitle("Create db-sync configuration")
+
+ self.project_name = project_name
+
+ self.start_page = SyncDirectionPage(self)
+ self.setPage(SYNC_DIRECTION_PAGE, self.start_page)
+
+ self.gpkg_page = GpkgSelectionPage(parent=self)
+ self.setPage(GPKG_SELECT_PAGE, self.gpkg_page)
+
+ self.db_page = DatabaseSelectionPage(parent=self)
+ self.setPage(DB_SELECT_PAGE, self.db_page)
+
+ self.config_page = ConfigFilePage(self.project_name, parent=self)
+ self.setPage(CONFIG_PAGE, self.config_page)
diff --git a/Mergin/images/default/tabler_icons/database-cog.svg b/Mergin/images/default/tabler_icons/database-cog.svg
new file mode 100644
index 00000000..47f2f65f
--- /dev/null
+++ b/Mergin/images/default/tabler_icons/database-cog.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/Mergin/images/white/tabler_icons/database-cog.svg b/Mergin/images/white/tabler_icons/database-cog.svg
new file mode 100644
index 00000000..444ab41a
--- /dev/null
+++ b/Mergin/images/white/tabler_icons/database-cog.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/Mergin/plugin.py b/Mergin/plugin.py
index 676cbd78..f4280d26 100644
--- a/Mergin/plugin.py
+++ b/Mergin/plugin.py
@@ -39,6 +39,7 @@
from .project_settings_widget import MerginProjectConfigFactory
from .projects_manager import MerginProjectsManager
from .sync_dialog import SyncDialog
+from .configure_sync_wizard import DbSyncConfigWizard
from .utils import (
ServerType,
ClientError,
@@ -143,6 +144,13 @@ def initGui(self):
enabled=False,
always_on=False,
)
+ self.action_db_sync_wizard = self.add_action(
+ "database-cog.svg",
+ text="Configure DB sync",
+ callback=self.configure_db_sync,
+ add_to_menu=True,
+ add_to_toolbar=None,
+ )
self.enable_toolbar_actions()
@@ -278,6 +286,28 @@ def configure(self):
self.on_config_changed()
self.show_browser_panel()
+ def configure_db_sync(self):
+ """Open db-sync setup wizard."""
+ project_path = QgsProject.instance().homePath()
+ if not project_path:
+ iface.messageBar().pushMessage("Mergin", "Project is not saved, please save project first", Qgis.Warning)
+ return
+
+ if not check_mergin_subdirs(project_path):
+ iface.messageBar().pushMessage("Mergin", "Current project is not a Mergin project. Please open a Mergin project first.", Qgis.Warning)
+ return
+
+ mp = MerginProject(project_path)
+ try:
+ project_name = mp.metadata["name"]
+ except InvalidProject as e:
+ iface.messageBar().pushMessage("Mergin", "Current project is not a Mergin project. Please open a Mergin project first.", Qgis.Warning)
+ return
+
+ wizard = DbSyncConfigWizard(project_name)
+ if not wizard.exec_():
+ return
+
def show_no_workspaces_dialog(self):
msg = (
"Workspace is a place to store your projects and share them with your colleagues. "
diff --git a/Mergin/ui/ui_config_file_page.ui b/Mergin/ui/ui_config_file_page.ui
new file mode 100644
index 00000000..9a5bd6cf
--- /dev/null
+++ b/Mergin/ui/ui_config_file_page.ui
@@ -0,0 +1,95 @@
+
+
+ WizardPage
+
+
+
+ 0
+ 0
+ 400
+ 269
+
+
+
+ Configuration file
+
+
+ -
+
+
+ Configuration file
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Save
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ See documentation on how to run the sync tool with generated configuration file
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/Mergin/ui/ui_db_selection_page.ui b/Mergin/ui/ui_db_selection_page.ui
new file mode 100644
index 00000000..c25d5386
--- /dev/null
+++ b/Mergin/ui/ui_db_selection_page.ui
@@ -0,0 +1,121 @@
+
+
+ WizardPage
+
+
+
+ 0
+ 0
+ 498
+ 253
+
+
+
+ Select DB connection
+
+
+ -
+
+
+ Pick a PostgreSQL database connection
+
+
+
+ -
+
+
+ -
+
+
+ Existing schema name for sync
+
+
+
+ -
+
+
+ 0
+
+
+
+
+ 0
+
+
+ QLayout::SetMinimumSize
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+
+
+
+
+ 0
+
+
+ QLayout::SetMinimumSize
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Schema name for internal use (will be created)
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 500
+
+
+
+
+
+
+
+
+
diff --git a/Mergin/ui/ui_gpkg_selection_page.ui b/Mergin/ui/ui_gpkg_selection_page.ui
new file mode 100644
index 00000000..0ecd9e4b
--- /dev/null
+++ b/Mergin/ui/ui_gpkg_selection_page.ui
@@ -0,0 +1,45 @@
+
+
+ WizardPage
+
+
+
+ 0
+ 0
+ 400
+ 78
+
+
+
+ Select GeoPackage
+
+
+ -
+
+
+ Pick a GeoPackage file that contains data to be synchronized
+
+
+ true
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ QgsFileWidget
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/Mergin/ui/ui_sync_direction_page.ui b/Mergin/ui/ui_sync_direction_page.ui
new file mode 100644
index 00000000..c1f71df3
--- /dev/null
+++ b/Mergin/ui/ui_sync_direction_page.ui
@@ -0,0 +1,61 @@
+
+
+ WizardPage
+
+
+
+ 0
+ 0
+ 400
+ 174
+
+
+
+ WizardPage
+
+
+ -
+
+
+ <html><head/><body><p>This wizard will create a configuration file for the Mergin Maps database sync tool for the currently opened project.</p><p>Please start by picking how the sync will be initialized</p></body></html>
+
+
+ true
+
+
+
+ -
+
+
+ Initialize from Mergin Maps project
+
+
+
+ -
+
+
+ Initialize from database
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+