summaryrefslogtreecommitdiff
path: root/player-server/web/index.html
blob: e0e461fc40aab301e175f31de61c0163da85e500 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Player</title>
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/css/theme.css">
<link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/components.css">
<link rel="stylesheet" href="/css/player.css">
<script type="module" src="/js/app.js"></script>
</head>
<body>
<header class="site-header" role="banner">
  <div class="header-main">
    <a href="/" class="logo"><img src="/logo.png" alt="Player" class="logo-img">Player</a>
    <div class="spacer"></div>
    <button id="sidebar-toggle" class="icon-btn" title="Sets" aria-label="Toggle sets sidebar">โ˜ฐ</button>
    <button id="shuffle-toggle" class="icon-btn" title="Shuffle (r)" aria-label="Toggle shuffle">๐Ÿ”€</button>
    <button id="help-toggle" class="icon-btn" title="Help (?)">?</button>
    <button id="theme-toggle" class="icon-btn" title="Toggle theme" aria-label="Toggle theme"></button>
    <button id="admin-toggle" class="icon-btn hidden" title="Admin panel">โš™</button>
    <button id="logout-btn" class="btn btn-ghost btn-sm">Log out</button>
  </div>
</header>

<div id="scan-indicator" class="scan-indicator hidden" role="status" aria-live="polite">
  <span class="scan-dot" aria-hidden="true"></span>
  <span id="scan-indicator-text">Scanning</span>
</div>

<div id="search-overlay" class="search-overlay hidden">
  <div id="search-bar" class="search-bar search-overlay-bar" role="search">
    <span aria-hidden="true">๐Ÿ”Ž</span>
    <input id="search-input" type="search" placeholder="Search or filterโ€ฆ (press /)" autocomplete="off" aria-label="Search media">
    <button id="search-clear" class="icon-btn btn-sm" title="Clear">โœ•</button>
    <div id="tag-filter-list" class="tag-filter-list" aria-label="Tag filters"></div>
    <div id="search-help" class="search-help hidden">
      <div class="search-help-title">Search syntax</div>
      <div class="search-help-row"><code>min:30</code> minimum duration (seconds)</div>
      <div class="search-help-row"><code>tag:a,b</code> filter by tags</div>
      <div class="search-help-row"><code>like:1</code> liked items only</div>
      <div class="search-help-row"><code>set:yoga</code> select a set by id or name</div>
      <div class="search-help-row"><code>type:video</code> / <code>type:audio</code> / <code>type:image</code></div>
      <div class="search-help-row"><code>sort:random</code> / <code>sort:name</code> / <code>sort:date</code> / <code>sort:duration</code></div>
      <div class="search-help-row"><code>minsize:10</code> min file size (MB)</div>
      <div class="search-help-row"><code>maxsize:500</code> max file size (MB)</div>
    </div>
  </div>
</div>

<!-- Sidebar: hidden by default, toggled via keyboard or menu -->
<nav id="sidebar" class="sidebar" aria-label="Sets">
  <div class="sidebar-header">
    <h2>Sets</h2>
    <button id="menu-close" class="icon-btn btn-sm" title="Close">โœ•</button>
  </div>
  <div id="set-list"></div>
</nav>

<!-- Main page area -->
<div class="page" role="main">
  <div class="main-content">
    <div id="result-count" class="text-muted text-xs mt-1"></div>

    <!-- Empty state hint: shown when no set is selected -->
    <div id="empty-hint" class="empty-hint">
      <p>Welcome to Player</p>
      <p class="text-muted text-sm">Use the sets button to browse, <kbd>/</kbd> to search, or <kbd>?</kbd> for help.</p>
    </div>

    <!-- Breadcrumb bar for folder navigation -->
    <div id="breadcrumb-bar" class="breadcrumb-bar hidden"></div>

    <!-- Media grid: hidden until a set is loaded or search is active -->
    <div id="media-grid" class="media-grid hidden" tabindex="0" aria-label="Media grid"></div>
  </div>
