music on atproto
plyr.fm
1// Set up auth header listener first (before any htmx requests)
2let currentToken = null;
3let currentFilter = 'pending'; // track current filter state
4
5document.body.addEventListener('htmx:configRequest', function(evt) {
6 if (currentToken) {
7 evt.detail.headers['X-Moderation-Key'] = currentToken;
8 }
9});
10
11// Track filter changes via htmx
12document.body.addEventListener('htmx:afterRequest', function(evt) {
13 const url = evt.detail.pathInfo?.requestPath || '';
14 const match = url.match(/filter=(\w+)/);
15 if (match) {
16 currentFilter = match[1];
17 }
18});
19
20function showMain() {
21 document.getElementById('main-content').style.display = 'block';
22}
23
24function authenticate() {
25 const token = document.getElementById('auth-token').value;
26 if (token && token !== '••••••••') {
27 localStorage.setItem('mod_token', token);
28 currentToken = token;
29 showMain();
30 htmx.trigger('#flags-list', 'load');
31 }
32}
33
34// Check for saved token on load
35const savedToken = localStorage.getItem('mod_token');
36if (savedToken) {
37 document.getElementById('auth-token').value = '••••••••';
38 currentToken = savedToken;
39 showMain();
40 // Trigger load after DOM is ready and htmx is initialized
41 setTimeout(() => htmx.trigger('#flags-list', 'load'), 0);
42}
43
44// Handle auth errors
45document.body.addEventListener('htmx:responseError', function(evt) {
46 if (evt.detail.xhr.status === 401) {
47 localStorage.removeItem('mod_token');
48 currentToken = null;
49 showToast('invalid token', 'error');
50 }
51});
52
53function showToast(message, type) {
54 const toast = document.getElementById('toast');
55 toast.className = 'toast ' + type;
56 toast.textContent = message;
57 toast.style.display = 'block';
58 setTimeout(() => { toast.style.display = 'none'; }, 3000);
59}
60
61// Reason options for false positive resolution
62const REASONS = [
63 { value: 'original_artist', label: 'original artist' },
64 { value: 'licensed', label: 'licensed' },
65 { value: 'fingerprint_noise', label: 'fp noise' },
66 { value: 'cover_version', label: 'cover/remix' },
67 { value: 'other', label: 'other' }
68];
69
70// Step 1 -> Step 2: Show reason selection buttons
71function showReasonSelect(btn) {
72 const flow = btn.closest('.resolve-flow');
73
74 // Replace button with reason selection
75 flow.innerHTML = `
76 <div class="reason-select">
77 ${REASONS.map(r => `
78 <button type="button" class="reason-btn" data-reason="${r.value}" onclick="selectReason(this, '${r.value}')">
79 ${r.label}
80 </button>
81 `).join('')}
82 <button type="button" class="reason-btn cancel" onclick="cancelResolve(this)">✕</button>
83 </div>
84 `;
85}
86
87// Step 2 -> Step 3: Show confirmation
88function selectReason(btn, reason) {
89 const flow = btn.closest('.resolve-flow');
90 const reasonLabel = REASONS.find(r => r.value === reason)?.label || reason;
91
92 // Replace with confirmation
93 flow.innerHTML = `
94 <div class="confirm-step">
95 <span class="confirm-text">resolve as <strong>${reasonLabel}</strong>?</span>
96 <button type="button" class="btn btn-confirm" onclick="confirmResolve(this, '${reason}')">confirm</button>
97 <button type="button" class="reason-btn cancel" onclick="cancelResolve(this)">cancel</button>
98 </div>
99 `;
100}
101
102// Step 3: Actually submit the resolution
103function confirmResolve(btn, reason) {
104 const flow = btn.closest('.resolve-flow');
105 const uri = flow.dataset.uri;
106 const val = flow.dataset.val;
107
108 // Show loading state
109 btn.disabled = true;
110 btn.textContent = '...';
111
112 // Submit via fetch (URLSearchParams for application/x-www-form-urlencoded)
113 const params = new URLSearchParams();
114 params.append('uri', uri);
115 params.append('val', val);
116 params.append('reason', reason);
117
118 fetch('/admin/resolve-htmx', {
119 method: 'POST',
120 headers: {
121 'X-Moderation-Key': currentToken,
122 'Content-Type': 'application/x-www-form-urlencoded'
123 },
124 body: params
125 })
126 .then(response => {
127 if (response.ok) {
128 return response.text();
129 }
130 throw new Error('Failed to resolve');
131 })
132 .then(html => {
133 // Parse and show toast from response
134 const match = html.match(/resolved: ([^<]+)/);
135 if (match) {
136 showToast(match[0], 'success');
137 }
138 // Refresh flags list with current filter
139 refreshFlagsList();
140 })
141 .catch(err => {
142 showToast('failed to resolve: ' + err.message, 'error');
143 cancelResolve(btn);
144 });
145}
146
147// Refresh flags list preserving current filter
148function refreshFlagsList() {
149 htmx.ajax('GET', `/admin/flags-html?filter=${currentFilter}`, '#flags-list');
150}
151
152// Cancel: restore original button
153function cancelResolve(btn) {
154 const flow = btn.closest('.resolve-flow');
155 flow.innerHTML = `
156 <button type="button" class="btn btn-warning" onclick="showReasonSelect(this)">
157 mark false positive
158 </button>
159 `;
160}