tinyjukebox 0.47.0__py3-none-any.whl

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.
Files changed (154) hide show
  1. tinyjukebox/__init__.py +7 -0
  2. tinyjukebox/albumplayer/__init__.py +1 -0
  3. tinyjukebox/albumplayer/aalbumplayer.py +402 -0
  4. tinyjukebox/albumplayer/clock.py +39 -0
  5. tinyjukebox/albumplayer/dummyplayer.py +49 -0
  6. tinyjukebox/albumplayer/player_interface.py +97 -0
  7. tinyjukebox/albumplayer/radioplayer.py +391 -0
  8. tinyjukebox/albumplayer/simpleplayer.py +419 -0
  9. tinyjukebox/albumplayer/spotifyplayer.py +671 -0
  10. tinyjukebox/backup.py +176 -0
  11. tinyjukebox/backups.py +340 -0
  12. tinyjukebox/cli.py +1076 -0
  13. tinyjukebox/control.py +1386 -0
  14. tinyjukebox/core/__init__.py +1 -0
  15. tinyjukebox/core/config.py +100 -0
  16. tinyjukebox/core/logging/__init__.py +113 -0
  17. tinyjukebox/core/microservice.py +303 -0
  18. tinyjukebox/core/pubsub/__init__.py +15 -0
  19. tinyjukebox/core/pubsub/base.py +196 -0
  20. tinyjukebox/core/pubsub/localqueue.py +102 -0
  21. tinyjukebox/core/pubsub/statistics.py +57 -0
  22. tinyjukebox/core/py.typed +0 -0
  23. tinyjukebox/core/schema/__init__.py +5 -0
  24. tinyjukebox/core/schema/logger.py +40 -0
  25. tinyjukebox/core/schema/microservice.py +48 -0
  26. tinyjukebox/core/threads.py +39 -0
  27. tinyjukebox/core/ui/__init__.py +5 -0
  28. tinyjukebox/core/ui/cli.py +120 -0
  29. tinyjukebox/core/ui/tools.py +73 -0
  30. tinyjukebox/core/unexpected.py +46 -0
  31. tinyjukebox/data/__init__.py +0 -0
  32. tinyjukebox/data/activities.py +175 -0
  33. tinyjukebox/data/activity.py +138 -0
  34. tinyjukebox/data/common.py +91 -0
  35. tinyjukebox/data/degraded.py +39 -0
  36. tinyjukebox/data/jsondata.py +218 -0
  37. tinyjukebox/data/sanitycheck.py +71 -0
  38. tinyjukebox/data/tag.py +98 -0
  39. tinyjukebox/data/tags.py +160 -0
  40. tinyjukebox/data/target.py +568 -0
  41. tinyjukebox/data/targetalbumtools.py +88 -0
  42. tinyjukebox/data/targets.py +256 -0
  43. tinyjukebox/data/volumelimits.py +68 -0
  44. tinyjukebox/data_folder.py +180 -0
  45. tinyjukebox/devices/__init__.py +1 -0
  46. tinyjukebox/devices/baseclasses/__init__.py +1 -0
  47. tinyjukebox/devices/baseclasses/adriver.py +115 -0
  48. tinyjukebox/devices/baseclasses/aevents.py +47 -0
  49. tinyjukebox/devices/baseclasses/gpiolevel.py +16 -0
  50. tinyjukebox/devices/bluetooth/__init__.py +1 -0
  51. tinyjukebox/devices/bluetooth/avrcp_input.py +118 -0
  52. tinyjukebox/devices/bluetooth/bluez.py +446 -0
  53. tinyjukebox/devices/bluetooth/link.py +396 -0
  54. tinyjukebox/devices/bluetooth/virtual.py +155 -0
  55. tinyjukebox/devices/drivermanager/__init__.py +1 -0
  56. tinyjukebox/devices/drivermanager/driverfactory.py +75 -0
  57. tinyjukebox/devices/drivermanager/drivermanager.py +151 -0
  58. tinyjukebox/devices/drivers/__init__.py +31 -0
  59. tinyjukebox/devices/drivers/bluetooth.py +257 -0
  60. tinyjukebox/devices/drivers/input.py +165 -0
  61. tinyjukebox/devices/drivers/rfid.py +297 -0
  62. tinyjukebox/devices/drivers/rgbled.py +215 -0
  63. tinyjukebox/devices/py.typed +0 -0
  64. tinyjukebox/devices/schema/__init__.py +1 -0
  65. tinyjukebox/devices/schema/adriver.py +33 -0
  66. tinyjukebox/devices/schema/bluetooth.py +31 -0
  67. tinyjukebox/devices/schema/drivermanager.py +41 -0
  68. tinyjukebox/devices/schema/input.py +23 -0
  69. tinyjukebox/devices/schema/rfid.py +44 -0
  70. tinyjukebox/devices/schema/rgbled.py +26 -0
  71. tinyjukebox/diagnostics.py +179 -0
  72. tinyjukebox/errors.py +91 -0
  73. tinyjukebox/headset.py +296 -0
  74. tinyjukebox/images.py +133 -0
  75. tinyjukebox/latency.py +43 -0
  76. tinyjukebox/library/__init__.py +2 -0
  77. tinyjukebox/library/diagnostics.py +183 -0
  78. tinyjukebox/library/journal.py +94 -0
  79. tinyjukebox/library/releases.py +348 -0
  80. tinyjukebox/library/schema.py +98 -0
  81. tinyjukebox/library/service.py +1115 -0
  82. tinyjukebox/library/statistics.py +288 -0
  83. tinyjukebox/library/store.py +1386 -0
  84. tinyjukebox/messages.py +202 -0
  85. tinyjukebox/metadata.py +133 -0
  86. tinyjukebox/py.typed +0 -0
  87. tinyjukebox/restore.py +391 -0
  88. tinyjukebox/rgbled.py +158 -0
  89. tinyjukebox/schema/__init__.py +1 -0
  90. tinyjukebox/schema/activities.py +27 -0
  91. tinyjukebox/schema/tags.py +54 -0
  92. tinyjukebox/schema/targets.py +83 -0
  93. tinyjukebox/schema/tinyjukebox.py +217 -0
  94. tinyjukebox/schema/volume.py +28 -0
  95. tinyjukebox/slots.py +234 -0
  96. tinyjukebox/spotify/__init__.py +1 -0
  97. tinyjukebox/spotify/account.py +647 -0
  98. tinyjukebox/spotify/engine.py +568 -0
  99. tinyjukebox/spotify/oembed.py +65 -0
  100. tinyjukebox/spotify/soloist.py +198 -0
  101. tinyjukebox/spotify/sound.py +316 -0
  102. tinyjukebox/stm/__init__.py +0 -0
  103. tinyjukebox/stm/base.py +174 -0
  104. tinyjukebox/stm/playradio.py +237 -0
  105. tinyjukebox/stm/playspotify.py +418 -0
  106. tinyjukebox/stm/stmoperation.py +769 -0
  107. tinyjukebox/stm/stmplayalbum.py +655 -0
  108. tinyjukebox/stm/stmvolume.py +221 -0
  109. tinyjukebox/sync/__init__.py +42 -0
  110. tinyjukebox/sync/box.py +486 -0
  111. tinyjukebox/sync/client.py +189 -0
  112. tinyjukebox/sync/data.py +489 -0
  113. tinyjukebox/sync/files.py +447 -0
  114. tinyjukebox/sync/handshake.py +42 -0
  115. tinyjukebox/sync/link.py +379 -0
  116. tinyjukebox/sync/manifest.py +162 -0
  117. tinyjukebox/sync/util.py +137 -0
  118. tinyjukebox/tinyjukebox.py +680 -0
  119. tinyjukebox/tools/__init__.py +1 -0
  120. tinyjukebox/tools/backup.py +173 -0
  121. tinyjukebox/tools/fill_library.py +120 -0
  122. tinyjukebox/tools/import_cards.py +274 -0
  123. tinyjukebox/tools/import_covers.py +138 -0
  124. tinyjukebox/tools/migrate_box01.py +151 -0
  125. tinyjukebox/volumemanager.py +355 -0
  126. tinyjukebox/web/__init__.py +1 -0
  127. tinyjukebox/web/auth.py +373 -0
  128. tinyjukebox/web/certificates.py +111 -0
  129. tinyjukebox/web/logbuffer.py +62 -0
  130. tinyjukebox/web/server.py +1237 -0
  131. tinyjukebox/web/uploads.py +262 -0
  132. tinyjukebox/web/users.py +106 -0
  133. tinyjukebox/web/web/app.js +3050 -0
  134. tinyjukebox/web/web/favicon.svg +6 -0
  135. tinyjukebox/web/web/help/de/admin.html +714 -0
  136. tinyjukebox/web/web/help/de/manual.html +1147 -0
  137. tinyjukebox/web/web/help/de/quick-guide.html +79 -0
  138. tinyjukebox/web/web/help/en/admin.html +701 -0
  139. tinyjukebox/web/web/help/en/manual.html +1126 -0
  140. tinyjukebox/web/web/help/en/quick-guide.html +79 -0
  141. tinyjukebox/web/web/help/help.css +25 -0
  142. tinyjukebox/web/web/help/images/box-2026.jpg +0 -0
  143. tinyjukebox/web/web/help/images/box-back.jpg +0 -0
  144. tinyjukebox/web/web/help/images/lid-top.jpg +0 -0
  145. tinyjukebox/web/web/help/images/logo-mark.svg +38 -0
  146. tinyjukebox/web/web/index.html +867 -0
  147. tinyjukebox/web/web/style.css +412 -0
  148. tinyjukebox/web/web/texts/de.json +892 -0
  149. tinyjukebox/web/web/texts/en.json +892 -0
  150. tinyjukebox-0.47.0.dist-info/METADATA +236 -0
  151. tinyjukebox-0.47.0.dist-info/RECORD +154 -0
  152. tinyjukebox-0.47.0.dist-info/WHEEL +4 -0
  153. tinyjukebox-0.47.0.dist-info/entry_points.txt +9 -0
  154. tinyjukebox-0.47.0.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,867 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>Tiny Jukebox</title>