</div>

<!-- Player: hidden until something plays -->
<aside id="player" class="player" aria-label="Player">
  <div class="stage">
    <video id="media-video" playsinline controlslist="nodownload"></video>
    <audio id="media-audio" class="hidden"></audio>
    <img id="cover-art" class="cover-art hidden" alt="Cover" loading="lazy">
    <img id="media-image" class="media-image hidden" alt="" loading="lazy">
    <div id="big-play" class="big-play" role="button" aria-label="Play" tabindex="0">
      <div class="play-icon">โ–ถ</div>
    </div>
  </div>
  <div class="controls">
    <button id="btn-minimize" class="icon-btn" aria-label="Minimize player (Shift+C)">โ–พ</button>
    <button id="btn-play" class="icon-btn" aria-label="Play/Pause (Space/p)">โ–ถ</button>
    <div class="time" id="time-elapsed">0:00</div>
    <div id="progress-track" class="progress-track" tabindex="0" aria-label="Seek">
      <div id="progress-buffered" class="progress-buffered"></div>
      <div id="progress-fill" class="progress-fill"></div>
      <div id="progress-thumb" class="progress-thumb" aria-hidden="true"></div>
    </div>
    <div class="time" id="time-total">0:00</div>
    <button id="btn-prev" class="icon-btn" aria-label="Previous">โฎ</button>
    <button id="btn-next" class="icon-btn" aria-label="Next">โญ</button>
    <div class="volume-control video-only">
      <button id="btn-mute" class="icon-btn" aria-label="Mute">๐Ÿ”Š</button>
      <input id="volume-slider" type="range" min="0" max="1" step="0.05" value="1" class="volume-slider" aria-label="Volume">
    </div>
    <button id="btn-fullscreen" class="icon-btn" aria-label="Fullscreen (f)">โ›ถ</button>
    <span id="crop-indicator" class="crop-indicator hidden" title="Crop mode active (c)">โœ‚๏ธ</span>
  </div>
  <button id="btn-restore-player" class="player-restore" type="button" aria-label="Restore player">
    <span id="player-restore-title">Player minimized</span>
  </button>
</aside>

<!-- Detached player placeholder bar -->
<div id="detached-bar" class="detached-bar hidden" role="status" aria-label="Player detached">
  <span>Player detached โ€” press <kbd>d</kbd> to reattach</span>
</div>

<footer class="toast" id="toast" role="status" aria-live="polite"></footer>

