All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
"""
|
|
Merge operations mixin for pyPhotoAlbum
|
|
"""
|
|
|
|
from PyQt6.QtWidgets import QFileDialog, QMessageBox
|
|
from pyPhotoAlbum.decorators import ribbon_action
|
|
from pyPhotoAlbum.merge_manager import MergeManager, concatenate_projects
|
|
from pyPhotoAlbum.merge_dialog import MergeDialog
|
|
from pyPhotoAlbum.project_serializer import load_from_zip, save_to_zip
|
|
from pyPhotoAlbum.models import set_asset_resolution_context
|
|
from pyPhotoAlbum.project import Project
|
|
import tempfile
|
|
import os
|
|
|
|
|
|
class MergeOperationsMixin:
|
|
"""Mixin providing project merge operations"""
|
|
|
|
@ribbon_action(
|
|
label="Merge Projects",
|
|
tooltip="Merge another project file with the current project",
|
|
tab="File",
|
|
group="Import/Export",
|
|
)
|
|
def merge_projects(self):
|
|
"""
|
|
Merge another project with the current project.
|
|
|
|
If the projects have the same project_id, conflicts will be resolved.
|
|
If they have different project_ids, they will be concatenated.
|
|
"""
|
|
# Check if current project has changes
|
|
if self.project.is_dirty():
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Unsaved Changes",
|
|
"You have unsaved changes in the current project. Save before merging?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Cancel:
|
|
return
|
|
elif reply == QMessageBox.StandardButton.Yes:
|
|
# Save current project first
|
|
if hasattr(self, "save_project"):
|
|
self.save_project()
|
|
|
|
# Select file to merge
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self, "Select Project to Merge", "", "Photo Album Projects (*.ppz);;All Files (*)"
|
|
)
|
|
|
|
if not file_path:
|
|
return
|
|
|
|
try:
|
|
# Disable autosave during merge
|
|
if hasattr(self, "_autosave_timer"):
|
|
self._autosave_timer.stop()
|
|
|
|
# Load the other project
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Load project data
|
|
other_project = load_from_zip(file_path, temp_dir)
|
|
|
|
# Serialize both projects for comparison
|
|
our_data = self.project.serialize()
|
|
their_data = other_project.serialize()
|
|
|
|
# Check if projects should be merged or concatenated
|
|
merge_manager = MergeManager()
|
|
should_merge = merge_manager.should_merge_projects(our_data, their_data)
|
|
|
|
if should_merge:
|
|
# Same project - merge with conflict resolution
|
|
self._perform_merge_with_conflicts(our_data, their_data)
|
|
else:
|
|
# Different projects - concatenate
|
|
self._perform_concatenation(our_data, their_data)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Merge Error", f"Failed to merge projects:\n{str(e)}")
|
|
finally:
|
|
# Re-enable autosave
|
|
if hasattr(self, "_autosave_timer"):
|
|
self._autosave_timer.start()
|
|
|
|
def _perform_merge_with_conflicts(self, our_data, their_data):
|
|
"""Perform merge with conflict resolution UI"""
|
|
# Detect conflicts
|
|
merge_manager = MergeManager()
|
|
conflicts = merge_manager.detect_conflicts(our_data, their_data)
|
|
|
|
if not conflicts:
|
|
# No conflicts - auto-merge
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"No Conflicts",
|
|
"No conflicts detected. Merge projects automatically?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
|
|
# Auto-merge non-conflicting changes
|
|
merged_data = merge_manager.apply_resolutions(our_data, their_data, {})
|
|
else:
|
|
# Show merge dialog for conflict resolution
|
|
dialog = MergeDialog(our_data, their_data, self)
|
|
|
|
if dialog.exec() != QMessageBox.DialogCode.Accepted:
|
|
QMessageBox.information(self, "Merge Cancelled", "Merge operation cancelled.")
|
|
return
|
|
|
|
# Get merged data from dialog
|
|
merged_data = dialog.get_merged_project_data()
|
|
|
|
# Apply merged data to current project
|
|
self._apply_merged_data(merged_data)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Merge Complete",
|
|
f"Projects merged successfully.\n"
|
|
f"Total pages: {len(merged_data.get('pages', []))}\n"
|
|
f"Resolved conflicts: {len(conflicts)}",
|
|
)
|
|
|
|
def _perform_concatenation(self, our_data, their_data):
|
|
"""Concatenate two different projects"""
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Different Projects",
|
|
f"These are different projects:\n"
|
|
f" • {our_data.get('name', 'Untitled')}\n"
|
|
f" • {their_data.get('name', 'Untitled')}\n\n"
|
|
f"Concatenate them (combine all pages)?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
|
|
# Concatenate projects
|
|
merged_data = concatenate_projects(our_data, their_data)
|
|
|
|
# Apply merged data
|
|
self._apply_merged_data(merged_data)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Concatenation Complete",
|
|
f"Projects concatenated successfully.\n" f"Total pages: {len(merged_data.get('pages', []))}",
|
|
)
|
|
|
|
def _apply_merged_data(self, merged_data):
|
|
"""Apply merged project data to current project"""
|
|
# Create new project from merged data
|
|
new_project = Project()
|
|
new_project.deserialize(merged_data)
|
|
|
|
# Replace current project
|
|
self._project = new_project
|
|
|
|
# Update asset resolution context
|
|
set_asset_resolution_context(new_project.folder_path)
|
|
|
|
# Mark as dirty (has unsaved changes from merge)
|
|
new_project.mark_dirty()
|
|
|
|
# Update UI
|
|
if hasattr(self, "gl_widget"):
|
|
self.gl_widget.set_project(new_project)
|
|
self.gl_widget.update()
|
|
|
|
if hasattr(self, "status_bar"):
|
|
self.status_bar.showMessage("Merge completed successfully", 3000)
|