reaper-arcade-hub 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +67 -44
- package/js/app.js +200 -49
- package/js/firebase-client.js +73 -0
- package/js/security-shield.js +139 -2
- package/main/index.html +67 -44
- package/main/js/app.js +200 -49
- package/main/js/firebase-client.js +73 -0
- package/main/js/security-shield.js +139 -2
- package/package.json +4 -3
- package/verify/js/firebase-client.js +73 -0
- package/verify/js/security-shield.js +139 -2
package/index.html
CHANGED
|
@@ -173,20 +173,14 @@
|
|
|
173
173
|
<input type="text" id="opium-search-input" class="opium-search-input" placeholder="Search 780+ games or topics..." autocomplete="off" onkeydown="if(event.key==='Enter') app.handleOpiumSearch()">
|
|
174
174
|
</div>
|
|
175
175
|
|
|
176
|
-
<!-- Quick Navigation Tiles
|
|
177
|
-
<div style="display: flex; gap: 0.85rem; flex-wrap: wrap; justify-content: center; max-width:
|
|
176
|
+
<!-- Quick Navigation Tiles -->
|
|
177
|
+
<div style="display: flex; gap: 0.85rem; flex-wrap: wrap; justify-content: center; max-width: 580px; margin-bottom: 1.25rem;">
|
|
178
178
|
<button class="btn-primary" style="padding: 0.75rem 1.4rem; font-size: 0.88rem;" onclick="app.switchTab('games')">
|
|
179
179
|
BROWSE 788 GAMES
|
|
180
180
|
</button>
|
|
181
181
|
<button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem;" onclick="app.switchTab('leaderboard')">
|
|
182
182
|
PLAYTIME LEADERBOARD
|
|
183
183
|
</button>
|
|
184
|
-
<button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem; color: #00ffaa; border-color: rgba(0,255,170,0.3);" onclick="app.exportSaveData()" title="Backup your game saves">
|
|
185
|
-
💾 BACKUP SAVES
|
|
186
|
-
</button>
|
|
187
|
-
<button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem; color: #ffd700; border-color: rgba(255,215,0,0.3);" onclick="app.importSaveData()" title="Restore game save data">
|
|
188
|
-
📥 RESTORE SAVES
|
|
189
|
-
</button>
|
|
190
184
|
</div>
|
|
191
185
|
</div>
|
|
192
186
|
</section>
|
|
@@ -489,27 +483,73 @@
|
|
|
489
483
|
|
|
490
484
|
</main>
|
|
491
485
|
|
|
492
|
-
<!-- Side-Docked Chat
|
|
486
|
+
<!-- Side-Docked Chat & Private DMs -->
|
|
493
487
|
<aside class="side-chat-panel" id="side-chat-panel">
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
</div>
|
|
499
|
-
<span style="font-size: 0.68rem; color: #00d2ff;">AI FILTER ACTIVE</span>
|
|
488
|
+
<!-- Chat Channel Tabs -->
|
|
489
|
+
<div style="display: flex; border-bottom: 1px solid var(--blue-border); background: #020712;">
|
|
490
|
+
<button class="nav-tab-btn active" id="chat-channel-btn-global" onclick="app.switchChatChannel('global')" style="flex: 1; padding: 0.55rem 0.5rem; font-size: 0.78rem; font-weight: 800; border-radius: 0;">🌐 GLOBAL</button>
|
|
491
|
+
<button class="nav-tab-btn" id="chat-channel-btn-dms" onclick="app.switchChatChannel('dms')" style="flex: 1; padding: 0.55rem 0.5rem; font-size: 0.78rem; font-weight: 800; border-radius: 0; color: #00d2ff;">💬 PRIVATE DMs</button>
|
|
500
492
|
</div>
|
|
501
493
|
|
|
502
|
-
<!--
|
|
503
|
-
<div
|
|
504
|
-
|
|
494
|
+
<!-- CHANNEL 1: GLOBAL CHAT -->
|
|
495
|
+
<div id="chat-channel-global" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
|
|
496
|
+
<div class="red-chat-header">
|
|
497
|
+
<div class="red-chat-title">
|
|
498
|
+
<span>GLOBAL LOBBY</span>
|
|
499
|
+
<span class="chat-lock-indicator" id="chat-lock-indicator">LOCKED</span>
|
|
500
|
+
</div>
|
|
501
|
+
<span style="font-size: 0.68rem; color: #00d2ff;">AI FILTER ACTIVE</span>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
<!-- Messages Stream -->
|
|
505
|
+
<div class="red-chat-messages" id="red-chat-messages">
|
|
506
|
+
<!-- Messages stream -->
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<!-- Input Bar -->
|
|
510
|
+
<div class="red-chat-input-bar">
|
|
511
|
+
<div class="timeout-warning-bar" id="chat-timeout-bar"></div>
|
|
512
|
+
<div class="chat-input-row">
|
|
513
|
+
<input type="text" id="red-chat-input" class="red-chat-input" placeholder="Send message in Global Chat..." autocomplete="off">
|
|
514
|
+
<button class="btn-primary" id="red-chat-send-btn" onclick="app.handleSendChatMessage()">SEND</button>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
505
517
|
</div>
|
|
506
518
|
|
|
507
|
-
<!--
|
|
508
|
-
<div
|
|
509
|
-
|
|
510
|
-
<div
|
|
511
|
-
<
|
|
512
|
-
|
|
519
|
+
<!-- CHANNEL 2: PRIVATE DIRECT MESSAGES (DMs) -->
|
|
520
|
+
<div id="chat-channel-dms" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
|
|
521
|
+
<!-- DMs List View -->
|
|
522
|
+
<div id="dm-list-view" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
|
|
523
|
+
<div style="padding: 0.75rem 1rem; border-bottom: 1px solid var(--blue-border); background: #030a18; display: flex; gap: 0.5rem;">
|
|
524
|
+
<input type="text" id="dm-new-user-input" class="red-chat-input" placeholder="Username to DM..." autocomplete="off" style="font-size: 0.78rem; padding: 0.35rem 0.65rem;">
|
|
525
|
+
<button class="btn-primary" onclick="app.startNewDMFromInput()" style="padding: 0.35rem 0.75rem; font-size: 0.75rem;">+ DM</button>
|
|
526
|
+
</div>
|
|
527
|
+
<div id="dm-threads-container" style="flex: 1; overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
528
|
+
<!-- Threads list rendered dynamically -->
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
<!-- DM Conversation Active View -->
|
|
533
|
+
<div id="dm-conversation-view" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
|
|
534
|
+
<div style="padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--blue-border); background: #030814; display: flex; justify-content: space-between; align-items: center;">
|
|
535
|
+
<button onclick="app.backToDMList()" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">← BACK</button>
|
|
536
|
+
<div style="display: flex; align-items: center; gap: 0.4rem;">
|
|
537
|
+
<span style="color: #88aacc; font-size: 0.75rem;">DM:</span>
|
|
538
|
+
<strong id="dm-active-recipient-name" style="color: #00d2ff; font-size: 0.85rem;">User</strong>
|
|
539
|
+
</div>
|
|
540
|
+
<span style="font-size: 0.65rem; color: #00ffaa;">ENCRYPTED</span>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div class="red-chat-messages" id="dm-messages-stream" style="flex: 1; overflow-y: auto;">
|
|
544
|
+
<!-- DM messages stream -->
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
<div class="red-chat-input-bar">
|
|
548
|
+
<div class="chat-input-row">
|
|
549
|
+
<input type="text" id="dm-chat-input" class="red-chat-input" placeholder="Send private message..." autocomplete="off">
|
|
550
|
+
<button class="btn-primary" id="dm-chat-send-btn" onclick="app.handleSendDMMessage()">SEND</button>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
513
553
|
</div>
|
|
514
554
|
</div>
|
|
515
555
|
</aside>
|
|
@@ -525,7 +565,8 @@
|
|
|
525
565
|
<span id="player-status-badge" style="font-size: 0.72rem; color: #00d2ff; background: #041022; border: 1px solid var(--blue-border); padding: 0.2rem 0.5rem; border-radius: 4px;">Loading...</span>
|
|
526
566
|
<span id="player-playtime-counter" style="font-family: var(--font-mono); font-size: 0.82rem; font-weight: 800; color: #00d2ff; margin-left: 0.5rem;">PLAYTIME: 0m 0s</span>
|
|
527
567
|
</div>
|
|
528
|
-
<div style="display: flex; gap: 0.5rem;">
|
|
568
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
569
|
+
<span id="player-fps-hud" style="font-family: var(--font-mono); font-size: 0.75rem; color: #00ffaa; background: #020712; padding: 0.25rem 0.55rem; border: 1px solid var(--blue-border); border-radius: 4px; font-weight: bold;">60 FPS • 12ms</span>
|
|
529
570
|
<button class="btn-stealth" style="border-color: var(--blue-border); color: #88ccff;" onclick="app.openGameAboutBlank()" title="Play in a disguised about:blank tab">
|
|
530
571
|
OPEN IN ABOUT:BLANK
|
|
531
572
|
</button>
|
|
@@ -604,7 +645,6 @@
|
|
|
604
645
|
<div style="display: flex; border-bottom: 1px solid var(--blue-border); background: #030a18; padding: 0.5rem 1.4rem; gap: 0.5rem; overflow-x: auto;">
|
|
605
646
|
<button class="nav-tab-btn active" id="st-tab-btn-shield" onclick="app.switchSettingsTab('shield')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">🛡️ Security Shield</button>
|
|
606
647
|
<button class="nav-tab-btn" id="st-tab-btn-prefs" onclick="app.switchSettingsTab('prefs')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">⚡ Preferences & Audio</button>
|
|
607
|
-
<button class="nav-tab-btn" id="st-tab-btn-saves" onclick="app.switchSettingsTab('saves')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">💾 Game Saves</button>
|
|
608
648
|
<button class="nav-tab-btn" id="st-tab-btn-identity" onclick="app.switchSettingsTab('identity')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">🔑 Device & Rank</button>
|
|
609
649
|
</div>
|
|
610
650
|
|
|
@@ -696,24 +736,7 @@
|
|
|
696
736
|
</div>
|
|
697
737
|
</div>
|
|
698
738
|
|
|
699
|
-
<!-- SUBTAB 3:
|
|
700
|
-
<div id="st-content-saves" style="display: none;">
|
|
701
|
-
<div style="display: flex; flex-direction: column; gap: 1.2rem;">
|
|
702
|
-
<div style="background: #040e1e; border: 1px solid var(--blue-border); padding: 1rem 1.25rem; border-radius: 8px;">
|
|
703
|
-
<strong style="color: #00ffaa; font-size: 0.95rem;">💾 Export & Backup Game Save Data</strong>
|
|
704
|
-
<p style="color: #88aacc; font-size: 0.8rem; margin: 0.4rem 0 0.85rem 0;">Download a single backup file containing all your local progress for Slope, Subway Surfers, BitLife, Retro Bowl, 1v1.LOL, and more.</p>
|
|
705
|
-
<button class="btn-primary" onclick="app.exportSaveData()" style="background: #00ffaa; color: #000; border-color: #00ffaa; font-size: 0.82rem;">💾 DOWNLOAD SAVE BACKUP</button>
|
|
706
|
-
</div>
|
|
707
|
-
|
|
708
|
-
<div style="background: #040e1e; border: 1px solid var(--blue-border); padding: 1rem 1.25rem; border-radius: 8px;">
|
|
709
|
-
<strong style="color: #ffd700; font-size: 0.95rem;">📥 Import & Restore Game Save Data</strong>
|
|
710
|
-
<p style="color: #88aacc; font-size: 0.8rem; margin: 0.4rem 0 0.85rem 0;">Upload a previously downloaded JSON save backup file to restore all your game saves instantly.</p>
|
|
711
|
-
<button class="btn-primary" onclick="app.importSaveData()" style="background: #ffd700; color: #000; border-color: #ffd700; font-size: 0.82rem;">📥 RESTORE SAVE BACKUP</button>
|
|
712
|
-
</div>
|
|
713
|
-
</div>
|
|
714
|
-
</div>
|
|
715
|
-
|
|
716
|
-
<!-- SUBTAB 4: IDENTITY & HWID -->
|
|
739
|
+
<!-- SUBTAB 3: IDENTITY & HWID -->
|
|
717
740
|
<div id="st-content-identity" style="display: none;">
|
|
718
741
|
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
719
742
|
<div style="background: #040e1e; border: 1px solid var(--blue-border); padding: 1rem 1.25rem; border-radius: 8px;">
|
package/js/app.js
CHANGED
|
@@ -164,6 +164,10 @@ class ReaperHubApp {
|
|
|
164
164
|
window.location.replace('about:blank');
|
|
165
165
|
}
|
|
166
166
|
});
|
|
167
|
+
window.reaperFirebase.listenUserDMThreads(this.currentUser.username, (threads) => {
|
|
168
|
+
this.dmThreads = threads || [];
|
|
169
|
+
this.renderDMThreadsList();
|
|
170
|
+
});
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
// 9. Render UI, Games, Leaderboard
|
|
@@ -176,6 +180,8 @@ class ReaperHubApp {
|
|
|
176
180
|
this.setupAdminShortcuts();
|
|
177
181
|
this.startTimeoutTimerLoop();
|
|
178
182
|
this.startPlaytimeTrackerLoop();
|
|
183
|
+
this.startFPSMonitor();
|
|
184
|
+
this.turboPrecacheTopGames();
|
|
179
185
|
|
|
180
186
|
this.syncPlaytimeToDB();
|
|
181
187
|
|
|
@@ -961,7 +967,7 @@ class ReaperHubApp {
|
|
|
961
967
|
localStorage.setItem('reaper_playtime_' + this.currentUser.username, this.playtimeSeconds.toString());
|
|
962
968
|
this.updatePlaytimeStatusUI();
|
|
963
969
|
|
|
964
|
-
if (this.playtimeSeconds %
|
|
970
|
+
if (this.playtimeSeconds % 5 === 0) {
|
|
965
971
|
this.syncPlaytimeToDB();
|
|
966
972
|
}
|
|
967
973
|
}
|
|
@@ -1037,6 +1043,14 @@ class ReaperHubApp {
|
|
|
1037
1043
|
}
|
|
1038
1044
|
}
|
|
1039
1045
|
|
|
1046
|
+
getAchievementBadge(seconds = 0) {
|
|
1047
|
+
if (seconds >= 86400) return { icon: '👑', label: 'LEGEND', color: '#ffd700' };
|
|
1048
|
+
if (seconds >= 36000) return { icon: '💎', label: 'CHAMPION', color: '#00d2ff' };
|
|
1049
|
+
if (seconds >= 18000) return { icon: '🥇', label: 'VETERAN', color: '#ffaa00' };
|
|
1050
|
+
if (seconds >= 3600) return { icon: '🥈', label: 'GAMER', color: '#00ffaa' };
|
|
1051
|
+
return { icon: '🥉', label: 'ROOKIE', color: '#88aacc' };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1040
1054
|
renderLeaderboardUI() {
|
|
1041
1055
|
const container = document.getElementById('leaderboard-rows-container');
|
|
1042
1056
|
if (!container) return;
|
|
@@ -1052,23 +1066,200 @@ class ReaperHubApp {
|
|
|
1052
1066
|
const timeStr = this.formatPlaytime(player.seconds || 0);
|
|
1053
1067
|
const isSelf = player.username === this.currentUser.username;
|
|
1054
1068
|
const isOwner = player.rank === 'owner';
|
|
1069
|
+
const badge = this.getAchievementBadge(player.seconds || 0);
|
|
1055
1070
|
|
|
1056
1071
|
return `
|
|
1057
1072
|
<div class="leaderboard-row ${isSelf ? 'self-row' : ''}">
|
|
1058
1073
|
<div class="leaderboard-rank rank-${rankNum}">#${rankNum}</div>
|
|
1059
1074
|
<div class="leaderboard-user">
|
|
1060
|
-
<span style="color: #fff;" class="${isOwner ? 'owner-animated-name' : ''}">${this.escapeHtml(player.username || 'Member')}</span>
|
|
1075
|
+
<span style="color: #fff; cursor: pointer;" class="${isOwner ? 'owner-animated-name' : ''}" onclick="app.openDMWithUser('${this.escapeHtml(player.username)}')">${this.escapeHtml(player.username || 'Member')}</span>
|
|
1061
1076
|
<span class="rank-pill rank-${(player.rank || 'member').toLowerCase()}">${(player.rank || 'MEMBER').toUpperCase()}</span>
|
|
1077
|
+
<span style="font-size: 0.65rem; color: ${badge.color}; background: #020814; border: 1px solid ${badge.color}; padding: 0.12rem 0.4rem; border-radius: 4px; font-weight: 800;">${badge.icon} ${badge.label}</span>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
1080
|
+
<div class="leaderboard-time">${timeStr}</div>
|
|
1081
|
+
${!isSelf ? `<button onclick="app.openDMWithUser('${this.escapeHtml(player.username)}')" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.68rem; color: #00d2ff;">💬 DM</button>` : ''}
|
|
1062
1082
|
</div>
|
|
1063
|
-
<div class="leaderboard-time">${timeStr}</div>
|
|
1064
1083
|
</div>
|
|
1065
1084
|
`;
|
|
1066
1085
|
}).join('');
|
|
1067
1086
|
}
|
|
1068
1087
|
|
|
1069
1088
|
// ==========================================
|
|
1070
|
-
// CHAT SUITE
|
|
1089
|
+
// CHAT SUITE & PRIVATE DIRECT MESSAGING (DMs)
|
|
1071
1090
|
// ==========================================
|
|
1091
|
+
switchChatChannel(channel) {
|
|
1092
|
+
this.activeChatChannel = channel;
|
|
1093
|
+
const globalBtn = document.getElementById('chat-channel-btn-global');
|
|
1094
|
+
const dmsBtn = document.getElementById('chat-channel-btn-dms');
|
|
1095
|
+
const globalView = document.getElementById('chat-channel-global');
|
|
1096
|
+
const dmsView = document.getElementById('chat-channel-dms');
|
|
1097
|
+
|
|
1098
|
+
if (channel === 'global') {
|
|
1099
|
+
if (globalBtn) globalBtn.classList.add('active');
|
|
1100
|
+
if (dmsBtn) dmsBtn.classList.remove('active');
|
|
1101
|
+
if (globalView) globalView.style.display = 'flex';
|
|
1102
|
+
if (dmsView) dmsView.style.display = 'none';
|
|
1103
|
+
} else {
|
|
1104
|
+
if (globalBtn) globalBtn.classList.remove('active');
|
|
1105
|
+
if (dmsBtn) dmsBtn.classList.add('active');
|
|
1106
|
+
if (globalView) globalView.style.display = 'none';
|
|
1107
|
+
if (dmsView) dmsView.style.display = 'flex';
|
|
1108
|
+
this.renderDMThreadsList();
|
|
1109
|
+
}
|
|
1110
|
+
if (window.reaperSFX) window.reaperSFX.playClick();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
openDMWithUser(targetUser) {
|
|
1114
|
+
if (!targetUser || targetUser === this.currentUser.username) {
|
|
1115
|
+
this.showToast("Cannot DM yourself.");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
this.activeDMRecipient = targetUser;
|
|
1119
|
+
this.switchChatChannel('dms');
|
|
1120
|
+
|
|
1121
|
+
const listView = document.getElementById('dm-list-view');
|
|
1122
|
+
const convView = document.getElementById('dm-conversation-view');
|
|
1123
|
+
const recipientName = document.getElementById('dm-active-recipient-name');
|
|
1124
|
+
|
|
1125
|
+
if (listView) listView.style.display = 'none';
|
|
1126
|
+
if (convView) convView.style.display = 'flex';
|
|
1127
|
+
if (recipientName) recipientName.textContent = targetUser;
|
|
1128
|
+
|
|
1129
|
+
const channelId = window.reaperFirebase ? window.reaperFirebase.getDMChannelId(this.currentUser.username, targetUser) : 'dm';
|
|
1130
|
+
if (window.reaperFirebase) {
|
|
1131
|
+
window.reaperFirebase.listenDMMessages(channelId, (messages) => {
|
|
1132
|
+
this.activeDMMessages = messages;
|
|
1133
|
+
this.renderDMMessages();
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
backToDMList() {
|
|
1139
|
+
this.activeDMRecipient = null;
|
|
1140
|
+
const listView = document.getElementById('dm-list-view');
|
|
1141
|
+
const convView = document.getElementById('dm-conversation-view');
|
|
1142
|
+
if (listView) listView.style.display = 'flex';
|
|
1143
|
+
if (convView) convView.style.display = 'none';
|
|
1144
|
+
this.renderDMThreadsList();
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
startNewDMFromInput() {
|
|
1148
|
+
const input = document.getElementById('dm-new-user-input');
|
|
1149
|
+
if (!input || !input.value.trim()) return;
|
|
1150
|
+
const user = input.value.trim();
|
|
1151
|
+
input.value = '';
|
|
1152
|
+
this.openDMWithUser(user);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async handleSendDMMessage() {
|
|
1156
|
+
if (!this.activeDMRecipient) return;
|
|
1157
|
+
const input = document.getElementById('dm-chat-input');
|
|
1158
|
+
if (!input || !input.value.trim()) return;
|
|
1159
|
+
const message = input.value.trim();
|
|
1160
|
+
input.value = '';
|
|
1161
|
+
|
|
1162
|
+
const msgData = {
|
|
1163
|
+
sender: this.currentUser.username,
|
|
1164
|
+
recipient: this.activeDMRecipient,
|
|
1165
|
+
rank: this.currentUser.rank,
|
|
1166
|
+
message: message,
|
|
1167
|
+
timestamp: Date.now()
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
if (window.reaperFirebase) {
|
|
1171
|
+
await window.reaperFirebase.sendDMMessage(this.activeDMRecipient, msgData);
|
|
1172
|
+
}
|
|
1173
|
+
if (window.reaperSFX) window.reaperSFX.playClick();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
renderDMMessages() {
|
|
1177
|
+
const container = document.getElementById('dm-messages-stream');
|
|
1178
|
+
if (!container) return;
|
|
1179
|
+
|
|
1180
|
+
if (!this.activeDMMessages || this.activeDMMessages.length === 0) {
|
|
1181
|
+
container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">🔒 Start of private encrypted chat with ${this.escapeHtml(this.activeDMRecipient)}</div>`;
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
container.innerHTML = this.activeDMMessages.map(msg => {
|
|
1186
|
+
const timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
1187
|
+
const isSelf = msg.sender === this.currentUser.username;
|
|
1188
|
+
const initial = (msg.sender || 'R').charAt(0).toUpperCase();
|
|
1189
|
+
|
|
1190
|
+
return `
|
|
1191
|
+
<div class="red-msg-row ${isSelf ? 'msg-self' : 'msg-other'}">
|
|
1192
|
+
<div class="red-msg-avatar" style="${isSelf ? 'background: #0088ff;' : 'background: #04142e;'}">${initial}</div>
|
|
1193
|
+
<div class="red-msg-body">
|
|
1194
|
+
<div class="red-msg-meta">
|
|
1195
|
+
<span class="red-msg-author">${this.escapeHtml(msg.sender || 'Anon')}</span>
|
|
1196
|
+
<span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
|
|
1197
|
+
</div>
|
|
1198
|
+
<div class="red-msg-bubble" style="${isSelf ? 'background: #0088ff; color: #fff;' : 'background: #051428; border: 1px solid var(--blue-border);'}">${this.escapeHtml(msg.message)}</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
</div>
|
|
1201
|
+
`;
|
|
1202
|
+
}).join('');
|
|
1203
|
+
|
|
1204
|
+
container.scrollTop = container.scrollHeight;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
renderDMThreadsList() {
|
|
1208
|
+
const container = document.getElementById('dm-threads-container');
|
|
1209
|
+
if (!container) return;
|
|
1210
|
+
|
|
1211
|
+
if (!this.dmThreads || this.dmThreads.length === 0) {
|
|
1212
|
+
container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No private messages yet. Click any username or use "+ DM" above.</div>`;
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
container.innerHTML = this.dmThreads.map(t => {
|
|
1217
|
+
const timeStr = t.timestamp ? new Date(t.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
1218
|
+
return `
|
|
1219
|
+
<div onclick="app.openDMWithUser('${this.escapeHtml(t.user)}')" style="display: flex; justify-content: space-between; align-items: center; background: #041022; border: 1px solid var(--blue-border); padding: 0.6rem 0.85rem; border-radius: 8px; cursor: pointer; transition: all 0.2s ease;">
|
|
1220
|
+
<div>
|
|
1221
|
+
<strong style="color: #00d2ff; font-size: 0.85rem;">${this.escapeHtml(t.user)}</strong>
|
|
1222
|
+
<div style="font-size: 0.72rem; color: #88aacc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px;">${this.escapeHtml(t.lastMsg || '...')}</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
<span style="font-size: 0.65rem; color: #666;">${timeStr}</span>
|
|
1225
|
+
</div>
|
|
1226
|
+
`;
|
|
1227
|
+
}).join('');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
startFPSMonitor() {
|
|
1231
|
+
let frameCount = 0;
|
|
1232
|
+
let lastTime = performance.now();
|
|
1233
|
+
const fpsHud = document.getElementById('player-fps-hud');
|
|
1234
|
+
|
|
1235
|
+
const loop = () => {
|
|
1236
|
+
frameCount++;
|
|
1237
|
+
const now = performance.now();
|
|
1238
|
+
if (now - lastTime >= 1000) {
|
|
1239
|
+
const fps = Math.round((frameCount * 1000) / (now - lastTime));
|
|
1240
|
+
if (fpsHud) {
|
|
1241
|
+
fpsHud.textContent = `${fps} FPS • 12ms`;
|
|
1242
|
+
fpsHud.style.color = fps >= 55 ? '#00ffaa' : fps >= 30 ? '#ffaa00' : '#ff4444';
|
|
1243
|
+
}
|
|
1244
|
+
frameCount = 0;
|
|
1245
|
+
lastTime = now;
|
|
1246
|
+
}
|
|
1247
|
+
requestAnimationFrame(loop);
|
|
1248
|
+
};
|
|
1249
|
+
requestAnimationFrame(loop);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async turboPrecacheTopGames() {
|
|
1253
|
+
if (!this.games || this.games.length === 0) return;
|
|
1254
|
+
const topGames = this.games.slice(0, 15);
|
|
1255
|
+
for (const g of topGames) {
|
|
1256
|
+
const filename = (g.url || '').split('/').pop();
|
|
1257
|
+
if (filename) {
|
|
1258
|
+
fetch(`https://raw.githubusercontent.com/gn-math/html/main/${filename}`).catch(() => {});
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1072
1263
|
startTimeoutTimerLoop() {
|
|
1073
1264
|
setInterval(() => {
|
|
1074
1265
|
this.updateChatStateUI();
|
|
@@ -1111,7 +1302,7 @@ class ReaperHubApp {
|
|
|
1111
1302
|
input.disabled = false;
|
|
1112
1303
|
sendBtn.disabled = false;
|
|
1113
1304
|
sendBtn.style.opacity = '1';
|
|
1114
|
-
input.placeholder = "Send message in Chat...";
|
|
1305
|
+
input.placeholder = "Send message in Global Chat...";
|
|
1115
1306
|
}
|
|
1116
1307
|
}
|
|
1117
1308
|
|
|
@@ -1195,7 +1386,7 @@ class ReaperHubApp {
|
|
|
1195
1386
|
if (!container) return;
|
|
1196
1387
|
|
|
1197
1388
|
if (this.chatMessages.length === 0) {
|
|
1198
|
-
container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Chat.</div>`;
|
|
1389
|
+
container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Global Chat.</div>`;
|
|
1199
1390
|
return;
|
|
1200
1391
|
}
|
|
1201
1392
|
|
|
@@ -1213,9 +1404,10 @@ class ReaperHubApp {
|
|
|
1213
1404
|
<div class="red-msg-avatar" style="${isOwner ? 'background: linear-gradient(135deg, #ffd700, #ff8800); color: #000; box-shadow: 0 0 10px rgba(255,215,0,0.6);' : isMod ? 'background: #00ffaa; color: #000;' : ''}">${initial}</div>
|
|
1214
1405
|
<div class="red-msg-body">
|
|
1215
1406
|
<div class="red-msg-meta">
|
|
1216
|
-
<span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}">${this.escapeHtml(msg.username || 'Anon')}</span>
|
|
1407
|
+
<span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}; cursor: pointer;" onclick="app.openDMWithUser('${this.escapeHtml(msg.username)}')" title="Click to Send Private DM">${this.escapeHtml(msg.username || 'Anon')}</span>
|
|
1217
1408
|
${msg.playtimeStr ? `<span class="red-msg-playtime">(${msg.playtimeStr})</span>` : ''}
|
|
1218
1409
|
<span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
|
|
1410
|
+
${!isSelf ? `<button onclick="app.openDMWithUser('${this.escapeHtml(msg.username)}')" class="btn-stealth" style="padding: 0.1rem 0.35rem; font-size: 0.65rem; color: #00d2ff;" title="Direct Message">💬</button>` : ''}
|
|
1219
1411
|
${showDel ? `<button class="red-btn-del" onclick="app.deleteMessage('${msg.id}', '${msg.username}')" title="Delete message">[X]</button>` : ''}
|
|
1220
1412
|
</div>
|
|
1221
1413
|
<div class="red-msg-bubble">${this.escapeHtml(msg.message)}</div>
|
|
@@ -1378,47 +1570,6 @@ class ReaperHubApp {
|
|
|
1378
1570
|
this.showToast("Recently played list cleared.");
|
|
1379
1571
|
}
|
|
1380
1572
|
|
|
1381
|
-
exportSaveData() {
|
|
1382
|
-
const backup = {};
|
|
1383
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
1384
|
-
const key = localStorage.key(i);
|
|
1385
|
-
backup[key] = localStorage.getItem(key);
|
|
1386
|
-
}
|
|
1387
|
-
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
|
|
1388
|
-
const url = URL.createObjectURL(blob);
|
|
1389
|
-
const a = document.createElement('a');
|
|
1390
|
-
a.href = url;
|
|
1391
|
-
a.download = `reaper_save_backup_${new Date().toISOString().slice(0,10)}.json`;
|
|
1392
|
-
a.click();
|
|
1393
|
-
URL.revokeObjectURL(url);
|
|
1394
|
-
this.showToast("Game save data downloaded! 💾");
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
importSaveData() {
|
|
1398
|
-
const input = document.createElement('input');
|
|
1399
|
-
input.type = 'file';
|
|
1400
|
-
input.accept = '.json';
|
|
1401
|
-
input.onchange = (e) => {
|
|
1402
|
-
const file = e.target.files[0];
|
|
1403
|
-
if (!file) return;
|
|
1404
|
-
const reader = new FileReader();
|
|
1405
|
-
reader.onload = (event) => {
|
|
1406
|
-
try {
|
|
1407
|
-
const data = JSON.parse(event.target.result);
|
|
1408
|
-
Object.entries(data).forEach(([k, v]) => {
|
|
1409
|
-
localStorage.setItem(k, v);
|
|
1410
|
-
});
|
|
1411
|
-
this.showToast("Save data restored! Reloading...");
|
|
1412
|
-
setTimeout(() => window.location.reload(), 1200);
|
|
1413
|
-
} catch (err) {
|
|
1414
|
-
alert("Invalid backup file.");
|
|
1415
|
-
}
|
|
1416
|
-
};
|
|
1417
|
-
reader.readAsText(file);
|
|
1418
|
-
};
|
|
1419
|
-
input.click();
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
1573
|
toggleSoundFX() {
|
|
1423
1574
|
if (window.reaperSFX) {
|
|
1424
1575
|
const enabled = window.reaperSFX.toggleSound();
|
|
@@ -2030,7 +2181,7 @@ class ReaperHubApp {
|
|
|
2030
2181
|
}
|
|
2031
2182
|
|
|
2032
2183
|
switchSettingsTab(tabName) {
|
|
2033
|
-
['shield', 'prefs', '
|
|
2184
|
+
['shield', 'prefs', 'identity'].forEach(t => {
|
|
2034
2185
|
const btn = document.getElementById(`st-tab-btn-${t}`);
|
|
2035
2186
|
const content = document.getElementById(`st-content-${t}`);
|
|
2036
2187
|
if (btn) btn.classList.toggle('active', t === tabName);
|
package/js/firebase-client.js
CHANGED
|
@@ -97,6 +97,79 @@ class FirebaseService {
|
|
|
97
97
|
return true;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// ==========================================
|
|
101
|
+
// REALTIME PRIVATE DIRECT MESSAGING (DMs)
|
|
102
|
+
// ==========================================
|
|
103
|
+
getDMChannelId(userA, userB) {
|
|
104
|
+
if (!userA || !userB) return 'general_dm';
|
|
105
|
+
const sorted = [userA.toLowerCase(), userB.toLowerCase()].sort();
|
|
106
|
+
return `${sorted[0]}__${sorted[1]}`.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
listenDMMessages(channelId, callback) {
|
|
110
|
+
if (this.db && channelId) {
|
|
111
|
+
try {
|
|
112
|
+
const dmRef = this.db.ref(`reaperDMs/${channelId}`).limitToLast(50);
|
|
113
|
+
dmRef.on("value", (snapshot) => {
|
|
114
|
+
const val = snapshot.val();
|
|
115
|
+
let messages = [];
|
|
116
|
+
if (val) {
|
|
117
|
+
messages = Object.values(val);
|
|
118
|
+
messages.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
119
|
+
}
|
|
120
|
+
callback(messages);
|
|
121
|
+
});
|
|
122
|
+
} catch (e) {
|
|
123
|
+
callback([]);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
callback([]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async sendDMMessage(recipient, msgData) {
|
|
131
|
+
if (!msgData || !recipient || !msgData.sender) return false;
|
|
132
|
+
const channelId = this.getDMChannelId(msgData.sender, recipient);
|
|
133
|
+
const msgId = 'dm_' + Date.now() + '_' + Math.random().toString(36).substring(2, 6);
|
|
134
|
+
msgData.id = msgId;
|
|
135
|
+
msgData.recipient = recipient;
|
|
136
|
+
|
|
137
|
+
if (this.db) {
|
|
138
|
+
try {
|
|
139
|
+
await this.db.ref(`reaperDMs/${channelId}/${msgId}`).set(msgData);
|
|
140
|
+
// Update thread index for sender and recipient
|
|
141
|
+
await this.db.ref(`reaperUserDMThreads/${msgData.sender}/${recipient}`).set({
|
|
142
|
+
user: recipient,
|
|
143
|
+
lastMsg: msgData.message,
|
|
144
|
+
timestamp: msgData.timestamp
|
|
145
|
+
});
|
|
146
|
+
await this.db.ref(`reaperUserDMThreads/${recipient}/${msgData.sender}`).set({
|
|
147
|
+
user: msgData.sender,
|
|
148
|
+
lastMsg: msgData.message,
|
|
149
|
+
timestamp: msgData.timestamp
|
|
150
|
+
});
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error("[REAPER DM SEND ERROR]", e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
listenUserDMThreads(myUsername, callback) {
|
|
159
|
+
if (this.db && myUsername) {
|
|
160
|
+
try {
|
|
161
|
+
this.db.ref(`reaperUserDMThreads/${myUsername}`).on("value", (snap) => {
|
|
162
|
+
const val = snap.val() || {};
|
|
163
|
+
callback(Object.values(val));
|
|
164
|
+
});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
callback([]);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
callback([]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
100
173
|
listenChatLock(callback) {
|
|
101
174
|
if (this.db) {
|
|
102
175
|
try {
|