<!-- Help Modal -->
<div id="help-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="help-title">
  <div class="modal help-modal">
    <div class="modal-header">
      <h3 id="help-title">Keyboard Shortcuts</h3>
      <div class="spacer"></div>
      <button id="help-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <div class="help-content">
      <div class="help-shortcuts">
        <div class="help-row"><span><kbd>โ†‘</kbd> <kbd>โ†“</kbd></span><span>Navigate media list</span></div>
        <div class="help-row"><span><kbd>k</kbd> <kbd>j</kbd></span><span>Navigate media list</span></div>
        <div class="help-row"><span><kbd>โ†</kbd> <kbd>โ†’</kbd></span><span>Seek or switch pages</span></div>
        <div class="help-row"><span><kbd>h</kbd> <kbd>l</kbd></span><span>Seek or switch pages</span></div>
        <div class="help-row"><span><kbd>Enter</kbd></span><span>Open selected item</span></div>
        <div class="help-row"><span><kbd>Space</kbd> <kbd>p</kbd></span><span>Play / pause</span></div>
        <div class="help-row"><span><kbd>N</kbd> <kbd>P</kbd></span><span>Next / previous track</span></div>
        <div class="help-row"><span><kbd>R</kbd></span><span>Play random track</span></div>
        <div class="help-row"><span><kbd>M</kbd></span><span>Rescan media library</span></div>
        <div class="help-row"><span><kbd>i</kbd></span><span>Show / hide media info</span></div>
        <div class="help-row"><span><kbd>n</kbd></span><span>Open notes</span></div>
        <div class="help-row"><span><kbd>f</kbd></span><span>Toggle fullscreen</span></div>
        <div class="help-row"><span><kbd>PgUp</kbd> <kbd>PgDn</kbd></span><span>Seek +10% / โˆ’10% (fullscreen)</span></div>
        <div class="help-row"><span><kbd>C</kbd></span><span>Minimize player</span></div>
        <div class="help-row"><span><kbd>d</kbd></span><span>Detach / attach player</span></div>
        <div class="help-row"><span><kbd>D</kbd></span><span>Download selected media</span></div>
        <div class="help-row"><span><kbd>T</kbd></span><span>Regenerate thumbnail</span></div>
        <div class="help-row"><span><kbd>Esc</kbd></span><span>Exit fullscreen / close</span></div>
        <div class="help-row"><span><kbd>q</kbd></span><span>Stop playback and close player</span></div>
        <div class="help-row"><span><kbd>Backspace</kbd></span><span>Go up one folder</span></div>
        <div class="help-row"><span><kbd>r</kbd></span><span>Enable / reshuffle</span></div>
        <div class="help-row"><span><kbd>s</kbd></span><span>Share selected media</span></div>
        <div class="help-row"><span><kbd>c</kbd></span><span>Toggle crop mode (fullscreen)</span></div>
        <div class="help-row"><span><kbd>Shift+โ†‘</kbd> <kbd>Shift+โ†“</kbd> <kbd>Shift+โ†</kbd> <kbd>Shift+โ†’</kbd></span><span>Nudge crop position</span></div>
        <div class="help-row"><span><kbd>x</kbd></span><span>Cycle crop anchor preset</span></div>
        <div class="help-row"><span><kbd>L</kbd></span><span>My Shares</span></div>
        <div class="help-row"><span><kbd>?</kbd></span><span>Show / hide help</span></div>
      </div>
      <div class="help-search text-muted text-xs mt-2">
        Search: plain text searches file name. Syntax: <code>set:yoga</code> <code>min:30</code> <code>max:55</code> <code>tag:a,b</code> <code>like:1</code> <code>type:video</code> <code>sort:random</code> <code>minsize:10</code> <code>maxsize:500</code>
      </div>
      <div class="help-shares text-muted text-xs mt-2">My Shares: <kbd>โ†‘</kbd> / <kbd>โ†“</kbd> or <kbd>k</kbd> / <kbd>j</kbd> navigate, <kbd>Enter</kbd> copies, <kbd>Delete</kbd> revokes, <kbd>Esc</kbd> closes.</div>
    </div>
  </div>
</div>

<!-- Media Info Modal -->
<div id="media-info-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="media-info-title">
  <div id="media-info-panel" class="modal modal-wide" tabindex="-1">
    <div class="modal-header">
      <h3 id="media-info-title">Media Info</h3>
      <div class="spacer"></div>
      <button id="media-info-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <div id="media-info-body"></div>
  </div>
</div>

<!-- Notes Modal -->
<div id="notes-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="notes-title">
  <div class="modal">
    <div class="modal-header">
      <h3 id="notes-title">Notes</h3>
      <div class="spacer"></div>
      <button id="notes-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <textarea id="notes-textarea" rows="6" placeholder="Add your notesโ€ฆ"></textarea>
    <div class="flex justify-end gap-2 mt-1">
      <button id="notes-delete" class="btn btn-danger btn-sm">Delete</button>
      <button id="notes-save" class="btn btn-primary btn-sm">Save</button>
    </div>
  </div>
</div>

