Repository navigation
Expand file tree
/
Copy pathproject_repo.py
More file actions
170 lines (135 loc) · 5.64 KB
/
Copy pathproject_repo.py
File metadata and controls
170 lines (135 loc) · 5.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""Project repository: single entry point for load/save/autosave with dirty tracking."""
from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Callable, Optional
from PyQt6.QtCore import QTimer, pyqtSignal, QObject
from model import Project, create_lyrics_block
from storage import detect_storage
_LOGGER = logging.getLogger(__name__)
def _user_data_root() -> Path:
target = Path(__file__).parent / "user_data"
target.mkdir(parents=True, exist_ok=True)
return target
class ProjectRepo(QObject):
"""Manages project persistence, dirty tracking, and autosave."""
projectChanged = pyqtSignal()
modifiedChanged = pyqtSignal(bool)
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self._project: Project = Project.new_default()
self._path: Optional[str] = None
self._modified: bool = False
self._snapshots: list[Project] = []
# Autosave debounce timer
self._autosave_timer = QTimer(self)
self._autosave_timer.setSingleShot(True)
self._autosave_timer.setInterval(500)
self._autosave_timer.timeout.connect(self._flush_autosave)
# ── properties ──────────────────────────────────────────────
@property
def project(self) -> Project:
return self._project
@property
def path(self) -> Optional[str]:
return self._path
@path.setter
def path(self, value: Optional[str]) -> None:
self._path = value
@property
def is_modified(self) -> bool:
return self._modified
# ── load ────────────────────────────────────────────────────
def load(self, path: str) -> None:
storage = detect_storage(path)
self._project = storage.load(path)
self._path = path
self._modified = False
self._snapshots.clear()
self.projectChanged.emit()
def load_autosave(self) -> bool:
autosave_path = _user_data_root() / "autosave.rapnote"
if not autosave_path.exists():
return False
try:
self.load(str(autosave_path))
self._path = None # autosave has no user path
self._modified = False
return True
except Exception as exc:
_LOGGER.warning("Failed to load autosave: %s", exc)
return False
# ── save ────────────────────────────────────────────────────
def save(self, path: Optional[str] = None) -> bool:
target = path or self._path
if not target:
return False
try:
storage = detect_storage(target)
storage.save(self._project, target)
self._path = target
self._modified = False
self.modifiedChanged.emit(False)
return True
except Exception as exc:
_LOGGER.error("Save failed: %s", exc)
return False
def save_autosave(self) -> None:
autosave_path = str(_user_data_root() / "autosave.rapnote")
try:
storage = detect_storage(autosave_path)
storage.save(self._project, autosave_path)
except Exception as exc:
_LOGGER.error("Autosave failed: %s", exc)
def delete_autosave(self) -> None:
autosave_path = _user_data_root() / "autosave.rapnote"
try:
autosave_path.unlink(missing_ok=True)
except Exception:
pass
# ── mutation tracking ───────────────────────────────────────
def mark_modified(self) -> None:
if not self._modified:
self._snapshot()
self._modified = True
self.modifiedChanged.emit(True)
if not self._autosave_timer.isActive():
self._autosave_timer.start()
def set_project(self, project: Project, path: Optional[str] = None) -> None:
self._snapshot()
self._project = project
if path is not None:
self._path = path
self._modified = True
self.modifiedChanged.emit(True)
self.projectChanged.emit()
def new_project(self) -> None:
self._snapshot()
self._project = Project.new_default()
self._path = None
self._modified = False
self.modifiedChanged.emit(False)
self.projectChanged.emit()
# ── undo (snapshot stack) ───────────────────────────────────
def _snapshot(self) -> None:
self._snapshots.append(self._clone_project())
if len(self._snapshots) > 20:
self._snapshots.pop(0)
def can_undo(self) -> bool:
return len(self._snapshots) > 0
def undo(self) -> bool:
if not self._snapshots:
return False
self._project = self._snapshots.pop()
self._modified = bool(self._snapshots)
self.modifiedChanged.emit(self._modified)
self.projectChanged.emit()
return True
def _clone_project(self) -> Project:
import copy
return copy.deepcopy(self._project)
# ── autosave ────────────────────────────────────────────────
def _flush_autosave(self) -> None:
if self._modified:
self.save_autosave()