8
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
9
+ <link rel="stylesheet" href="style.css">
10
+ <script src="app.js" defer></script>
11
+ </head>
12
+ <body>
13
+
14
+ <section id="login-view" class="login" hidden>
15
+ <form id="login-form" class="card login-card">
16
+ <div class="brand brand-large"><svg class="brand-logo" viewBox="0 0 64 64" aria-hidden="true"><circle class="lid" cx="32" cy="32" r="29"/><circle class="window" cx="31" cy="41" r="10.5"/><circle class="brand-led" cx="13.29" cy="28.7" r="2.8"/><circle class="key" cx="17.45" cy="19.79" r="4.2"/><circle class="key" cx="25.5" cy="14.15" r="4.2"/><circle class="key key-yellow" cx="35.3" cy="13.29" r="4.2"/><circle class="key key-yellow" cx="44.21" cy="17.45" r="4.2"/></svg>Tiny Jukebox</div>
17
+ <!-- the library or which box (app.js: showIdentity) -->
18
+ <p id="login-place" class="login-place" hidden></p>
19
+ <label><span data-t="login.user"></span> <input name="username" autocomplete="username" required autofocus></label>
20
+ <label><span data-t="login.password"></span> <input name="password" type="password" autocomplete="current-password" required></label>
21
+ <button type="submit" class="primary" data-t="login.submit"></button>
22
+ <p id="login-error" class="error" role="alert"></p>
23
+ <select class="language" data-t-label="language.label"></select>
24
+ <a id="login-help" class="login-help" target="_blank" rel="noopener" data-t="help.quick_guide"></a>
25
+ </form>
26
+ </section>
27
+
28
+ <div id="app-view" hidden>
29
+ <header class="topbar">
30
+ <div class="brand"><svg class="brand-logo" viewBox="0 0 64 64" aria-hidden="true"><circle class="lid" cx="32" cy="32" r="29"/><circle class="window" cx="31" cy="41" r="10.5"/><circle class="brand-led" cx="13.29" cy="28.7" r="2.8"/><circle class="key" cx="17.45" cy="19.79" r="4.2"/><circle class="key" cx="25.5" cy="14.15" r="4.2"/><circle class="key key-yellow" cx="35.3" cy="13.29" r="4.2"/><circle class="key key-yellow" cx="44.21" cy="17.45" r="4.2"/></svg>Tiny Jukebox
31
+ <span class="library-only muted small" data-t="library.title"></span>
32
+ <span id="place-name" class="box-only place-name" hidden></span>
33
+ <span id="led" class="led box-only" data-t-title="led.label" role="img" data-t-label="led.label"></span>
34
+ <span id="headset-icon" class="headset-icon" data-t-title="headset.connected" role="img"
35
+ data-t-label="headset.connected" hidden>🎧</span></div>
36
+ <nav class="tabs" role="tablist">
37
+ <button role="tab" data-tab="now" class="active box-only" data-t="tabs.now"></button>
38
+ <button role="tab" data-tab="boxes" class="library-only" data-t="tabs.boxes"></button>
39
+ <button role="tab" data-tab="versions" class="library-only" data-t="tabs.versions"></button>
40
+ <button role="tab" data-tab="tags" data-t="tabs.tags"></button>
41
+ <button role="tab" id="tab-button-statistics" data-tab="statistics" data-t="tabs.statistics"></button>
42
+ <button role="tab" data-tab="tag-management" data-t="tabs.tagman"></button>
43
+ <button role="tab" data-tab="albums" data-t="tabs.albums"></button>
44
+ <button role="tab" data-tab="radio" data-t="tabs.radio"></button>
45
+ <button role="tab" data-tab="spotify" data-t="tabs.spotify"></button>
46
+ <button role="tab" data-tab="bluetooth" class="box-only" data-t="tabs.bluetooth"></button>
47
+ <button role="tab" id="tab-button-upload" data-tab="upload" class="admin-only" data-t="tabs.upload"></button>
48
+ <button role="tab" data-tab="system" data-t="tabs.system"></button>
49
+ <button role="tab" data-tab="log" class="admin-only" data-t="tabs.log"></button>
50
+ </nav>
51
+ <div class="user"><select class="language" data-t-label="language.label"></select><span id="user-name"></span>
52
+ <!-- the help of the tab that is open (app.js: HELP_OF_TAB) -->
53
+ <a id="help-link" class="button help-link" target="_blank" rel="noopener" data-t="help.button"
54
+ data-t-title="help.title"></a><button id="logout" class="link" data-t="logout"></button></div>
55
+ </header>
56
+
57
+ <main>
58
+ <!-- after every login, until "Do not show it again" was ticked in this browser -->
59
+ <div id="help-hint" class="card notice row" hidden>
60
+ <span class="grow-text" data-t="help.hint"></span>
61
+ <a id="help-hint-open" class="button" target="_blank" rel="noopener" data-t="help.quick_guide"></a>
62
+ <label class="check"><input type="checkbox" id="help-hint-never"> <span data-t="help.hint.never"></span></label>
63
+ <button id="help-hint-close" class="link" data-t="common.close"></button>
64
+ </div>
65
+ <!-- a box without a library that asks before it installs a release: on every tab, until an admin answered -->
66
+ <div id="version-banner" class="card notice row admin-only box-only" hidden>
67
+ <span id="version-banner-text" class="grow-text"></span>
68
+ <button id="version-banner-install" class="primary" data-t="version.install"></button>
69
+ <button id="version-banner-skip" data-t="version.skip"></button>
70
+ <button id="version-banner-notes" class="link" data-t="version.offer.notes"></button>
71
+ </div>
72
+ <!-- now playing -->
73
+ <section id="tab-now" class="tab">
74
+ <div class="card now">
75
+ <div class="cover" id="now-cover"><span class="cover-placeholder">♪</span></div>
76
+ <div class="now-info">
77
+ <span id="now-state" class="badge">…</span>
78
+ <h1 id="now-title">–</h1>
79
+ <p id="now-artist" class="muted"></p>
80
+ <p class="muted small"><span id="now-tag"></span>
81
+ <button id="now-unlink" class="link admin-only" hidden data-t="common.unlink"></button></p>
82
+ <div id="now-track" class="track-now" hidden>
83
+ <div class="track-title" id="now-track-title"></div>
84
+ <div class="progress"><div id="now-progress" class="progress-bar"></div></div>
85
+ <div class="times small muted"><span id="now-pos">0:00</span><span id="now-len">0:00</span></div>
86
+ </div>
87
+ <div class="controls">
88
+ <button class="round" id="btn-prev" data-t-title="now.prev.title" data-t-label="common.back">⏮</button>
89
+ <button class="round" id="btn-playpause" data-t-title="common.pause" data-t-label="common.pause">⏸</button>
90
+ <button class="round" id="btn-next" data-t-title="now.next.title" data-t-label="common.next">⏭</button>
91
+ <button class="round" id="btn-restart" data-t-title="now.restart" data-t-label="now.restart">↺</button>
92
+ </div>
93
+ <div class="volume">
94
+ <button class="round small-round" id="btn-vol-down" data-t-title="now.quieter.title" data-t-label="common.quieter">−</button>
95
+ <input type="range" id="volume" min="0" max="100" step="1" data-t-label="common.volume">
96
+ <button class="round small-round" id="btn-vol-up" data-t-title="now.louder.title" data-t-label="common.louder">+</button>
97
+ <span id="volume-value" class="volume-value"></span>
98
+ </div>
99
+ <p class="muted small"><span id="volume-info"></span>
100
+ <button id="volume-limits" class="link admin-only" data-t="now.volume.change"></button></p>
101
+ </div>
102
+ </div>
103
+
104
+ <div id="now-link" class="card notice admin-only" hidden>
105
+ <h2 data-t="now.link.title"></h2>
106
+ <p class="muted" data-t="now.link.text"><code id="now-link-uid" data-part="uid"></code></p>
107
+ <div class="row">
108
+ <select id="now-link-album"></select>
109
+ <button id="now-link-button" class="primary" data-t="now.link.submit"></button>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="card" id="now-tracks-card" hidden>
114
+ <h2 data-t="now.tracks"></h2>
115
+ <ol id="now-tracks" class="tracklist"></ol>
116
+ </div>
117
+ </section>
118
+
119
+ <!-- tags (the web page as rfid reader) -->
120
+ <section id="tab-tags" class="tab" hidden>
121
+ <div class="card">
122
+ <div class="card-head">
123
+ <h2 class="box-only" data-t="tags.title.box"></h2>
124
+ <h2 class="library-only" data-t="tabs.tags"></h2>
125
+ <button id="rfid-remove" class="box-only" data-t="tags.remove"></button>
126
+ </div>
127
+ <p class="muted small box-only" data-t="tags.help.box"></p>
128
+ <p class="muted small library-only" data-t="tags.help.library"></p>
129
+ <div class="row deck-controls">
130
+ <div class="segmented" id="deck-filter" role="group" data-t-label="tags.filter.label">
131
+ <button data-filter="all" data-t="tags.filter.all"></button>
132
+ <button data-filter="album" data-t="tags.filter.album"></button>
133
+ <button data-filter="radio" data-t="tags.filter.radio"></button>
134
+ <button data-filter="spotify" data-t="tags.filter.spotify"></button>
135
+ </div>
136
+ <div class="segmented" id="deck-view" role="group" data-t-label="tags.view.label">
137
+ <button data-view="tiles" data-t="tags.view.tiles"></button>
138
+ <button data-view="list" data-t="tags.view.list"></button>
139
+ </div>
140
+ </div>
141
+ <div id="deck" class="deck"></div>
142
+ <form id="rfid-form" class="row box-only">
143
+ <input name="uid" data-t-placeholder="tags.uid.placeholder" required>
144
+ <button type="submit" data-t="tags.place"></button>
145
+ </form>
146
+ </div>
147
+ </section>
148
+
149
+ <!-- tag management -->
150
+ <section id="tab-tag-management" class="tab" hidden>
151
+ <div class="card">
152
+ <div class="card-head">
153
+ <h2 data-t="tabs.tagman"></h2>
154
+ <label class="check"><input type="checkbox" id="tags-unmapped"> <span data-t="tagman.unlinked_only"></span></label>
155
+ </div>
156
+ <div class="table-wrap">
157
+ <table class="table">
158
+ <thead><tr><th data-t="tagman.column.cover"></th><th data-t="common.name"></th><th data-t="common.uid"></th><th data-t="common.target"></th><th data-t="common.progress"></th><th></th></tr></thead>
159
+ <tbody id="tags-body"></tbody>
160
+ </table>
161
+ </div>
162
+ <p class="muted small" data-t="tagman.help"></p>
163
+ </div>
164
+ <!-- by hand (admin), or with the phone as reader of the library (every user; app.js says where) -->
165
+ <div id="tag-new-card" class="card">
166
+ <div class="card-head">
167
+ <h2 data-t="tagman.new.title"></h2>
168
+ <button id="scan-toggle" hidden data-t="scan.start"></button>
169
+ </div>
170
+ <p id="scan-refused" class="scan notice" hidden></p>
171
+ <div id="scan" class="scan" hidden>
172
+ <p class="muted" data-t="scan.help"></p>
173
+ <div id="scan-result"></div>
174
+ </div>
175
+ <form id="tag-new" class="admin-only">
176
+ <div class="row">
177
+ <input name="uid" data-t-placeholder="tagman.new.uid">
178
+ <input name="name" data-t-placeholder="tagman.new.name">
179
+ <select name="album_id" id="tag-new-album"></select>
180
+ <button type="submit" class="primary" data-t="common.create"></button>
181
+ </div>
182
+ </form>
183
+ </div>
184
+ </section>
185
+
186
+ <!-- albums -->
187
+ <section id="tab-albums" class="tab" hidden>
188
+ <div class="card">
189
+ <div class="card-head">
190
+ <h2 data-t="tabs.albums"></h2>
191
+ <div class="row admin-only">
192
+ <button id="albums-scan" data-t="albums.scan"></button>
193
+ </div>
194
+ </div>
195
+ <div class="row">
196
+ <label class="check"><input type="checkbox" id="albums-unmapped"> <span data-t="albums.without_tag_only"></span></label>
197
+ </div>
198
+ <div class="table-wrap">
199
+ <table class="table albums-table">
200
+ <thead id="albums-head"><tr>
201
+ <th data-t="common.image"></th>
202
+ <th data-sort="title" data-t="albums.column.title"></th>
203
+ <th data-sort="tracks" data-t="albums.column.tracks"></th>
204
+ <th data-sort="length" data-t="albums.column.length"></th>
205
+ <th data-sort="tag" data-t="tabs.tags"></th>
206
+ <th data-sort="created" data-t="albums.column.created"></th>
207
+ <th data-sort="folder" data-t="albums.column.folder"></th>
208
+ </tr></thead>
209
+ <tbody id="albums-list"></tbody>
210
+ </table>
211
+ </div>
212
+ <form id="album-add" class="row admin-only">
213
+ <input name="folder" data-t-placeholder="albums.add.folder" required>
214
+ <input name="title" data-t-placeholder="albums.add.title" maxlength="200" required>
215
+ <button type="submit" data-t="albums.add.submit"></button>
216
+ </form>
217
+ </div>
218
+ </section>
219
+
220
+ <!-- radio stations -->
221
+ <section id="tab-radio" class="tab" hidden>
222
+ <div class="card">
223
+ <h2 data-t="radio.title"></h2>
224
+ <p class="muted small" data-t="radio.help"></p>
225
+ <div id="radio-test" class="notice row admin-only box-only" hidden>
226
+ <span id="radio-test-text" class="grow-text"></span>
227
+ <button id="radio-test-stop" data-t="radio.test.stop"></button>
228
+ </div>
229
+ <div class="table-wrap">
230
+ <table class="table">
231
+ <thead><tr><th data-t="common.image"></th><th data-t="common.name"></th><th data-t="radio.column.url"></th><th data-t="tabs.tags"></th><th></th></tr></thead>
232
+ <tbody id="radios-body"></tbody>
233
+ </table>
234
+ </div>
235
+ </div>
236
+ <form id="radio-new" class="card admin-only">
237
+ <h2 data-t="radio.new.title"></h2>
238
+ <p class="muted small" data-t="radio.new.help"></p>
239
+ <div class="row">
240
+ <input name="name" data-t-placeholder="radio.new.name" maxlength="100" required>
241
+ <input name="url" type="url" data-t-placeholder="radio.new.url" maxlength="2000" required class="grow">
242
+ <button type="button" id="radio-new-test" class="box-only" data-t="radio.test"></button>
243
+ <button type="submit" class="primary" data-t="common.create"></button>
244
+ </div>
245
+ </form>
246
+ </section>
247
+
248
+ <!-- entries of Spotify -->
249
+ <section id="tab-spotify" class="tab" hidden>
250
+ <div class="card">
251
+ <h2 data-t="spotify.title"></h2>
252
+ <p class="muted small" data-t="spotify.help"></p>
253
+ <div id="spotify-test" class="notice row admin-only box-only" hidden>
254
+ <span id="spotify-test-text" class="grow-text"></span>
255
+ <button id="spotify-test-stop" data-t="radio.test.stop"></button>
256
+ </div>
257
+ <div class="table-wrap">
258
+ <table class="table">
259
+ <thead><tr><th data-t="common.image"></th><th data-t="common.name"></th><th data-t="spotify.column.kind"></th><th data-t="spotify.column.link"></th><th data-t="tabs.tags"></th><th></th></tr></thead>
260
+ <tbody id="spotify-body"></tbody>
261
+ </table>
262
+ </div>
263
+ </div>
264
+ <form id="spotify-new" class="card admin-only">
265
+ <h2 data-t="spotify.new.title"></h2>
266
+ <p class="muted small" data-t="spotify.new.help"></p>
267
+ <div class="row">
268
+ <input name="link" data-t-placeholder="spotify.new.link" maxlength="2000" required class="grow">
269
+ <input name="name" data-t-placeholder="spotify.new.name" maxlength="100">
270
+ <button type="button" id="spotify-new-test" class="box-only" data-t="radio.test"></button>
271
+ <button type="submit" class="primary" data-t="common.create"></button>
272
+ </div>
273
+ </form>
274
+ <div class="card admin-only library-only">
275
+ <h2 data-t="spotify.app.title"></h2>
276
+ <p class="muted small" data-t="spotify.app.help"></p>
277
+ <form id="spotify-app-form" class="row">
278
+ <input name="client" autocomplete="off" maxlength="32" data-t-placeholder="spotify.app.client" class="grow">
279
+ <button type="submit" data-t="common.save"></button>
280
+ </form>
281
+ </div>
282
+ <div class="card admin-only box-only">
283
+ <h2 data-t="spotify.account.title"></h2>
284
+ <p class="muted small" data-t="spotify.account.help"></p>
285
+ <p id="spotify-account-state"></p>
286
+ <form id="spotify-key" class="row">
287
+ <input name="key" type="password" autocomplete="off" data-t-placeholder="spotify.account.key" class="grow" required>
288
+ <button type="submit" data-t="spotify.account.key_save"></button>
289
+ </form>
290
+ <div class="row">
291
+ <button id="spotify-connect" class="primary" data-t="spotify.account.connect"></button>
292
+ <button id="spotify-disconnect" data-t="spotify.account.disconnect"></button>
293
+ </div>
294
+ <label class="row"><input type="checkbox" id="spotify-speaker"><span data-t="spotify.account.speaker"></span></label>
295
+ </div>
296
+ <div class="card admin-only box-only">
297
+ <h2 data-t="spotify.player.title"></h2>
298
+ <p id="spotify-player-state"></p>
299
+ <p id="spotify-player-asked" class="muted small"></p>
300
+ <p id="spotify-player-missing" class="muted small" hidden></p>
301
+ <button id="spotify-update" data-t="spotify.player.update"></button>
302
+ </div>
303
+ </section>
304
+
305
+ <!-- bluetooth headset -->
306
+ <section id="tab-bluetooth" class="tab" hidden>
307
+ <div class="card">
308
+ <div class="card-head">
309
+ <h2 data-t="bluetooth.title"></h2>
310
+ <div class="row admin-only">
311
+ <button id="bt-pair" class="primary" data-t="bluetooth.pair"></button>
312
+ <button id="bt-unpair" class="danger" data-t="bluetooth.unpair"></button>
313
+ </div>
314
+ </div>
315
+ <p id="bt-message" class="muted"></p>
316
+ <dl class="facts" id="bt-facts">
317
+ <dt data-t="common.name"></dt><dd id="bt-name"></dd>
318
+ <dt data-t="common.address"></dt><dd id="bt-address"></dd>
319
+ <dt data-t="bluetooth.connected"></dt><dd id="bt-connected"></dd>
320
+ <dt data-t="bluetooth.battery"></dt><dd id="bt-battery"></dd>
321
+ <dt data-t="common.volume"></dt><dd id="bt-volume"></dd>
322
+ </dl>
323
+ <dl class="facts">
324
+ <dt data-t="common.default_volume"></dt><dd id="bt-default"></dd>
325
+ <dt data-t="common.max_volume"></dt><dd id="bt-max"></dd>
326
+ </dl>
327
+ <div class="row admin-only"><button id="bt-limits" data-t="bluetooth.limits"></button></div>
328
+ <p class="muted small" data-t="bluetooth.help"></p>
329
+ </div>
330
+ <div class="card admin-only" id="bt-virtual" hidden>
331
+ <h2 data-t="bluetooth.virtual.title"></h2>
332
+ <p class="muted small" data-t="bluetooth.virtual.help"></p>
333
+ <div class="row">
334
+ <button id="bt-virtual-on" data-t="bluetooth.virtual.on"></button>
335
+ <button id="bt-virtual-off" data-t="bluetooth.virtual.off"></button>
336
+ </div>
337
+ <div class="controls" id="bt-virtual-buttons">
338
+ <button class="round" data-button="prev" data-t-title="common.back" data-t-label="common.back">⏮</button>
339
+ <button class="round" data-button="play" data-t-title="common.play" data-t-label="common.play">▶</button>
340
+ <button class="round" data-button="pause" data-t-title="common.pause" data-t-label="common.pause">⏸</button>
341
+ <button class="round" data-button="next" data-t-title="common.next" data-t-label="common.next">⏭</button>
342
+ <button class="round small-round" data-button="volume-down" data-t-title="common.quieter" data-t-label="common.quieter">−</button>
343
+ <button class="round small-round" data-button="volume-up" data-t-title="common.louder" data-t-label="common.louder">+</button>
344
+ </div>
345
+ </div>
346
+ </section>
347
+
348
+ <!-- upload -->
349
+ <section id="tab-upload" class="tab admin-only" hidden>
350
+ <div class="card notice" id="pending-uploads" hidden>
351
+ <h2 data-t="upload.pending.title"></h2>
352
+ <ul id="pending-list" class="pending-list"></ul>
353
+ </div>
354
+ <div class="card">
355
+ <h2 data-t="upload.title"></h2>
356
+ <p class="muted small" data-t="upload.help"></p>
357
+ <label><span data-t="upload.album_title"></span>
358
+ <input id="upload-name" data-t-placeholder="upload.album_title.placeholder" maxlength="150">
359
+ </label>
360
+ <label><span data-t="upload.cover"></span>
361
+ <input type="file" id="upload-cover" accept="image/jpeg,image/png,image/webp">
362
+ </label>
363
+ <div id="dropzone" class="dropzone" tabindex="0">
364
+ <p data-t="upload.drop"></p>
365
+ <div class="row center">
366
+ <label class="button"><span data-t="upload.choose_files"></span><input type="file" id="upload-files" accept=".mp3,audio/mpeg" multiple hidden></label>
367
+ <label class="button"><span data-t="upload.choose_folder"></span><input type="file" id="upload-folder" webkitdirectory hidden></label>
368
+ </div>
369
+ </div>
370
+ <ol id="upload-list" class="upload-list"></ol>
371
+ <label><span data-t="upload.link_tag"></span>
372
+ <select id="upload-tag"></select>
373
+ </label>
374
+ <div class="row">
375
+ <button id="upload-finish" class="primary" disabled data-t="upload.finish"></button>
376
+ <button id="upload-abort" disabled data-t="common.cancel"></button>
377
+ </div>
378
+ <p id="upload-message" role="status"></p>
379
+ </div>
380
+ </section>
381
+
382
+ <!-- system -->
383
+ <section id="tab-system" class="tab" hidden>
384
+ <div class="card">
385
+ <h2 data-t="tabs.system"></h2>
386
+ <dl class="facts">
387
+ <dt class="box-only" data-t="common.state"></dt><dd id="sys-state" class="box-only"></dd>
388
+ <dt class="box-only" data-t="system.tag"></dt><dd id="sys-tag" class="box-only"></dd>
389
+ <dt data-t="system.user"></dt><dd id="sys-user"></dd>
390
+ <dt data-t="language.label"></dt><dd><select class="language" data-t-label="language.label"></select></dd>
391
+ </dl>
392
+ <div class="row admin-only">
393
+ <button id="sys-sanity" data-t="system.sanity"></button>
394
+ <button id="sys-rebuild" class="danger box-only" data-t="system.rebuild"></button>
395
+ </div>
396
+ <pre id="sys-output" class="output" hidden></pre>
397
+ </div>
398
+ <!-- what the drivers say about buttons, led and rfid reader (app.js: loadHardware) -->
399
+ <div class="card box-only admin-only">
400
+ <h2 data-t="hardware.title"></h2>
401
+ <p class="muted small" data-t="hardware.help"></p>
402
+ <p id="hw-none" class="muted" hidden data-t="hardware.none"></p>
403
+ <div id="hw-view" hidden>
404
+ <h3 data-t="hardware.buttons"></h3>
405
+ <div class="table-wrap">
406
+ <table class="table">
407
+ <thead><tr><th data-t="hardware.column.button"></th><th data-t="hardware.column.pin"></th><th data-t="hardware.column.level"></th><th data-t="hardware.column.last"></th><th data-t="hardware.column.presses"></th><th data-t="hardware.column.driver"></th></tr></thead>
408
+ <tbody id="hw-buttons"></tbody>
409
+ </table>
410
+ </div>
411
+ <h3 data-t="hardware.led"></h3>
412
+ <div id="hw-leds"></div>
413
+ <h3 data-t="hardware.reader"></h3>
414
+ <div id="hw-readers"></div>
415
+ </div>
416
+ </div>
417
+ <div class="card admin-only">
418
+ <h2 data-t="backup.title"></h2>
419
+ <p class="muted small box-only" data-t="backup.help.box"></p>
420
+ <p class="muted small library-only" data-t="backup.help.library"></p>
421
+ <div class="row">
422
+ <a class="button" href="/api/backup" download data-t="backup.download"></a>
423
+ <a class="button" href="/api/backup?albums=1" download data-t="backup.download_albums"></a>
424
+ </div>
425
+ <p class="muted small" data-t="backup.albums_note"></p>
426
+ <!-- putting a backup back (app.js) -->
427
+ <h3 data-t="restore.title"></h3>
428
+ <p class="muted small" data-t="restore.help"></p>
429
+ <div class="row">
430
+ <label class="button" id="restore-choose"><span data-t="restore.choose"></span><input type="file" id="restore-file"
431
+ accept=".gz,.tar,application/gzip,application/x-tar" hidden></label>
432
+ <button type="button" id="restore-confirm" class="danger" data-t="restore.put_back" hidden></button>
433
+ <button type="button" id="restore-discard" data-t="restore.discard" hidden></button>
434
+ </div>
435
+ <p id="restore-state" class="muted small"></p>
436
+ <!-- the backups that the library, or a box without a library, makes by itself: two schedules (app.js) -->
437
+ <div id="backup-kept" hidden>
438
+ <h3 data-t="backup.schedule.title"></h3>
439
+ <form id="backup-form">
440
+ <fieldset>
441
+ <legend data-t="backup.without_albums"></legend>
442
+ <div class="row">
443
+ <label class="check"><input type="checkbox" name="plain-enabled"> <span data-t="backup.schedule.enabled"></span></label>
444
+ <label class="check" data-t="backup.schedule.every"><input type="number" name="plain-every" min="1" max="365" required data-part="every">
445
+ <select name="plain-unit" data-part="unit">
446
+ <option value="days" data-t="backup.unit.days"></option>
447
+ <option value="weeks" data-t="backup.unit.weeks"></option>
448
+ <option value="months" data-t="backup.unit.months"></option>
449
+ </select>
450
+ <input type="time" name="plain-time" required data-part="time"></label>
451
+ <label class="check"><input type="number" name="plain-keep" min="1" max="999" required> <span data-t="backup.schedule.keep"></span></label>
452
+ </div>
453
+ <div class="row">
454
+ <input name="plain-folder" class="grow" data-t-label="backup.folder"
455
+ data-t-title="backup.folder.title">
456
+ </div>
457
+ <p id="backup-state-plain" class="muted small"></p>
458
+ </fieldset>
459
+ <fieldset>
460
+ <legend data-t="backup.with_albums"></legend>
461
+ <div class="row">
462
+ <label class="check"><input type="checkbox" name="albums-enabled"> <span data-t="backup.schedule.enabled"></span></label>
463
+ <label class="check" data-t="backup.schedule.every"><input type="number" name="albums-every" min="1" max="365" required data-part="every">
464
+ <select name="albums-unit" data-part="unit">
465
+ <option value="days" data-t="backup.unit.days"></option>
466
+ <option value="weeks" data-t="backup.unit.weeks"></option>
467
+ <option value="months" data-t="backup.unit.months"></option>
468
+ </select>
469
+ <input type="time" name="albums-time" required data-part="time"></label>
470
+ <label class="check"><input type="number" name="albums-keep" min="1" max="999" required> <span data-t="backup.schedule.keep"></span></label>
471
+ </div>
472
+ <div class="row">
473
+ <input name="albums-folder" class="grow" data-t-label="backup.folder"
474
+ data-t-title="backup.folder.title">
475
+ </div>
476
+ <p id="backup-state-albums" class="muted small"></p>
477
+ </fieldset>
478
+ <div class="row">
479
+ <button type="submit" class="primary" data-t="common.save"></button>
480
+ <button type="button" id="backup-now" data-t="backup.now"></button>
481
+ <button type="button" id="backup-now-albums" data-t="backup.now_albums"></button>
482
+ </div>
483
+ </form>
484
+ <p id="backup-state" class="muted small"></p>
485
+ <div class="table-wrap">
486
+ <table class="table">
487
+ <thead><tr><th data-t="backup.column.when"></th><th data-t="backup.column.content"></th><th data-t="backup.column.size"></th><th></th></tr></thead>
488
+ <tbody id="backups-body"></tbody>
489
+ </table>
490
+ </div>
491
+ </div>
492
+ </div>
493
+ <div class="card box-only">
494
+ <h2 data-t="library.title"></h2>
495
+ <p class="muted small" data-t="library.help"></p>
496
+ <dl class="facts">
497
+ <dt data-t="common.address"></dt><dd id="lib-url"></dd>
498
+ <dt data-t="library.connection"></dt><dd id="lib-state"></dd>
499
+ <dt data-t="library.this_box"></dt>
500
+ <dd><span id="lib-box"></span>
501
+ <button id="lib-rename" type="button" class="link admin-only" data-t="boxes.rename" hidden></button></dd>
502
+ <dt data-t="library.unsettled"></dt><dd id="lib-outbox"></dd>
503
+ </dl>
504
+ <form id="lib-form" class="admin-only">
505
+ <div class="row">
506
+ <input name="url" type="url" data-t-placeholder="library.address.placeholder" data-t-label="library.address" required
507
+ class="grow">
508
+ <input name="bootstrap" type="password" data-t-placeholder="library.bootstrap.placeholder"
509
+ data-t-label="library.bootstrap" autocomplete="off">
510
+ <button type="submit" class="primary" data-t="common.save"></button>
511
+ </div>
512
+ </form>
513
+ <div class="row admin-only" id="lib-unlink-row" hidden>
514
+ <button id="lib-unlink" class="danger" data-t="library.unlink"></button>
515
+ </div>
516
+ </div>
517
+ <div class="card box-only admin-only">
518
+ <h2 data-t="common.version"></h2>
519
+ <p class="muted small" id="ver-help"></p>
520
+ <dl class="facts">
521
+ <dt data-t="version.running"></dt><dd id="ver-running"></dd>
522
+ <dt data-t="version.source"></dt><dd id="ver-source"></dd>
523
+ <dt data-t="version.updates"></dt>
524
+ <dd><select id="ver-mode-select" data-t-label="version.mode.label" hidden></select><span id="ver-mode"></span></dd>
525
+ <dt id="ver-offer-label" hidden data-t="version.new"></dt>
526
+ <dd id="ver-offer" hidden>
527
+ <div id="ver-offer-text"></div>
528
+ <details id="ver-offer-notes" hidden>
529
+ <summary data-t="version.offer.notes"></summary>
530
+ <pre class="output"></pre>
531
+ </details>
532
+ <div class="row">
533
+ <button id="ver-offer-install" class="primary"></button>
534
+ <button id="ver-offer-skip" data-t="version.skip" hidden></button>
535
+ </div>
536
+ </dd>
537
+ <dt id="ver-problem-label" hidden data-t="version.note"></dt><dd id="ver-problem" hidden></dd>
538
+ <dt id="ver-degraded-label" hidden data-t="version.degraded"></dt><dd id="ver-degraded" hidden><ul></ul></dd>
539
+ </dl>
540
+ <div id="ver-controls" hidden>
541
+ <div class="table-wrap">
542
+ <table class="table">
543
+ <thead><tr><th data-t="version.column.slot"></th><th data-t="common.version"></th><th data-t="common.state"></th></tr></thead>
544
+ <tbody id="ver-slots"></tbody>
545
+ </table>
546
+ </div>
547
+ <div class="row">
548
+ <button data-slot-command="check" data-t="version.check"></button>
549
+ <button data-slot-command="update --now" class="primary" data-t="version.update_now"></button>
550
+ </div>
551
+ <div class="row">
552
+ <button data-slot-command="revert" class="danger"
553
+ data-t-confirm="version.revert.confirm" data-t="version.revert"></button>
554
+ <button data-slot-command="fallback" class="danger"
555
+ data-t-confirm="version.fallback.confirm" data-t="version.fallback"></button>
556
+ <button data-slot-command="promote"
557
+ data-t-confirm="version.promote.confirm" data-t="version.promote"></button>
558
+ </div>
559
+ <pre id="ver-output" class="output" hidden></pre>
560
+ </div>
561
+ </div>
562
+ <div class="card">
563
+ <h2 data-t="password.title"></h2>
564
+ <p class="muted small" data-t="password.help"></p>
565
+ <form id="own-password">
566
+ <input name="username" autocomplete="username" hidden>
567
+ <div class="row">
568
+ <input name="current" type="password" data-t-placeholder="password.current" data-t-label="password.current"
569
+ autocomplete="current-password" required>
570
+ <input name="password" type="password" data-t-placeholder="password.new" data-t-label="password.new"
571
+ minlength="8" autocomplete="new-password" required>
572
+ <input name="repeat" type="password" data-t-placeholder="password.repeat"
573
+ data-t-label="password.repeat" minlength="8" autocomplete="new-password" required>
574
+ <button type="submit" class="primary" data-t="password.submit"></button>
575
+ </div>
576
+ </form>
577
+ </div>
578
+ <div class="card admin-only">
579
+ <h2 data-t="users.title"></h2>
580
+ <p class="muted small" data-t="users.help"></p>
581
+ <div class="table-wrap">
582
+ <table class="table">
583
+ <thead><tr><th data-t="common.name"></th><th data-t="users.role"></th><th data-t="users.set_password"></th><th></th></tr></thead>
584
+ <tbody id="users-body"></tbody>
585
+ </table>
586
+ </div>
587
+ <form id="user-new">
588
+ <h3 data-t="users.new.title"></h3>
589
+ <div class="row">
590
+ <input name="username" data-t-placeholder="common.name" maxlength="64" autocomplete="off" required>
591
+ <input name="password" type="password" data-t-placeholder="users.new.password" minlength="8"
592
+ autocomplete="new-password" required>
593
+ <select name="role" data-t-label="users.role">
594
+ <option value="user" selected data-t="role.user"></option><option value="admin" data-t="role.admin"></option>
595
+ </select>
596
+ <button type="submit" class="primary" data-t="common.create"></button>
597
+ </div>
598
+ </form>
599
+ </div>
600
+ <div class="card box-only">
601
+ <h2 data-t="progress.title"></h2>
602
+ <div class="table-wrap">
603
+ <table class="table">
604
+ <thead><tr><th data-t="progress.column.tag"></th><th data-t="common.album"></th><th data-t="common.track"></th><th data-t="progress.column.position"></th><th data-t="progress.column.finished"></th></tr></thead>
605
+ <tbody id="activities-body"></tbody>
606
+ </table>
607
+ </div>
608
+ </div>
609
+ </section>
610
+ <!-- library: boxes -->
611
+ <section id="tab-boxes" class="tab library-only" hidden>
612
+ <div class="card">
613
+ <h2 data-t="tabs.boxes"></h2>
614
+ <p class="muted small" data-t="boxes.help"></p>
615
+ <div class="table-wrap">
616
+ <table class="table">
617
+ <thead><tr><th data-t="common.name"></th><th data-t="common.version"></th><th data-t="boxes.column.contact"></th><th data-t="boxes.column.state"></th><th></th></tr></thead>
618
+ <tbody id="boxes-body"></tbody>
619
+ </table>
620
+ </div>
621
+ </div>
622
+ <div class="card admin-only" id="install-card">
623
+ <h2 data-t="boxes.add.title"></h2>
624
+ <p class="muted small" id="install-help"></p>
625
+ <div class="row" id="install-row">
626
+ <code id="install-line" class="grow selectable"></code>
627
+ <button id="install-copy" data-t="boxes.add.copy"></button>
628
+ </div>
629
+ </div>
630
+ <div class="card admin-only" id="phone-card">
631
+ <h2 data-t="phones.add.title"></h2>
632
+ <p class="muted small" id="phone-app-help"></p>
633
+ <div class="row" id="phone-app-row" hidden>
634
+ <a id="phone-app-link" href="/app.apk" download class="grow"></a>
635
+ <div id="phone-app-qr" class="qr"></div>
636
+ </div>
637
+ <p class="muted small" data-t="phones.pair.help"></p>
638
+ <div class="row"><button id="phone-pair" class="primary" data-t="phones.pair"></button></div>
639
+ <div id="phone-pair-qr" class="qr" hidden></div>
640
+ <p id="phone-pair-state" class="muted small" hidden></p>
641
+ </div>
642
+ <div class="card" id="uploads-card" hidden>
643
+ <h2 data-t="boxes.uploads.title"></h2>
644
+ <p class="muted small" data-t="boxes.uploads.help"></p>
645
+ <ul id="uploads-list"></ul>
646
+ </div>
647
+ <div class="card admin-only">
648
+ <h2 data-t="access.title"></h2>
649
+ <p class="muted small" data-t="access.help"></p>
650
+ <form id="bootstrap-form">
651
+ <div class="row">
652
+ <input name="password" type="password" data-t-placeholder="access.bootstrap.placeholder"
653
+ minlength="16" autocomplete="off" required class="grow">
654
+ <button type="submit" data-t="access.bootstrap.submit"></button>
655
+ </div>
656
+ </form>
657
+ <div class="row">
658
+ <button id="token-new" data-t="access.token.new"></button>
659
+ <code id="token-value" hidden></code>
660
+ </div>
661
+ <div class="row"><button id="library-verify" data-t="access.verify"></button></div>
662
+ </div>
663
+ </section>
664
+ <!-- library: versions of the box software -->
665
+ <section id="tab-versions" class="tab library-only" hidden>
666
+ <div class="card">
667
+ <div class="card-head">
668
+ <h2 data-t="releases.title"></h2>
669
+ <button id="releases-check" class="admin-only" data-t="releases.check"></button>
670
+ </div>
671
+ <p class="muted small" data-t="releases.help"></p>
672
+ <p id="releases-state" class="muted small"></p>
673
+ <div class="row">
674
+ <label class="check"><input type="checkbox" id="versions-hide-old" checked> <span data-t="releases.hide_old"></span></label>
675
+ <span id="versions-hidden" class="muted small"></span>
676
+ </div>
677
+ <div class="table-wrap">
678
+ <table class="table">
679
+ <thead><tr><th data-t="common.version"></th><th data-t="common.state"></th><th data-t="releases.column.set"></th><th data-t="releases.column.runs_on"></th><th data-t="releases.column.prepared_on"></th><th></th></tr></thead>
680
+ <tbody id="versions-body"></tbody>
681
+ </table>
682
+ </div>
683
+ <form id="releases-keep-form" class="admin-only">
684
+ <div class="row">
685
+ <label class="check" data-t="releases.keep.label"><input type="number" name="versions" min="1" max="999" required data-part="versions"></label>
686
+ <button type="submit" data-t="common.save"></button>
687
+ </div>
688
+ <p class="muted small" data-t="releases.keep.help"></p>
689
+ </form>
690
+ </div>
691
+ <div class="card admin-only">
692
+ <h2 data-t="releases.access.title"></h2>
693
+ <p id="releases-token-state" class="muted small"></p>
694
+ <form id="releases-token-form">
695
+ <div class="row">
696
+ <input name="token" type="password" data-t-placeholder="releases.token.placeholder" autocomplete="off"
697
+ required class="grow">
698
+ <button type="submit" data-t="releases.token.submit"></button>
699
+ </div>
700
+ </form>
701
+ </div>
702
+ </section>
703
+ <!-- play statistics: of one box on its page; in the library of all boxes or of the one that is chosen -->
704
+ <section id="tab-statistics" class="tab" hidden>
705
+ <div class="card">
706
+ <div class="card-head">
707
+ <h2 data-t="statistics.title"></h2>
708
+ <select id="stats-box" data-t-label="statistics.box.label" hidden></select>
709
+ </div>
710
+ <p id="stats-help" class="muted small"></p>
711
+ <div id="stats-tags-wrap" class="table-wrap">
712
+ <table class="table">
713
+ <thead id="stats-tags-head"><tr><th data-sort="name" data-t="statistics.column.tag"></th><th data-sort="album" data-t="common.target"></th><th data-sort="plays" data-t="statistics.column.placed"></th><th data-sort="seconds" data-t="statistics.column.time"></th><th data-sort="finished" data-t="statistics.column.finished"></th><th data-sort="last" data-t="statistics.column.last"></th><th class="stats-several" data-t="statistics.column.boxes"></th></tr></thead>
714
+ <tbody id="stats-tags"></tbody>
715
+ </table>
716
+ </div>
717
+ </div>
718
+ <div id="stats-rest">
719
+ <div id="stats-boxes-card" class="card" hidden>
720
+ <h2 data-t="tabs.boxes"></h2>
721
+ <div class="table-wrap">
722
+ <table class="table">
723
+ <thead><tr><th data-t="statistics.column.box"></th><th data-t="statistics.column.time"></th><th data-t="statistics.column.placed"></th><th data-t="statistics.column.last"></th></tr></thead>
724
+ <tbody id="stats-boxes"></tbody>
725
+ </table>
726
+ </div>
727
+ </div>
728
+ <div class="card">
729
+ <h2 data-t="statistics.days.title"></h2>
730
+ <div class="table-wrap">
731
+ <table class="table">
732
+ <thead><tr><th data-t="statistics.column.day"></th><th class="stats-several" data-t="statistics.column.box"></th><th data-t="statistics.column.time"></th><th data-t="output.speaker"></th><th data-t="output.headphones"></th><th data-t="statistics.column.hours"></th></tr></thead>
733
+ <tbody id="stats-days"></tbody>
734
+ </table>
735
+ </div>
736
+ <p id="stats-unknown" class="muted small"></p>
737
+ </div>
738
+ <div class="card">
739
+ <h2 data-t="statistics.radio_skipped.title"></h2>
740
+ <div class="table-wrap">
741
+ <table class="table">
742
+ <thead><tr><th data-t="statistics.column.station"></th><th data-t="statistics.column.time"></th></tr></thead>
743
+ <tbody id="stats-stations"></tbody>
744
+ </table>
745
+ </div>
746
+ <div class="table-wrap">
747
+ <table class="table">
748
+ <thead><tr><th data-t="common.album"></th><th data-t="common.track"></th><th data-t="statistics.column.skipped"></th></tr></thead>
749
+ <tbody id="stats-skipped"></tbody>
750
+ </table>
751
+ </div>
752
+ </div>
753
+ <div class="card">
754
+ <h2 data-t="statistics.operation.title"></h2>
755
+ <div class="table-wrap">
756
+ <table class="table">
757
+ <thead><tr><th data-t="common.version"></th><th data-t="statistics.column.starts"></th><th data-t="statistics.column.errors"></th><th data-t="statistics.column.fallbacks"></th><th data-t="statistics.column.installed"></th><th data-t="statistics.column.running"></th><th data-t="statistics.column.active"></th></tr></thead>
758
+ <tbody id="stats-versions"></tbody>
759
+ </table>
760
+ </div>
761
+ <p class="muted small" data-t="statistics.operation.help"></p>
762
+ </div>
763
+ <div id="stats-never-card" class="card" hidden>
764
+ <h2 data-t="statistics.never.title"></h2>
765
+ <p id="stats-never"></p>
766
+ </div>
767
+ </div>
768
+ </section>
769
+ <!-- log -->
770
+ <section id="tab-log" class="tab admin-only" hidden>
771
+ <div class="card">
772
+ <div class="card-head">
773
+ <h2 data-t="tabs.log"></h2>
774
+ <div class="row">
775
+ <select id="log-source" class="library-only" data-t-label="log.source"></select>
776
+ <select id="log-level" data-t-label="log.level">
777
+ <option value="DEBUG" data-t="log.level.debug"></option><option value="INFO" selected data-t="log.level.info"></option>
778
+ <option value="WARNING" data-t="log.level.warning"></option><option value="ERROR" data-t="log.level.error"></option>
779
+ </select>
780
+ <input id="log-text" data-t-placeholder="log.filter" data-t-label="log.filter">
781
+ <label class="check" id="log-follow-label"><input type="checkbox" id="log-follow" checked> <span data-t="log.live"></span></label>
782
+ <button id="log-download" data-t="log.export"></button>
783
+ </div>
784
+ </div>
785
+ <!-- a box chosen on the page of the library: what it delivered of its log (library.md, section 4.22) -->
786
+ <div id="log-box" class="row" hidden>
787
+ <select id="log-component" data-t-label="log.component"></select>
788
+ <select id="log-version" data-t-label="common.version"></select>
789
+ <select id="log-code" data-t-label="log.code"></select>
790
+ <label><span data-t="log.since"></span> <input type="datetime-local" id="log-since"></label>
791
+ <button id="log-copy" data-t="log.copy"></button>
792
+ <button id="log-export" data-t="log.export"></button>
793
+ </div>
794
+ <div id="log-startup" class="notice" hidden><p id="log-startup-text"></p><pre class="output" id="log-startup-error"></pre></div>
795
+ <div id="log-list" class="log-list" role="log"></div>
796
+ </div>
797
+ </section>
798
+ </main>
799
+ </div>
800
+
801
+ <dialog id="album-dialog">
802
+ <form method="dialog" class="dialog-close"><button data-t-label="common.close">✕</button></form>
803
+ <div class="dialog-head">
804
+ <div class="cover cover-small" id="dlg-cover"></div>
805
+ <div>
806
+ <h2 id="dlg-title"></h2>
807
+ <p id="dlg-meta" class="muted small"></p>
808
+ <div id="dlg-tags" class="tag-chips"></div>
809
+ <label class="button small admin-only"><span data-t="cover.change"></span><input type="file" id="dlg-cover-file" accept="image/jpeg,image/png,image/webp" hidden></label>
810
+ </div>
811
+ </div>
812
+ <div id="dlg-problems" class="notice problems" hidden>
813
+ <strong data-t="album.problems"></strong>
814
+ <ul id="dlg-problem-list"></ul>
815
+ <button id="dlg-playlist" class="admin-only" data-t="album.playlist.rebuild"></button>
816
+ </div>
817
+ <div id="dlg-broken" class="notice problems" hidden>
818
+ <strong data-t="album.broken"></strong>
819
+ <ul id="dlg-broken-list"></ul>
820
+ <button id="dlg-broken-forget" class="admin-only" data-t="album.broken.forget"></button>
821
+ </div>
822
+ <ol id="dlg-tracks" class="tracklist"></ol>
823
+ <div class="admin-only dialog-actions">
824
+ <label class="check"><input type="checkbox" id="dlg-delete-files"> <span data-t="album.delete_files"></span></label>
825
+ <button id="dlg-delete" class="danger" data-t="album.remove"></button>
826
+ </div>
827
+ </dialog>
828
+
829
+ <dialog id="limits-dialog">
830
+ <form method="dialog" class="dialog-close"><button data-t-label="common.close">✕</button></form>
831
+ <h2 data-t="limits.title"></h2>
832
+ <p class="muted small" data-t="limits.help"></p>
833
+ <form id="limits-form">
834
+ <fieldset>
835
+ <legend data-t="output.speaker"></legend>
836
+ <div class="row">
837
+ <label><span data-t="limits.default"></span> <input type="number" name="speaker_default" min="0" max="100" step="1" required></label>
838
+ <label><span data-t="limits.max"></span> <input type="number" name="speaker_max" min="0" max="100" step="1" required></label>
839
+ </div>
840
+ </fieldset>
841
+ <fieldset>
842
+ <legend data-t="output.headphones"></legend>
843
+ <div class="row">
844
+ <label><span data-t="limits.default"></span> <input type="number" name="headphones_default" min="0" max="100" step="1" required></label>
845
+ <label><span data-t="limits.max"></span> <input type="number" name="headphones_max" min="0" max="100" step="1" required></label>
846
+ </div>
847
+ </fieldset>
848
+ <div class="dialog-actions"><span></span><button type="submit" class="primary" data-t="common.save"></button></div>
849
+ </form>
850
+ </dialog>
851
+
852
+ <dialog id="phone-limits-dialog">
853
+ <form method="dialog" class="dialog-close"><button data-t-label="common.close">✕</button></form>
854
+ <h2 data-t="phones.limits.title"></h2>
855
+ <p class="muted small" data-t="phones.limits.help"></p>
856
+ <form id="phone-limits-form">
857
+ <div class="row">
858
+ <label><span data-t="output.speaker"></span> <input type="number" name="speaker" min="0" max="100" step="1" required></label>
859
+ <label><span data-t="output.headphones"></span> <input type="number" name="headphones" min="0" max="100" step="1" required></label>
860
+ </div>
861
+ <div class="dialog-actions"><span></span><button type="submit" class="primary" data-t="common.save"></button></div>
862
+ </form>
863
+ </dialog>
864
+
865
+ <div id="toast" class="toast" role="status" hidden></div>
866
+ </body>
867
+ </html>