<!-- Tags Modal -->
<div id="tags-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="tags-title">
  <div class="modal">
    <div class="modal-header">
      <h3 id="tags-title">Tags</h3>
      <div class="spacer"></div>
      <button id="tags-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <div id="tags-list" class="flex flex-wrap gap-2 mb-2"></div>
    <div class="flex gap-2">
      <input id="tags-new" type="text" placeholder="New tagโ€ฆ" autocomplete="off">
      <button id="tags-add" class="btn btn-primary btn-sm">Add</button>
    </div>
  </div>
</div>

<!-- Upload Modal -->
<div id="upload-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="upload-title">
  <div class="modal">
    <div class="modal-header">
      <h3 id="upload-title">Upload</h3>
      <div class="spacer"></div>
      <button id="upload-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <form id="upload-form" class="flex flex-col gap-2">
      <input id="upload-file" type="file" required>
      <button type="submit" class="btn btn-primary btn-sm">Upload to current set</button>
    </form>
  </div>
</div>

<!-- Admin Modal -->
<div id="admin-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="admin-title">
    <div class="modal modal-wide">
    <div class="modal-header">
      <h3 id="admin-title">Admin</h3>
      <div class="spacer"></div>
      <button id="admin-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <div class="flex gap-4 flex-wrap">
      <section class="flex-1-18">
        <h4 class="my-1 text-90">Users</h4>
        <div id="admin-users"></div>
        <form id="admin-user-form" class="mt-2">
          <input name="username" placeholder="Username" required autocomplete="off">
          <input type="password" name="password" placeholder="Password" required autocomplete="new-password">
          <label class="flex align-center gap-1 text-xs">
            <input type="checkbox" name="is_admin"> Admin
          </label>
          <button type="submit" class="btn btn-primary btn-sm mt-1">Add user</button>
        </form>
      </section>
      <section class="flex-1-18">
        <h4 class="my-1 text-90">Permissions</h4>
        <div id="admin-permissions"></div>
      </section>
    </div>
    <div class="flex gap-2 mt-3">
      <button id="admin-rescan" class="btn btn-primary btn-sm">Trigger rescan</button>
      <button id="admin-trash" class="btn btn-danger btn-sm">View trash</button>
      <button id="admin-podcasts" class="btn btn-primary btn-sm">Podcasts</button>
    </div>
  </div>
</div>

<!-- Podcast Manager Modal -->
<div id="podcast-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="podcast-title">
  <div class="modal modal-wide">
    <div class="modal-header">
      <h3 id="podcast-title">Podcast Feeds</h3>
      <div class="spacer"></div>
      <button id="podcast-close" class="icon-btn" aria-label="Close">โœ•</button>
    </div>
    <div class="flex gap-4 flex-wrap">
      <section class="flex-1-18">
        <h4 class="my-1 text-90">Subscribe</h4>
        <form id="podcast-form" class="mt-2">
          <input id="podcast-url" type="url" placeholder="https://example.com/feed.xml" required autocomplete="off">
          <button type="submit" class="btn btn-primary btn-sm mt-1">Subscribe</button>
        </form>
      </section>
      <section class="flex-1-18">
        <h4 class="my-1 text-90">Active Feeds</h4>
        <div id="podcast-list"></div>
      </section>
    </div>
  </div>
</div>

<!-- Shares Modal -->
<div id="shares-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="shares-title">
  <div class="modal modal-wide">
    <div class="modal-header">
      <h3 id="shares-title">My Shares</h3>
      <div class="spacer"></div>
      <button id="shares-close" class="icon-btn" aria-label="Close">&#10005;</button>
    </div>
    <div id="shares-list"></div>
    <div class="shares-hint">
      <kbd>&#8593;</kbd> <kbd>&#8595;</kbd> <kbd>k</kbd> <kbd>j</kbd> navigate &middot;
      <kbd>Enter</kbd> copy &middot;
        <kbd>Delete</kbd> revoke &middot;
        <kbd>Esc</kbd> close
    </div>
  </div>
</div>

</body>
</html>