yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
collaboration_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <ctime>
5#include <iomanip>
6#include <sstream>
7
8#include "absl/strings/str_format.h"
9#include "imgui/imgui.h"
10
11namespace yaze {
12
13namespace gui {
14
16 : rom_(nullptr),
17 version_mgr_(nullptr),
18 approval_mgr_(nullptr),
19 selected_tab_(0),
20 selected_rom_sync_(-1),
21 selected_snapshot_(-1),
22 selected_proposal_(-1),
23 show_sync_details_(false),
24 show_snapshot_preview_(true),
25 auto_scroll_(true),
26 filter_pending_only_(false) {
27 // Initialize search filter
28 search_filter_[0] = '\0';
29
30 // Initialize colors
31 colors_.sync_applied = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
32 colors_.sync_pending = ImVec4(0.8f, 0.8f, 0.2f, 1.0f);
33 colors_.sync_error = ImVec4(0.8f, 0.2f, 0.2f, 1.0f);
34 colors_.proposal_pending = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
35 colors_.proposal_approved = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
36 colors_.proposal_rejected = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
37 colors_.proposal_applied = ImVec4(0.2f, 0.6f, 0.8f, 1.0f);
38}
39
41 // Cleanup any OpenGL textures
42 for (auto& snapshot : snapshots_) {
43 if (snapshot.texture_id) {
44 // Note: Actual texture cleanup would depend on your renderer
45 // This is a placeholder
46 snapshot.texture_id = nullptr;
47 }
48 }
49}
50
52 Rom* rom, net::RomVersionManager* version_mgr,
53 net::ProposalApprovalManager* approval_mgr) {
54 rom_ = rom;
55 version_mgr_ = version_mgr;
56 approval_mgr_ = approval_mgr;
57}
58
59void CollaborationPanel::Render(bool* p_open) {
60 if (!ImGui::Begin("Collaboration", p_open, ImGuiWindowFlags_None)) {
61 ImGui::End();
62 return;
63 }
64
65 // Tabs for different collaboration features
66 if (ImGui::BeginTabBar("CollaborationTabs")) {
67 if (ImGui::BeginTabItem("ROM Sync")) {
68 selected_tab_ = 0;
70 ImGui::EndTabItem();
71 }
72
73 if (ImGui::BeginTabItem("Version History")) {
74 selected_tab_ = 1;
76 ImGui::EndTabItem();
77 }
78
79 if (ImGui::BeginTabItem("Snapshots")) {
80 selected_tab_ = 2;
82 ImGui::EndTabItem();
83 }
84
85 if (ImGui::BeginTabItem("Proposals")) {
86 selected_tab_ = 3;
88 ImGui::EndTabItem();
89 }
90
91 if (ImGui::BeginTabItem("🔒 Approvals")) {
92 selected_tab_ = 4;
94 ImGui::EndTabItem();
95 }
96
97 ImGui::EndTabBar();
98 }
99
100 ImGui::End();
101}
102
104 ImGui::TextWrapped("ROM Synchronization History");
105 ImGui::Separator();
106
107 // Toolbar
108 if (ImGui::Button("Clear History")) {
109 rom_syncs_.clear();
110 }
111 ImGui::SameLine();
112 ImGui::Checkbox("Auto-scroll", &auto_scroll_);
113 ImGui::SameLine();
114 ImGui::Checkbox("Show Details", &show_sync_details_);
115
116 ImGui::Separator();
117
118 // Stats
119 int applied_count = 0;
120 int pending_count = 0;
121 int error_count = 0;
122
123 for (const auto& sync : rom_syncs_) {
124 if (sync.applied)
125 applied_count++;
126 else if (!sync.error_message.empty())
127 error_count++;
128 else
129 pending_count++;
130 }
131
132 ImGui::Text("Total: %zu | ", rom_syncs_.size());
133 ImGui::SameLine();
134 ImGui::TextColored(colors_.sync_applied, "Applied: %d", applied_count);
135 ImGui::SameLine();
136 ImGui::TextColored(colors_.sync_pending, "Pending: %d", pending_count);
137 ImGui::SameLine();
138 ImGui::TextColored(colors_.sync_error, "Errors: %d", error_count);
139
140 ImGui::Separator();
141
142 // Sync list
143 if (ImGui::BeginChild("SyncList", ImVec2(0, 0), true)) {
144 for (size_t i = 0; i < rom_syncs_.size(); ++i) {
146 }
147
148 if (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
149 ImGui::SetScrollHereY(1.0f);
150 }
151 }
152 ImGui::EndChild();
153}
154
156 ImGui::TextWrapped("Shared Snapshots Gallery");
157 ImGui::Separator();
158
159 // Toolbar
160 if (ImGui::Button("Clear Gallery")) {
161 snapshots_.clear();
162 }
163 ImGui::SameLine();
164 ImGui::Checkbox("Show Preview", &show_snapshot_preview_);
165 ImGui::SameLine();
166 ImGui::InputText("Search", search_filter_, sizeof(search_filter_));
167
168 ImGui::Separator();
169
170 // Snapshot grid
171 if (ImGui::BeginChild("SnapshotGrid", ImVec2(0, 0), true)) {
172 float thumbnail_size = 150.0f;
173 float padding = 10.0f;
174 float cell_size = thumbnail_size + padding;
175
176 int columns =
177 std::max(1, (int)((ImGui::GetContentRegionAvail().x) / cell_size));
178
179 for (size_t i = 0; i < snapshots_.size(); ++i) {
180 // Filter by search
181 if (search_filter_[0] != '\0') {
182 std::string search_lower = search_filter_;
183 std::string sender_lower = snapshots_[i].sender;
184 std::transform(search_lower.begin(), search_lower.end(),
185 search_lower.begin(), ::tolower);
186 std::transform(sender_lower.begin(), sender_lower.end(),
187 sender_lower.begin(), ::tolower);
188
189 if (sender_lower.find(search_lower) == std::string::npos &&
190 snapshots_[i].snapshot_type.find(search_lower) ==
191 std::string::npos) {
192 continue;
193 }
194 }
195
197
198 // Grid layout
199 if ((i + 1) % columns != 0 && i < snapshots_.size() - 1) {
200 ImGui::SameLine();
201 }
202 }
203 }
204 ImGui::EndChild();
205}
206
208 ImGui::TextWrapped("AI Proposals & Suggestions");
209 ImGui::Separator();
210
211 // Toolbar
212 if (ImGui::Button("Clear All")) {
213 proposals_.clear();
214 }
215 ImGui::SameLine();
216 ImGui::Checkbox("Pending Only", &filter_pending_only_);
217 ImGui::SameLine();
218 ImGui::InputText("Search", search_filter_, sizeof(search_filter_));
219
220 ImGui::Separator();
221
222 // Stats
223 int pending = 0, approved = 0, rejected = 0, applied = 0;
224 for (const auto& proposal : proposals_) {
225 if (proposal.status == "pending")
226 pending++;
227 else if (proposal.status == "approved")
228 approved++;
229 else if (proposal.status == "rejected")
230 rejected++;
231 else if (proposal.status == "applied")
232 applied++;
233 }
234
235 ImGui::Text("Total: %zu", proposals_.size());
236 ImGui::SameLine();
237 ImGui::TextColored(colors_.proposal_pending, " | Pending: %d", pending);
238 ImGui::SameLine();
239 ImGui::TextColored(colors_.proposal_approved, " | Approved: %d", approved);
240 ImGui::SameLine();
241 ImGui::TextColored(colors_.proposal_rejected, " | Rejected: %d", rejected);
242 ImGui::SameLine();
243 ImGui::TextColored(colors_.proposal_applied, " | Applied: %d", applied);
244
245 ImGui::Separator();
246
247 // Proposal list
248 if (ImGui::BeginChild("ProposalList", ImVec2(0, 0), true)) {
249 for (size_t i = 0; i < proposals_.size(); ++i) {
250 // Filter
251 if (filter_pending_only_ && proposals_[i].status != "pending") {
252 continue;
253 }
254
255 if (search_filter_[0] != '\0') {
256 std::string search_lower = search_filter_;
257 std::string sender_lower = proposals_[i].sender;
258 std::string desc_lower = proposals_[i].description;
259 std::transform(search_lower.begin(), search_lower.end(),
260 search_lower.begin(), ::tolower);
261 std::transform(sender_lower.begin(), sender_lower.end(),
262 sender_lower.begin(), ::tolower);
263 std::transform(desc_lower.begin(), desc_lower.end(), desc_lower.begin(),
264 ::tolower);
265
266 if (sender_lower.find(search_lower) == std::string::npos &&
267 desc_lower.find(search_lower) == std::string::npos) {
268 continue;
269 }
270 }
271
273 }
274 }
275 ImGui::EndChild();
276}
277
279 int index) {
280 ImGui::PushID(index);
281
282 // Status indicator
283 ImVec4 status_color;
284 const char* status_icon;
285
286 if (entry.applied) {
287 status_color = colors_.sync_applied;
288 status_icon = "[✓]";
289 } else if (!entry.error_message.empty()) {
290 status_color = colors_.sync_error;
291 status_icon = "[✗]";
292 } else {
293 status_color = colors_.sync_pending;
294 status_icon = "[◷]";
295 }
296
297 ImGui::TextColored(status_color, "%s", status_icon);
298 ImGui::SameLine();
299
300 // Entry info
301 ImGui::Text("%s - %s (%s)", FormatTimestamp(entry.timestamp).c_str(),
302 entry.sender.c_str(), FormatFileSize(entry.diff_size).c_str());
303
304 // Details on hover or if enabled
305 if (show_sync_details_ || ImGui::IsItemHovered()) {
306 ImGui::Indent();
307 ImGui::TextWrapped("ROM Hash: %s", entry.rom_hash.substr(0, 16).c_str());
308 if (!entry.error_message.empty()) {
309 ImGui::TextColored(colors_.sync_error, "Error: %s",
310 entry.error_message.c_str());
311 }
312 ImGui::Unindent();
313 }
314
315 ImGui::Separator();
316 ImGui::PopID();
317}
318
320 int index) {
321 ImGui::PushID(index);
322
323 ImGui::BeginGroup();
324
325 // Thumbnail placeholder or actual image
326 if (show_snapshot_preview_ && entry.is_image && entry.texture_id) {
327 ImGui::Image(entry.texture_id, ImVec2(150, 150));
328 } else {
329 // Placeholder
330 ImGui::BeginChild("SnapshotPlaceholder", ImVec2(150, 150), true);
331 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 50);
332 ImGui::TextWrapped("%s", entry.snapshot_type.c_str());
333 ImGui::EndChild();
334 }
335
336 // Info
337 ImGui::TextWrapped("%s", entry.sender.c_str());
338 ImGui::Text("%s", FormatTimestamp(entry.timestamp).c_str());
339 ImGui::Text("%s", FormatFileSize(entry.data_size).c_str());
340
341 // Actions
342 if (ImGui::SmallButton("View")) {
343 selected_snapshot_ = index;
344 // TODO: Open snapshot viewer
345 }
346 ImGui::SameLine();
347 if (ImGui::SmallButton("Export")) {
348 // TODO: Export snapshot to file
349 }
350
351 ImGui::EndGroup();
352
353 ImGui::PopID();
354}
355
357 int index) {
358 ImGui::PushID(index);
359
360 // Status icon and color
361 const char* icon = GetProposalStatusIcon(entry.status);
362 ImVec4 color = GetProposalStatusColor(entry.status);
363
364 ImGui::TextColored(color, "%s", icon);
365 ImGui::SameLine();
366
367 // Collapsible header
368 bool is_open = ImGui::TreeNode(entry.description.c_str());
369
370 if (is_open) {
371 ImGui::Indent();
372
373 ImGui::Text("From: %s", entry.sender.c_str());
374 ImGui::Text("Time: %s", FormatTimestamp(entry.timestamp).c_str());
375 ImGui::Text("Status: %s", entry.status.c_str());
376
377 ImGui::Separator();
378
379 // Proposal data
380 ImGui::TextWrapped("%s", entry.proposal_data.c_str());
381
382 // Actions for pending proposals
383 if (entry.status == "pending") {
384 ImGui::Separator();
385 if (ImGui::Button("✓ Approve")) {
386 // TODO: Send approval to server
387 }
388 ImGui::SameLine();
389 if (ImGui::Button("✗ Reject")) {
390 // TODO: Send rejection to server
391 }
392 ImGui::SameLine();
393 if (ImGui::Button("â–ļ Apply Now")) {
394 // TODO: Execute proposal
395 }
396 }
397
398 ImGui::Unindent();
399 ImGui::TreePop();
400 }
401
402 ImGui::Separator();
403 ImGui::PopID();
404}
405
407 rom_syncs_.push_back(entry);
408}
409
411 snapshots_.push_back(entry);
412}
413
415 proposals_.push_back(entry);
416}
417
418void CollaborationPanel::UpdateProposalStatus(const std::string& proposal_id,
419 const std::string& status) {
420 for (auto& proposal : proposals_) {
421 if (proposal.proposal_id == proposal_id) {
422 proposal.status = status;
423 break;
424 }
425 }
426}
427
429 rom_syncs_.clear();
430 snapshots_.clear();
431 proposals_.clear();
432}
433
434ProposalEntry* CollaborationPanel::GetProposal(const std::string& proposal_id) {
435 for (auto& proposal : proposals_) {
436 if (proposal.proposal_id == proposal_id) {
437 return &proposal;
438 }
439 }
440 return nullptr;
441}
442
443std::string CollaborationPanel::FormatTimestamp(int64_t timestamp) {
444 std::time_t time = timestamp / 1000; // Convert ms to seconds
445 std::tm* tm = std::localtime(&time);
446
447 char buffer[32];
448 std::strftime(buffer, sizeof(buffer), "%H:%M:%S", tm);
449 return std::string(buffer);
450}
451
452std::string CollaborationPanel::FormatFileSize(size_t bytes) {
453 const char* units[] = {"B", "KB", "MB", "GB"};
454 int unit_index = 0;
455 double size = static_cast<double>(bytes);
456
457 while (size >= 1024.0 && unit_index < 3) {
458 size /= 1024.0;
459 unit_index++;
460 }
461
462 char buffer[32];
463 snprintf(buffer, sizeof(buffer), "%.1f %s", size, units[unit_index]);
464 return std::string(buffer);
465}
466
468 const std::string& status) {
469 if (status == "pending")
470 return "[◷]";
471 if (status == "approved")
472 return "[✓]";
473 if (status == "rejected")
474 return "[✗]";
475 if (status == "applied")
476 return "[âœĻ]";
477 return "[?]";
478}
479
480ImVec4 CollaborationPanel::GetProposalStatusColor(const std::string& status) {
481 if (status == "pending")
482 return colors_.proposal_pending;
483 if (status == "approved")
484 return colors_.proposal_approved;
485 if (status == "rejected")
486 return colors_.proposal_rejected;
487 if (status == "applied")
488 return colors_.proposal_applied;
489 return ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
490}
491
493 if (!version_mgr_) {
494 ImGui::TextWrapped("Version management not initialized");
495 return;
496 }
497
498 ImGui::TextWrapped("ROM Version History & Protection");
499 ImGui::Separator();
500
501 // Stats
502 auto stats = version_mgr_->GetStats();
503 ImGui::Text("Total Snapshots: %zu", stats.total_snapshots);
504 ImGui::SameLine();
505 ImGui::TextColored(colors_.sync_applied, "Safe Points: %zu",
506 stats.safe_points);
507 ImGui::SameLine();
508 ImGui::TextColored(colors_.sync_pending, "Auto-Backups: %zu",
509 stats.auto_backups);
510
511 ImGui::Text("Storage Used: %s",
512 FormatFileSize(stats.total_storage_bytes).c_str());
513
514 ImGui::Separator();
515
516 // Toolbar
517 if (ImGui::Button("💾 Create Checkpoint")) {
518 auto result =
519 version_mgr_->CreateSnapshot("Manual checkpoint", "user", true);
520 // TODO: Show result in UI
521 }
522 ImGui::SameLine();
523 if (ImGui::Button("đŸ›Ąī¸ Mark Current as Safe Point")) {
524 std::string current_hash = version_mgr_->GetCurrentHash();
525 // TODO: Find snapshot with this hash and mark as safe
526 }
527 ImGui::SameLine();
528 if (ImGui::Button("🔍 Check for Corruption")) {
529 auto result = version_mgr_->DetectCorruption();
530 // TODO: Show result
531 }
532
533 ImGui::Separator();
534
535 // Version list
536 if (ImGui::BeginChild("VersionList", ImVec2(0, 0), true)) {
537 auto snapshots = version_mgr_->GetSnapshots();
538
539 for (size_t i = 0; i < snapshots.size(); ++i) {
540 RenderVersionSnapshot(snapshots[i], i);
541 }
542 }
543 ImGui::EndChild();
544}
545
547 if (!approval_mgr_) {
548 ImGui::TextWrapped("Approval management not initialized");
549 return;
550 }
551
552 ImGui::TextWrapped("Proposal Approval System");
553 ImGui::Separator();
554
555 // Pending proposals that need votes
556 auto pending = approval_mgr_->GetPendingProposals();
557
558 if (pending.empty()) {
559 ImGui::TextWrapped("No proposals pending approval.");
560 return;
561 }
562
563 ImGui::Text("Pending Proposals: %zu", pending.size());
564 ImGui::Separator();
565
566 if (ImGui::BeginChild("ApprovalList", ImVec2(0, 0), true)) {
567 for (size_t i = 0; i < pending.size(); ++i) {
568 RenderApprovalProposal(pending[i], i);
569 }
570 }
571 ImGui::EndChild();
572}
573
575 int index) {
576 ImGui::PushID(index);
577
578 // Icon based on type
579 const char* icon;
580 ImVec4 color;
581
582 if (snapshot.is_safe_point) {
583 icon = "đŸ›Ąī¸";
584 color = colors_.sync_applied;
585 } else if (snapshot.is_checkpoint) {
586 icon = "💾";
587 color = colors_.proposal_approved;
588 } else {
589 icon = "📝";
590 color = colors_.sync_pending;
591 }
592
593 ImGui::TextColored(color, "%s", icon);
594 ImGui::SameLine();
595
596 // Collapsible header
597 bool is_open = ImGui::TreeNode(snapshot.description.c_str());
598
599 if (is_open) {
600 ImGui::Indent();
601
602 ImGui::Text("Creator: %s", snapshot.creator.c_str());
603 ImGui::Text("Time: %s", FormatTimestamp(snapshot.timestamp).c_str());
604 ImGui::Text("Hash: %s", snapshot.rom_hash.substr(0, 16).c_str());
605 ImGui::Text("Size: %s", FormatFileSize(snapshot.compressed_size).c_str());
606
607 if (snapshot.is_safe_point) {
608 ImGui::TextColored(colors_.sync_applied, "✓ Safe Point (Host Verified)");
609 }
610
611 ImGui::Separator();
612
613 // Actions
614 if (ImGui::Button("â†Šī¸ Restore This Version")) {
615 auto result = version_mgr_->RestoreSnapshot(snapshot.snapshot_id);
616 // TODO: Show result
617 }
618 ImGui::SameLine();
619 if (!snapshot.is_safe_point && ImGui::Button("đŸ›Ąī¸ Mark as Safe")) {
621 }
622 ImGui::SameLine();
623 if (!snapshot.is_safe_point && ImGui::Button("đŸ—‘ī¸ Delete")) {
625 }
626
627 ImGui::Unindent();
628 ImGui::TreePop();
629 }
630
631 ImGui::Separator();
632 ImGui::PopID();
633}
634
636 const net::ProposalApprovalManager::ApprovalStatus& status, int index) {
637 ImGui::PushID(index);
638
639 // Status indicator
640 ImGui::TextColored(colors_.proposal_pending, "[âŗ]");
641 ImGui::SameLine();
642
643 // Proposal ID (shortened)
644 std::string short_id = status.proposal_id.substr(0, 8);
645 bool is_open =
646 ImGui::TreeNode(absl::StrFormat("Proposal %s", short_id.c_str()).c_str());
647
648 if (is_open) {
649 ImGui::Indent();
650
651 ImGui::Text("Created: %s", FormatTimestamp(status.created_at).c_str());
652 ImGui::Text("Snapshot Before: %s",
653 status.snapshot_before.substr(0, 8).c_str());
654
655 ImGui::Separator();
656 ImGui::TextWrapped("Votes:");
657
658 for (const auto& [username, approved] : status.votes) {
659 ImVec4 vote_color =
660 approved ? colors_.proposal_approved : colors_.proposal_rejected;
661 const char* vote_icon = approved ? "✓" : "✗";
662 ImGui::TextColored(vote_color, " %s %s", vote_icon, username.c_str());
663 }
664
665 ImGui::Separator();
666
667 // Voting actions
668 if (ImGui::Button("✓ Approve")) {
669 // TODO: Send approval vote
670 // approval_mgr_->VoteOnProposal(status.proposal_id, "current_user",
671 // true);
672 }
673 ImGui::SameLine();
674 if (ImGui::Button("✗ Reject")) {
675 // TODO: Send rejection vote
676 // approval_mgr_->VoteOnProposal(status.proposal_id, "current_user",
677 // false);
678 }
679 ImGui::SameLine();
680 if (ImGui::Button("â†Šī¸ Rollback")) {
681 // Restore snapshot from before this proposal
683 }
684
685 ImGui::Unindent();
686 ImGui::TreePop();
687 }
688
689 ImGui::Separator();
690 ImGui::PopID();
691}
692
693} // namespace gui
694
695} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
std::string FormatFileSize(size_t bytes)
ProposalEntry * GetProposal(const std::string &proposal_id)
net::RomVersionManager * version_mgr_
void RenderVersionSnapshot(const net::RomSnapshot &snapshot, int index)
void RenderProposalEntry(const ProposalEntry &entry, int index)
std::string FormatTimestamp(int64_t timestamp)
void AddSnapshot(const SnapshotEntry &entry)
void AddProposal(const ProposalEntry &entry)
void UpdateProposalStatus(const std::string &proposal_id, const std::string &status)
const char * GetProposalStatusIcon(const std::string &status)
std::vector< ProposalEntry > proposals_
void RenderSnapshotEntry(const SnapshotEntry &entry, int index)
void AddRomSync(const RomSyncEntry &entry)
void RenderApprovalProposal(const net::ProposalApprovalManager::ApprovalStatus &status, int index)
struct yaze::gui::CollaborationPanel::@0 colors_
ImVec4 GetProposalStatusColor(const std::string &status)
void Render(bool *p_open=nullptr)
std::vector< RomSyncEntry > rom_syncs_
net::ProposalApprovalManager * approval_mgr_
void RenderRomSyncEntry(const RomSyncEntry &entry, int index)
std::vector< SnapshotEntry > snapshots_
void Initialize(Rom *rom, net::RomVersionManager *version_mgr, net::ProposalApprovalManager *approval_mgr)
Manages proposal approval workflow for collaborative sessions.
std::vector< ApprovalStatus > GetPendingProposals() const
Manages ROM versioning, snapshots, and rollback capabilities.
absl::StatusOr< std::string > CreateSnapshot(const std::string &description, const std::string &creator, bool is_checkpoint=false)
absl::StatusOr< bool > DetectCorruption()
absl::Status RestoreSnapshot(const std::string &snapshot_id)
absl::Status MarkAsSafePoint(const std::string &snapshot_id)
absl::Status DeleteSnapshot(const std::string &snapshot_id)
std::vector< RomSnapshot > GetSnapshots(bool safe_points_only=false) const
Represents an AI-generated proposal.
Represents a ROM synchronization event.
Represents a shared snapshot (image, map state, etc.)
Represents a versioned snapshot of ROM state.