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