yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
recent_projects_model.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_SHELL_COORDINATOR_RECENT_PROJECTS_MODEL_H_
2#define YAZE_APP_EDITOR_SHELL_COORDINATOR_RECENT_PROJECTS_MODEL_H_
3
4#include <atomic>
5#include <chrono>
6#include <cstdint>
7#include <deque>
8#include <filesystem>
9#include <memory>
10#include <mutex>
11#include <string>
12#include <unordered_map>
13#include <vector>
14
15namespace yaze {
16namespace editor {
17
18// Display-ready record of a single recent project or ROM.
19//
20// The first block of fields is what the welcome screen needs to render a card.
21// The second block is carried forward for features landing in subsequent
22// commits — relink (is_missing), pin/rename/notes (display_name_override,
23// pinned, notes), and the metadata+CRC cache (size_bytes, mtime_epoch_ns,
24// crc32, snes_region, snes_map_mode). Defaults keep today's behavior.
26 // Display fields consumed by DrawProjectPanel.
27 std::string name;
28 std::string filepath;
29 std::string rom_title;
30 std::string metadata_summary;
31 std::string last_modified;
32 std::string item_type; // "ROM" / "Project" / "File" / "Unavailable"
33 std::string item_icon;
34 std::string thumbnail_path; // Optional screenshot
35 bool unavailable = false; // platform permission gate (iOS)
36 int days_ago = 0;
37
38 // --- Forward-looking (populated in later tasks) ---
39 bool is_missing = false;
40 bool pinned = false;
42 std::string notes;
43 std::uint64_t size_bytes = 0;
44 std::int64_t mtime_epoch_ns = 0;
45 std::string crc32;
46 std::string snes_region;
47 std::string snes_map_mode;
48};
49
50// Observable, mutable list of recent projects.
51//
52// Owns no global state of its own; under the hood it reads and writes
53// project::RecentFilesManager::GetInstance() so existing persistence keeps
54// working. This seam lets the welcome screen, command palette, and tests
55// consume the same list without each of them touching the singleton.
56//
57// Refresh() is gated by the manager's generation counter — cheap no-op when
58// nothing changed, which is the common per-frame case.
60 public:
63
64 // Rebuild the entry vector from RecentFilesManager, resolving file
65 // metadata. Fast path: returns early if the manager's generation counter
66 // has not advanced since the last Refresh. Pass force=true to override.
67 //
68 // Expensive ROM metadata (SNES header parse + full-ROM CRC32) runs on a
69 // detached worker for cache-miss entries; the display-ready entry carries
70 // a "Scanning…" placeholder until the worker's result is drained on a
71 // later Refresh. The UI doesn't need to do anything special — the welcome
72 // screen already calls Refresh every frame.
73 void Refresh(bool force = false);
74
75 const std::vector<RecentProject>& entries() const { return entries_; }
76 std::uint64_t generation() const { return cached_generation_; }
77
78 // Mutations pass through to RecentFilesManager + Save(). They bump the
79 // manager's generation counter so the next Refresh() picks up the change
80 // automatically on a subsequent frame.
81 void AddRecent(const std::string& path);
82 void RemoveRecent(const std::string& path);
83 void ClearAll();
84
85 // Replace old_path with new_path in the recents list, preserving any
86 // cached extras (pin/rename/notes carry over). Use when a recent file has
87 // been moved on disk — the user points at its new location via a file
88 // dialog, we swap the entry without losing its annotations.
89 void RelinkRecent(const std::string& old_path, const std::string& new_path);
90
91 // Per-entry annotations. All three persist across restarts via the
92 // sidecar cache. Pinned entries sort first. Passing an empty name or notes
93 // string clears the override.
94 void SetPinned(const std::string& path, bool pinned);
95 void SetDisplayName(const std::string& path, std::string display_name);
96 void SetNotes(const std::string& path, std::string notes);
97
98 // Undo support for RemoveRecent. When a user removes a recent entry we
99 // stash its path + cached extras for a short window so the welcome screen
100 // can surface an "Undo" affordance. Restoring re-adds the path to
101 // RecentFilesManager (which puts it at the front — we don't try to recover
102 // its original position) and re-attaches pin/rename/notes/crc so no user
103 // annotations are lost. The window is `kUndoWindowSeconds`; after that the
104 // removal is permanent and HasUndoableRemoval() returns false.
105 static constexpr float kUndoWindowSeconds = 8.0f;
106 struct PendingUndo {
107 std::string path;
108 std::string display_name; // For the toast text.
109 };
110 bool HasUndoableRemoval() const;
112 bool UndoLastRemoval();
113 void DismissLastRemoval(); // Explicitly drop the pending undo.
114
115 private:
116 // Persisted extras per path. Stored in a sidecar JSON alongside the
117 // recent-files list. The (size_bytes, mtime_epoch_ns) pair acts as a cache
118 // key for the expensive reads (SNES header parse + full-ROM CRC32); when
119 // neither has changed we skip the I/O entirely.
120 //
121 // The pinned / display_name_override / notes fields are wired up by later
122 // commits but live here so the sidecar format is stable across the feature
123 // set rolling out in this initiative.
125 std::uint64_t size_bytes = 0;
126 std::int64_t mtime_epoch_ns = 0;
127 std::string crc32;
128 std::string snes_title;
129 std::string snes_region;
130 std::string snes_map_mode;
131 bool pinned = false;
133 std::string notes;
134 };
135
136 RecentProject BuildEntry(const std::string& filepath);
137 void DispatchBackgroundRomScan(const std::string& filepath,
138 std::uint64_t size_bytes,
139 std::int64_t mtime_epoch_ns);
140 bool DrainAsyncResults(); // true iff state changed; bumps generation.
141
142 void LoadCache();
143 void SaveCache();
144 std::filesystem::path CachePath() const;
145
146 std::vector<RecentProject> entries_;
147 std::uint64_t cached_generation_ = 0;
148 bool loaded_once_ = false;
149
150 std::unordered_map<std::string, CachedExtras> cache_;
151 bool cache_dirty_ = false;
152 bool cache_loaded_ = false;
153
154 // Bumped by annotation mutations that RecentFilesManager doesn't see
155 // (Pin/Rename/Notes). Combined with the manager's generation so the
156 // Refresh fast-path still short-circuits correctly.
157 std::uint64_t annotation_generation_ = 0;
158
159 // Single-slot undo buffer for RemoveRecent. Keeps the path + cached extras
160 // so a restore re-attaches pin/rename/notes. The expiry is a steady_clock
161 // deadline; HasUndoableRemoval() returns false past it.
163 std::string path;
164 std::string display_name;
166 std::chrono::steady_clock::time_point expires_at;
167 };
168 std::deque<RemovedRecent> undo_buffer_;
169
170 // Shared async-scan state. Workers hold their own shared_ptr copy, so
171 // destroying the model mid-scan is safe: we flip `cancelled` and release
172 // our reference; workers check `cancelled` before posting their result
173 // and then drop the state when their ref dies. The mutex guards the
174 // `ready` queue and the `in_flight` set.
176 std::string path;
177 std::uint64_t size_bytes = 0;
178 std::int64_t mtime_epoch_ns = 0;
179 std::string crc32;
180 std::string snes_title;
181 std::string snes_region;
182 std::string snes_map_mode;
183 };
185 std::mutex mu;
186 std::vector<AsyncScanResult> ready;
187 std::unordered_map<std::string, bool> in_flight; // path -> true
188 std::atomic<bool> cancelled{false};
189 };
190 std::shared_ptr<AsyncScanState> scan_state_;
191};
192
193} // namespace editor
194} // namespace yaze
195
196#endif // YAZE_APP_EDITOR_SHELL_COORDINATOR_RECENT_PROJECTS_MODEL_H_
std::unordered_map< std::string, CachedExtras > cache_
void SetPinned(const std::string &path, bool pinned)
std::filesystem::path CachePath() const
std::vector< RecentProject > entries_
void SetDisplayName(const std::string &path, std::string display_name)
void DispatchBackgroundRomScan(const std::string &filepath, std::uint64_t size_bytes, std::int64_t mtime_epoch_ns)
void SetNotes(const std::string &path, std::string notes)
void RelinkRecent(const std::string &old_path, const std::string &new_path)
void RemoveRecent(const std::string &path)
std::deque< RemovedRecent > undo_buffer_
const std::vector< RecentProject > & entries() const
std::shared_ptr< AsyncScanState > scan_state_
RecentProject BuildEntry(const std::string &filepath)
void AddRecent(const std::string &path)
std::unordered_map< std::string, bool > in_flight
std::chrono::steady_clock::time_point expires_at