@rmdes/indiekit-endpoint-funkwhale 1.0.3 → 1.0.4

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.js CHANGED
@@ -45,10 +45,6 @@ export default class FunkwhaleEndpoint {
45
45
  return path.join(__dirname, "locales");
46
46
  }
47
47
 
48
- get styles() {
49
- return path.join(__dirname, "assets", "styles.css");
50
- }
51
-
52
48
  get navigationItems() {
53
49
  return {
54
50
  href: this.options.mountPath,
@@ -68,17 +64,12 @@ export default class FunkwhaleEndpoint {
68
64
 
69
65
  /**
70
66
  * Protected routes (require authentication)
71
- * HTML pages for admin dashboard
67
+ * Admin dashboard only - detailed views are on the public frontend
72
68
  */
73
69
  get routes() {
74
- // Dashboard
70
+ // Dashboard overview
75
71
  protectedRouter.get("/", dashboardController.get);
76
72
 
77
- // Individual sections (HTML pages)
78
- protectedRouter.get("/listenings", listeningsController.get);
79
- protectedRouter.get("/favorites", favoritesController.get);
80
- protectedRouter.get("/stats", statsController.get);
81
-
82
73
  // Manual sync trigger
83
74
  protectedRouter.post("/sync", dashboardController.sync);
84
75
 
@@ -87,13 +78,10 @@ export default class FunkwhaleEndpoint {
87
78
 
88
79
  /**
89
80
  * Public routes (no authentication required)
90
- * JSON API endpoints for Eleventy widgets
81
+ * JSON API endpoints for Eleventy frontend
91
82
  */
92
83
  get routesPublic() {
93
- // Now playing widget
94
84
  publicRouter.get("/api/now-playing", nowPlayingController.api);
95
-
96
- // JSON API for widgets
97
85
  publicRouter.get("/api/listenings", listeningsController.api);
98
86
  publicRouter.get("/api/favorites", favoritesController.api);
99
87
  publicRouter.get("/api/stats", statsController.api);
@@ -1,6 +1,5 @@
1
1
  import { FunkwhaleClient } from "../funkwhale-client.js";
2
- import { runSync } from "../sync.js";
3
- import { getSummary } from "../stats.js";
2
+ import { runSync, getCachedStats } from "../sync.js";
4
3
  import * as utils from "../utils.js";
5
4
 
6
5
  /**
@@ -13,7 +12,7 @@ export const dashboardController = {
13
12
  */
14
13
  async get(request, response, next) {
15
14
  try {
16
- const { funkwhaleConfig } = request.app.locals.application;
15
+ const { funkwhaleConfig, funkwhaleEndpoint } = request.app.locals.application;
17
16
 
18
17
  if (!funkwhaleConfig) {
19
18
  return response.status(500).render("funkwhale", {
@@ -64,16 +63,13 @@ export const dashboardController = {
64
63
  });
65
64
  }
66
65
 
67
- // Get stats summary from database if available
68
- let summary = null;
69
- const db = request.app.locals.database;
70
- if (db) {
71
- try {
72
- summary = await getSummary(db, "all");
73
- } catch (dbError) {
74
- console.error("[Funkwhale] DB error:", dbError.message);
75
- }
76
- }
66
+ // Get stats from cache (same source as public API)
67
+ const cachedStats = getCachedStats();
68
+ const summary = cachedStats?.summary?.all || null;
69
+
70
+ // Determine public frontend URL (strip 'api' from mount path)
71
+ // e.g., /funkwhaleapi -> /funkwhale
72
+ const publicUrl = funkwhaleEndpoint ? funkwhaleEndpoint.replace(/api$/, "") : "/funkwhale";
77
73
 
78
74
  response.render("funkwhale", {
79
75
  title: response.locals.__("funkwhale.title"),
@@ -81,6 +77,7 @@ export const dashboardController = {
81
77
  listenings: listenings.slice(0, 5),
82
78
  favorites: favorites.slice(0, 5),
83
79
  summary,
80
+ publicUrl,
84
81
  mountPath: request.baseUrl,
85
82
  });
86
83
  } catch (error) {
package/locales/en.json CHANGED
@@ -30,8 +30,8 @@
30
30
  "noConfig": "Funkwhale endpoint not configured correctly"
31
31
  },
32
32
  "widget": {
33
- "description": "View your Funkwhale listening activity",
34
- "view": "View Activity"
33
+ "description": "View full listening activity on the public page",
34
+ "view": "View Public Page"
35
35
  }
36
36
  }
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-funkwhale",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Funkwhale listening activity endpoint for Indiekit. Display listening history, favorites, and statistics.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -34,7 +34,6 @@
34
34
  ".": "./index.js"
35
35
  },
36
36
  "files": [
37
- "assets",
38
37
  "includes",
39
38
  "lib",
40
39
  "locales",
@@ -137,6 +137,21 @@
137
137
  text-overflow: ellipsis;
138
138
  }
139
139
  .fw-list-title:hover { text-decoration: underline; }
140
+
141
+ /* Public link banner */
142
+ .fw-public-link {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: space-between;
146
+ padding: 1rem;
147
+ background: var(--color-offset, #f5f5f5);
148
+ border-radius: 0.5rem;
149
+ margin-top: 2rem;
150
+ }
151
+ .fw-public-link p {
152
+ margin: 0;
153
+ color: var(--color-text-secondary, #666);
154
+ }
140
155
  </style>
141
156
 
142
157
  {% if error %}
@@ -195,11 +210,6 @@
195
210
  <span class="fw-stat-label">{{ __("funkwhale.artists") }}</span>
196
211
  </div>
197
212
  </div>
198
- {{ button({
199
- classes: "button--secondary",
200
- href: mountPath + "/stats",
201
- text: __("funkwhale.viewStats")
202
- }) }}
203
213
  </section>
204
214
  {% endif %}
205
215
 
@@ -229,11 +239,6 @@
229
239
  </li>
230
240
  {% endfor %}
231
241
  </ul>
232
- {{ button({
233
- classes: "button--secondary",
234
- href: mountPath + "/listenings",
235
- text: __("funkwhale.viewAll")
236
- }) }}
237
242
  {% else %}
238
243
  {{ prose({ text: __("funkwhale.noRecentPlays") }) }}
239
244
  {% endif %}
@@ -262,14 +267,19 @@
262
267
  </li>
263
268
  {% endfor %}
264
269
  </ul>
265
- {{ button({
266
- classes: "button--secondary",
267
- href: mountPath + "/favorites",
268
- text: __("funkwhale.viewAll")
269
- }) }}
270
270
  {% else %}
271
271
  {{ prose({ text: __("funkwhale.noFavorites") }) }}
272
272
  {% endif %}
273
273
  </section>
274
+
275
+ {# Link to public page #}
276
+ <div class="fw-public-link">
277
+ <p>{{ __("funkwhale.widget.description") }}</p>
278
+ {{ button({
279
+ href: publicUrl,
280
+ text: __("funkwhale.widget.view"),
281
+ target: "_blank"
282
+ }) }}
283
+ </div>
274
284
  {% endif %}
275
285
  {% endblock %}
package/assets/styles.css DELETED
@@ -1,453 +0,0 @@
1
- /* Funkwhale Endpoint Styles */
2
-
3
- /* Sections */
4
- .funkwhale-section {
5
- margin-block-end: 2rem;
6
- }
7
-
8
- .funkwhale-section h2 {
9
- margin-block-end: 1rem;
10
- }
11
-
12
- /* Now Playing Animation */
13
- .funkwhale-status {
14
- display: inline-flex;
15
- align-items: center;
16
- gap: 0.5rem;
17
- }
18
-
19
- .funkwhale-status--playing {
20
- color: var(--color-accent, #22c55e);
21
- }
22
-
23
- .funkwhale-bars {
24
- display: flex;
25
- gap: 2px;
26
- align-items: flex-end;
27
- height: 16px;
28
- }
29
-
30
- .funkwhale-bars span {
31
- width: 3px;
32
- background: currentColor;
33
- animation: funkwhale-bar 0.5s infinite ease-in-out alternate;
34
- }
35
-
36
- .funkwhale-bars span:nth-child(2) {
37
- animation-delay: 0.2s;
38
- }
39
-
40
- .funkwhale-bars span:nth-child(3) {
41
- animation-delay: 0.4s;
42
- }
43
-
44
- @keyframes funkwhale-bar {
45
- from { height: 4px; }
46
- to { height: 16px; }
47
- }
48
-
49
- /* Featured Track */
50
- .funkwhale-track--featured {
51
- display: flex;
52
- gap: 1rem;
53
- padding: 1rem;
54
- background: var(--color-offset, #f5f5f5);
55
- border-radius: 0.5rem;
56
- }
57
-
58
- .funkwhale-track__cover {
59
- width: 80px;
60
- height: 80px;
61
- object-fit: cover;
62
- border-radius: 0.25rem;
63
- }
64
-
65
- .funkwhale-track__cover--placeholder {
66
- background: var(--color-border, #ddd);
67
- }
68
-
69
- .funkwhale-track__info {
70
- flex: 1;
71
- display: flex;
72
- flex-direction: column;
73
- gap: 0.25rem;
74
- }
75
-
76
- .funkwhale-track__title {
77
- font-weight: 600;
78
- text-decoration: none;
79
- color: var(--color-text, inherit);
80
- }
81
-
82
- .funkwhale-track__title:hover {
83
- text-decoration: underline;
84
- }
85
-
86
- .funkwhale-track__album {
87
- margin: 0;
88
- color: var(--color-text-muted, #666);
89
- font-size: 0.875rem;
90
- }
91
-
92
- /* Stats Grid */
93
- .funkwhale-stats-grid {
94
- display: grid;
95
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
96
- gap: 1rem;
97
- margin-block-end: 1rem;
98
- }
99
-
100
- .funkwhale-stat {
101
- display: flex;
102
- flex-direction: column;
103
- align-items: center;
104
- padding: 1rem;
105
- background: var(--color-offset, #f5f5f5);
106
- border-radius: 0.5rem;
107
- text-align: center;
108
- }
109
-
110
- .funkwhale-stat__value {
111
- font-size: 1.5rem;
112
- font-weight: 700;
113
- color: var(--color-accent, #3b82f6);
114
- }
115
-
116
- .funkwhale-stat__label {
117
- font-size: 0.75rem;
118
- color: var(--color-text-muted, #666);
119
- text-transform: uppercase;
120
- letter-spacing: 0.05em;
121
- }
122
-
123
- /* List */
124
- .funkwhale-list {
125
- list-style: none;
126
- padding: 0;
127
- margin: 0;
128
- }
129
-
130
- .funkwhale-list__item {
131
- display: flex;
132
- align-items: center;
133
- gap: 0.75rem;
134
- padding: 0.75rem 0;
135
- border-bottom: 1px solid var(--color-border, #eee);
136
- }
137
-
138
- .funkwhale-list__item:last-child {
139
- border-bottom: none;
140
- }
141
-
142
- .funkwhale-list__cover {
143
- width: 48px;
144
- height: 48px;
145
- object-fit: cover;
146
- border-radius: 0.25rem;
147
- flex-shrink: 0;
148
- }
149
-
150
- .funkwhale-list__cover--placeholder {
151
- background: var(--color-border, #ddd);
152
- }
153
-
154
- .funkwhale-list__info {
155
- flex: 1;
156
- min-width: 0;
157
- }
158
-
159
- .funkwhale-list__title {
160
- display: block;
161
- font-weight: 500;
162
- text-decoration: none;
163
- color: var(--color-text, inherit);
164
- white-space: nowrap;
165
- overflow: hidden;
166
- text-overflow: ellipsis;
167
- }
168
-
169
- .funkwhale-list__title:hover {
170
- text-decoration: underline;
171
- }
172
-
173
- .funkwhale-list__album {
174
- margin: 0;
175
- font-size: 0.875rem;
176
- color: var(--color-text-muted, #666);
177
- }
178
-
179
- .funkwhale-meta {
180
- display: flex;
181
- gap: 0.5rem;
182
- color: var(--color-text-muted, #666);
183
- font-size: 0.75rem;
184
- }
185
-
186
- /* Pagination */
187
- .funkwhale-pagination {
188
- display: flex;
189
- align-items: center;
190
- justify-content: center;
191
- gap: 1rem;
192
- margin-block: 1.5rem;
193
- }
194
-
195
- .funkwhale-pagination__info {
196
- color: var(--color-text-muted, #666);
197
- }
198
-
199
- /* Tabs */
200
- .funkwhale-tabs {
201
- display: flex;
202
- gap: 0.25rem;
203
- margin-block-end: 1.5rem;
204
- border-bottom: 1px solid var(--color-border, #ddd);
205
- }
206
-
207
- .funkwhale-tab {
208
- padding: 0.75rem 1rem;
209
- border: none;
210
- background: none;
211
- color: var(--color-text-muted, #666);
212
- cursor: pointer;
213
- border-bottom: 2px solid transparent;
214
- margin-bottom: -1px;
215
- font-size: 0.875rem;
216
- font-weight: 500;
217
- }
218
-
219
- .funkwhale-tab:hover {
220
- color: var(--color-text, inherit);
221
- }
222
-
223
- .funkwhale-tab--active {
224
- color: var(--color-accent, #3b82f6);
225
- border-bottom-color: var(--color-accent, #3b82f6);
226
- }
227
-
228
- .funkwhale-tab-content {
229
- display: none;
230
- }
231
-
232
- .funkwhale-tab-content--active {
233
- display: block;
234
- }
235
-
236
- /* Top List */
237
- .funkwhale-top-list {
238
- list-style: none;
239
- padding: 0;
240
- margin: 0 0 1.5rem 0;
241
- counter-reset: rank;
242
- }
243
-
244
- .funkwhale-top-list__item {
245
- display: flex;
246
- align-items: center;
247
- gap: 0.75rem;
248
- padding: 0.5rem 0;
249
- border-bottom: 1px solid var(--color-border, #eee);
250
- }
251
-
252
- .funkwhale-top-list__rank {
253
- width: 1.5rem;
254
- height: 1.5rem;
255
- display: flex;
256
- align-items: center;
257
- justify-content: center;
258
- font-size: 0.75rem;
259
- font-weight: 600;
260
- background: var(--color-offset, #f5f5f5);
261
- border-radius: 50%;
262
- color: var(--color-text-muted, #666);
263
- }
264
-
265
- .funkwhale-top-list__name {
266
- flex: 1;
267
- font-weight: 500;
268
- }
269
-
270
- .funkwhale-top-list__count {
271
- font-size: 0.875rem;
272
- color: var(--color-text-muted, #666);
273
- }
274
-
275
- /* Album Grid */
276
- .funkwhale-album-grid {
277
- display: grid;
278
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
279
- gap: 1rem;
280
- list-style: none;
281
- padding: 0;
282
- margin: 0 0 1.5rem 0;
283
- }
284
-
285
- .funkwhale-album {
286
- display: flex;
287
- flex-direction: column;
288
- gap: 0.5rem;
289
- }
290
-
291
- .funkwhale-album__cover {
292
- width: 100%;
293
- aspect-ratio: 1;
294
- object-fit: cover;
295
- border-radius: 0.25rem;
296
- }
297
-
298
- .funkwhale-album__cover--placeholder {
299
- background: var(--color-border, #ddd);
300
- }
301
-
302
- .funkwhale-album__info {
303
- display: flex;
304
- flex-direction: column;
305
- gap: 0.125rem;
306
- }
307
-
308
- .funkwhale-album__title {
309
- font-weight: 500;
310
- font-size: 0.875rem;
311
- white-space: nowrap;
312
- overflow: hidden;
313
- text-overflow: ellipsis;
314
- }
315
-
316
- .funkwhale-album__artist {
317
- font-size: 0.75rem;
318
- color: var(--color-text-muted, #666);
319
- white-space: nowrap;
320
- overflow: hidden;
321
- text-overflow: ellipsis;
322
- }
323
-
324
- /* Trend Chart */
325
- .funkwhale-chart {
326
- margin: 1rem 0;
327
- }
328
-
329
- .funkwhale-chart__bars {
330
- display: flex;
331
- align-items: flex-end;
332
- gap: 2px;
333
- height: 120px;
334
- padding: 0.5rem 0;
335
- }
336
-
337
- .funkwhale-chart__bar-wrapper {
338
- flex: 1;
339
- height: 100%;
340
- display: flex;
341
- align-items: flex-end;
342
- }
343
-
344
- .funkwhale-chart__bar {
345
- width: 100%;
346
- background: var(--color-accent, #3b82f6);
347
- border-radius: 2px 2px 0 0;
348
- min-height: 2px;
349
- transition: height 0.3s ease;
350
- }
351
-
352
- .funkwhale-chart__bar-wrapper:hover .funkwhale-chart__bar {
353
- background: var(--color-accent-hover, #2563eb);
354
- }
355
-
356
- .funkwhale-chart__labels {
357
- display: flex;
358
- justify-content: space-between;
359
- font-size: 0.75rem;
360
- color: var(--color-text-muted, #666);
361
- margin-top: 0.5rem;
362
- }
363
-
364
- /* Widget Styles (for embeddable widgets) */
365
- .funkwhale-widget {
366
- display: flex;
367
- flex-wrap: wrap;
368
- align-items: center;
369
- gap: 0.5rem;
370
- padding: 0.75rem;
371
- background: var(--color-offset, #f5f5f5);
372
- border-radius: 0.5rem;
373
- }
374
-
375
- .funkwhale-widget--playing {
376
- background: linear-gradient(135deg, #22c55e10, #22c55e05);
377
- border: 1px solid #22c55e30;
378
- }
379
-
380
- .funkwhale-widget__status {
381
- font-size: 0.625rem;
382
- text-transform: uppercase;
383
- letter-spacing: 0.1em;
384
- color: var(--color-text-muted, #666);
385
- width: 100%;
386
- }
387
-
388
- .funkwhale-widget--playing .funkwhale-widget__status {
389
- color: #22c55e;
390
- }
391
-
392
- .funkwhale-widget__cover {
393
- width: 48px;
394
- height: 48px;
395
- object-fit: cover;
396
- border-radius: 0.25rem;
397
- }
398
-
399
- .funkwhale-widget__info {
400
- flex: 1;
401
- min-width: 0;
402
- }
403
-
404
- .funkwhale-widget__title {
405
- display: block;
406
- font-weight: 500;
407
- font-size: 0.875rem;
408
- text-decoration: none;
409
- color: var(--color-text, inherit);
410
- white-space: nowrap;
411
- overflow: hidden;
412
- text-overflow: ellipsis;
413
- }
414
-
415
- .funkwhale-widget__time {
416
- font-size: 0.75rem;
417
- color: var(--color-text-muted, #666);
418
- }
419
-
420
- .funkwhale-widget__empty,
421
- .funkwhale-widget__error {
422
- font-size: 0.875rem;
423
- color: var(--color-text-muted, #666);
424
- margin: 0;
425
- }
426
-
427
- /* Stats Widget */
428
- .funkwhale-stats-widget__grid {
429
- display: grid;
430
- grid-template-columns: repeat(3, 1fr);
431
- gap: 0.5rem;
432
- }
433
-
434
- .funkwhale-stats-widget__stat {
435
- display: flex;
436
- flex-direction: column;
437
- align-items: center;
438
- padding: 0.5rem;
439
- background: var(--color-offset, #f5f5f5);
440
- border-radius: 0.25rem;
441
- }
442
-
443
- .funkwhale-stats-widget__value {
444
- font-size: 1.25rem;
445
- font-weight: 700;
446
- color: var(--color-accent, #3b82f6);
447
- }
448
-
449
- .funkwhale-stats-widget__label {
450
- font-size: 0.625rem;
451
- color: var(--color-text-muted, #666);
452
- text-transform: uppercase;
453
- }
@@ -1,124 +0,0 @@
1
- {% extends "document.njk" %}
2
-
3
- {% block content %}
4
- <style>
5
- .fw-section { margin-bottom: 2rem; }
6
- .fw-list {
7
- list-style: none;
8
- padding: 0;
9
- margin: 0 0 1rem 0;
10
- }
11
- .fw-list-item {
12
- display: flex;
13
- align-items: center;
14
- gap: 0.75rem;
15
- padding: 0.75rem 0;
16
- border-bottom: 1px solid var(--color-border, #eee);
17
- }
18
- .fw-list-item:last-child { border-bottom: none; }
19
- .fw-list-item img {
20
- width: 48px;
21
- height: 48px;
22
- object-fit: cover;
23
- border-radius: 0.25rem;
24
- flex-shrink: 0;
25
- }
26
- .fw-list-item-placeholder {
27
- width: 48px;
28
- height: 48px;
29
- background: var(--color-border, #ddd);
30
- border-radius: 0.25rem;
31
- flex-shrink: 0;
32
- }
33
- .fw-list-info {
34
- flex: 1;
35
- min-width: 0;
36
- }
37
- .fw-list-title {
38
- display: block;
39
- font-weight: 500;
40
- text-decoration: none;
41
- color: inherit;
42
- white-space: nowrap;
43
- overflow: hidden;
44
- text-overflow: ellipsis;
45
- }
46
- .fw-list-title:hover { text-decoration: underline; }
47
- .fw-list-album {
48
- margin: 0.25rem 0;
49
- color: var(--color-text-secondary, #666);
50
- font-size: 0.875rem;
51
- }
52
- .fw-meta {
53
- display: flex;
54
- gap: 0.5rem;
55
- color: var(--color-text-secondary, #666);
56
- font-size: 0.75rem;
57
- }
58
- .fw-pagination {
59
- display: flex;
60
- align-items: center;
61
- justify-content: center;
62
- gap: 1rem;
63
- margin-top: 1.5rem;
64
- }
65
- .fw-pagination-info {
66
- color: var(--color-text-secondary, #666);
67
- }
68
- </style>
69
-
70
- {% if error %}
71
- {{ prose({ text: error.message }) }}
72
- {% else %}
73
- <section class="fw-section">
74
- {% if favorites and favorites.length > 0 %}
75
- <ul class="fw-list">
76
- {% for favorite in favorites %}
77
- <li class="fw-list-item">
78
- {% if favorite.coverUrl %}
79
- <img src="{{ favorite.coverUrl }}" alt="" loading="lazy">
80
- {% else %}
81
- <div class="fw-list-item-placeholder"></div>
82
- {% endif %}
83
- <div class="fw-list-info">
84
- <a href="{{ favorite.trackUrl }}" class="fw-list-title" target="_blank" rel="noopener">
85
- {{ favorite.artist }} - {{ favorite.track }}
86
- </a>
87
- {% if favorite.album %}
88
- <p class="fw-list-album">{{ favorite.album }}</p>
89
- {% endif %}
90
- <small class="fw-meta">
91
- <span>{{ favorite.duration }}</span>
92
- <span>Favorited {{ favorite.relativeTime }}</span>
93
- </small>
94
- </div>
95
- </li>
96
- {% endfor %}
97
- </ul>
98
-
99
- {# Pagination #}
100
- {% if pagination %}
101
- <nav class="fw-pagination">
102
- {% if pagination.hasPrev %}
103
- <a href="?page={{ pagination.current - 1 }}" class="button button--secondary">Previous</a>
104
- {% endif %}
105
- <span class="fw-pagination-info">
106
- Page {{ pagination.current }} of {{ pagination.total }}
107
- </span>
108
- {% if pagination.hasNext %}
109
- <a href="?page={{ pagination.current + 1 }}" class="button button--secondary">Next</a>
110
- {% endif %}
111
- </nav>
112
- {% endif %}
113
- {% else %}
114
- {{ prose({ text: __("funkwhale.noFavorites") }) }}
115
- {% endif %}
116
- </section>
117
-
118
- {{ button({
119
- classes: "button--secondary",
120
- href: mountPath,
121
- text: "Back to Dashboard"
122
- }) }}
123
- {% endif %}
124
- {% endblock %}
@@ -1,129 +0,0 @@
1
- {% extends "document.njk" %}
2
-
3
- {% block content %}
4
- <style>
5
- .fw-section { margin-bottom: 2rem; }
6
- .fw-list {
7
- list-style: none;
8
- padding: 0;
9
- margin: 0 0 1rem 0;
10
- }
11
- .fw-list-item {
12
- display: flex;
13
- align-items: center;
14
- gap: 0.75rem;
15
- padding: 0.75rem 0;
16
- border-bottom: 1px solid var(--color-border, #eee);
17
- }
18
- .fw-list-item:last-child { border-bottom: none; }
19
- .fw-list-item img {
20
- width: 48px;
21
- height: 48px;
22
- object-fit: cover;
23
- border-radius: 0.25rem;
24
- flex-shrink: 0;
25
- }
26
- .fw-list-item-placeholder {
27
- width: 48px;
28
- height: 48px;
29
- background: var(--color-border, #ddd);
30
- border-radius: 0.25rem;
31
- flex-shrink: 0;
32
- }
33
- .fw-list-info {
34
- flex: 1;
35
- min-width: 0;
36
- }
37
- .fw-list-title {
38
- display: block;
39
- font-weight: 500;
40
- text-decoration: none;
41
- color: inherit;
42
- white-space: nowrap;
43
- overflow: hidden;
44
- text-overflow: ellipsis;
45
- }
46
- .fw-list-title:hover { text-decoration: underline; }
47
- .fw-list-album {
48
- margin: 0.25rem 0;
49
- color: var(--color-text-secondary, #666);
50
- font-size: 0.875rem;
51
- }
52
- .fw-meta {
53
- display: flex;
54
- gap: 0.5rem;
55
- color: var(--color-text-secondary, #666);
56
- font-size: 0.75rem;
57
- }
58
- .fw-pagination {
59
- display: flex;
60
- align-items: center;
61
- justify-content: center;
62
- gap: 1rem;
63
- margin-top: 1.5rem;
64
- }
65
- .fw-pagination-info {
66
- color: var(--color-text-secondary, #666);
67
- }
68
- </style>
69
-
70
- {% if error %}
71
- {{ prose({ text: error.message }) }}
72
- {% else %}
73
- <section class="fw-section">
74
- {% if listenings and listenings.length > 0 %}
75
- <ul class="fw-list">
76
- {% for listening in listenings %}
77
- <li class="fw-list-item">
78
- {% if listening.coverUrl %}
79
- <img src="{{ listening.coverUrl }}" alt="" loading="lazy">
80
- {% else %}
81
- <div class="fw-list-item-placeholder"></div>
82
- {% endif %}
83
- <div class="fw-list-info">
84
- <a href="{{ listening.trackUrl }}" class="fw-list-title" target="_blank" rel="noopener">
85
- {{ listening.artist }} - {{ listening.track }}
86
- </a>
87
- {% if listening.album %}
88
- <p class="fw-list-album">{{ listening.album }}</p>
89
- {% endif %}
90
- <small class="fw-meta">
91
- <span>{{ listening.duration }}</span>
92
- <span>{{ listening.relativeTime }}</span>
93
- </small>
94
- </div>
95
- {% if listening.status == "now-playing" %}
96
- {{ badge({ color: "green", text: __("funkwhale.nowPlaying") }) }}
97
- {% elif listening.status == "recently-played" %}
98
- {{ badge({ color: "blue", text: __("funkwhale.recentlyPlayed") }) }}
99
- {% endif %}
100
- </li>
101
- {% endfor %}
102
- </ul>
103
-
104
- {# Pagination #}
105
- {% if pagination %}
106
- <nav class="fw-pagination">
107
- {% if pagination.hasPrev %}
108
- <a href="?page={{ pagination.current - 1 }}" class="button button--secondary">Previous</a>
109
- {% endif %}
110
- <span class="fw-pagination-info">
111
- Page {{ pagination.current }} of {{ pagination.total }}
112
- </span>
113
- {% if pagination.hasNext %}
114
- <a href="?page={{ pagination.current + 1 }}" class="button button--secondary">Next</a>
115
- {% endif %}
116
- </nav>
117
- {% endif %}
118
- {% else %}
119
- {{ prose({ text: __("funkwhale.noRecentPlays") }) }}
120
- {% endif %}
121
- </section>
122
-
123
- {{ button({
124
- classes: "button--secondary",
125
- href: mountPath,
126
- text: "Back to Dashboard"
127
- }) }}
128
- {% endif %}
129
- {% endblock %}
@@ -1,18 +0,0 @@
1
- <div class="funkwhale-stats-grid">
2
- <div class="funkwhale-stat">
3
- <span class="funkwhale-stat__value">{{ summary.totalPlays }}</span>
4
- <span class="funkwhale-stat__label">{{ __("funkwhale.plays") }}</span>
5
- </div>
6
- <div class="funkwhale-stat">
7
- <span class="funkwhale-stat__value">{{ summary.uniqueTracks }}</span>
8
- <span class="funkwhale-stat__label">{{ __("funkwhale.tracks") }}</span>
9
- </div>
10
- <div class="funkwhale-stat">
11
- <span class="funkwhale-stat__value">{{ summary.uniqueArtists }}</span>
12
- <span class="funkwhale-stat__label">{{ __("funkwhale.artists") }}</span>
13
- </div>
14
- <div class="funkwhale-stat">
15
- <span class="funkwhale-stat__value">{{ summary.totalDurationFormatted }}</span>
16
- <span class="funkwhale-stat__label">{{ __("funkwhale.listeningTime") }}</span>
17
- </div>
18
- </div>
@@ -1,20 +0,0 @@
1
- {% if topAlbums and topAlbums.length > 0 %}
2
- <ul class="funkwhale-album-grid">
3
- {% for album in topAlbums %}
4
- <li class="funkwhale-album">
5
- {% if album.coverUrl %}
6
- <img src="{{ album.coverUrl }}" alt="" class="funkwhale-album__cover" loading="lazy">
7
- {% else %}
8
- <div class="funkwhale-album__cover funkwhale-album__cover--placeholder"></div>
9
- {% endif %}
10
- <div class="funkwhale-album__info">
11
- <span class="funkwhale-album__title">{{ album.title }}</span>
12
- <span class="funkwhale-album__artist">{{ album.artist }}</span>
13
- <small class="funkwhale-meta">{{ album.playCount }} {{ __("funkwhale.plays") }}</small>
14
- </div>
15
- </li>
16
- {% endfor %}
17
- </ul>
18
- {% else %}
19
- {{ prose({ text: "No data yet" }) }}
20
- {% endif %}
@@ -1,13 +0,0 @@
1
- {% if topArtists and topArtists.length > 0 %}
2
- <ol class="funkwhale-top-list">
3
- {% for artist in topArtists %}
4
- <li class="funkwhale-top-list__item">
5
- <span class="funkwhale-top-list__rank">{{ loop.index }}</span>
6
- <span class="funkwhale-top-list__name">{{ artist.name }}</span>
7
- <span class="funkwhale-top-list__count">{{ artist.playCount }} {{ __("funkwhale.plays") }}</span>
8
- </li>
9
- {% endfor %}
10
- </ol>
11
- {% else %}
12
- {{ prose({ text: "No data yet" }) }}
13
- {% endif %}
package/views/stats.njk DELETED
@@ -1,432 +0,0 @@
1
- {% extends "document.njk" %}
2
-
3
- {% block content %}
4
- <style>
5
- .fw-section { margin-bottom: 2rem; }
6
-
7
- /* Stats grid */
8
- .fw-stats-grid {
9
- display: grid;
10
- grid-template-columns: repeat(4, 1fr);
11
- gap: 1rem;
12
- margin-bottom: 1.5rem;
13
- }
14
- @media (max-width: 600px) {
15
- .fw-stats-grid { grid-template-columns: repeat(2, 1fr); }
16
- }
17
- .fw-stat {
18
- display: flex;
19
- flex-direction: column;
20
- align-items: center;
21
- padding: 1rem;
22
- background: var(--color-offset, #f5f5f5);
23
- border-radius: 0.5rem;
24
- text-align: center;
25
- }
26
- .fw-stat-value {
27
- font-size: 1.5rem;
28
- font-weight: 700;
29
- color: var(--color-accent, #3b82f6);
30
- }
31
- .fw-stat-label {
32
- font-size: 0.75rem;
33
- color: var(--color-text-secondary, #666);
34
- text-transform: uppercase;
35
- letter-spacing: 0.05em;
36
- }
37
-
38
- /* Tabs */
39
- .fw-tabs {
40
- display: flex;
41
- gap: 0.25rem;
42
- margin-bottom: 1.5rem;
43
- border-bottom: 1px solid var(--color-border, #ddd);
44
- }
45
- .fw-tab {
46
- padding: 0.75rem 1rem;
47
- border: none;
48
- background: none;
49
- color: var(--color-text-secondary, #666);
50
- cursor: pointer;
51
- border-bottom: 2px solid transparent;
52
- margin-bottom: -1px;
53
- font-size: 0.875rem;
54
- font-weight: 500;
55
- }
56
- .fw-tab:hover { color: inherit; }
57
- .fw-tab--active {
58
- color: var(--color-accent, #3b82f6);
59
- border-bottom-color: var(--color-accent, #3b82f6);
60
- }
61
- .fw-tab-content { display: none; }
62
- .fw-tab-content--active { display: block; }
63
-
64
- /* Top list */
65
- .fw-top-list {
66
- list-style: none;
67
- padding: 0;
68
- margin: 0 0 1.5rem 0;
69
- }
70
- .fw-top-list li {
71
- display: flex;
72
- align-items: center;
73
- gap: 0.75rem;
74
- padding: 0.5rem 0;
75
- border-bottom: 1px solid var(--color-border, #eee);
76
- }
77
- .fw-top-list li:last-child { border-bottom: none; }
78
- .fw-rank {
79
- width: 1.5rem;
80
- height: 1.5rem;
81
- display: flex;
82
- align-items: center;
83
- justify-content: center;
84
- font-size: 0.75rem;
85
- font-weight: 600;
86
- background: var(--color-offset, #f5f5f5);
87
- border-radius: 50%;
88
- color: var(--color-text-secondary, #666);
89
- flex-shrink: 0;
90
- }
91
- .fw-name { flex: 1; font-weight: 500; }
92
- .fw-count {
93
- font-size: 0.875rem;
94
- color: var(--color-text-secondary, #666);
95
- }
96
-
97
- /* Album grid */
98
- .fw-album-grid {
99
- display: grid;
100
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
101
- gap: 1rem;
102
- list-style: none;
103
- padding: 0;
104
- margin: 0 0 1.5rem 0;
105
- }
106
- .fw-album {
107
- display: flex;
108
- flex-direction: column;
109
- gap: 0.5rem;
110
- }
111
- .fw-album img {
112
- width: 100%;
113
- aspect-ratio: 1;
114
- object-fit: cover;
115
- border-radius: 0.25rem;
116
- }
117
- .fw-album-placeholder {
118
- width: 100%;
119
- aspect-ratio: 1;
120
- background: var(--color-border, #ddd);
121
- border-radius: 0.25rem;
122
- }
123
- .fw-album-title {
124
- font-weight: 500;
125
- font-size: 0.875rem;
126
- white-space: nowrap;
127
- overflow: hidden;
128
- text-overflow: ellipsis;
129
- }
130
- .fw-album-artist {
131
- font-size: 0.75rem;
132
- color: var(--color-text-secondary, #666);
133
- white-space: nowrap;
134
- overflow: hidden;
135
- text-overflow: ellipsis;
136
- }
137
- .fw-album-plays {
138
- font-size: 0.75rem;
139
- color: var(--color-text-secondary, #666);
140
- }
141
-
142
- /* Chart */
143
- .fw-chart { margin: 1rem 0 1.5rem 0; }
144
- .fw-chart-bars {
145
- display: flex;
146
- align-items: flex-end;
147
- gap: 2px;
148
- height: 120px;
149
- padding: 0.5rem 0;
150
- }
151
- .fw-chart-bar-wrapper {
152
- flex: 1;
153
- height: 100%;
154
- display: flex;
155
- align-items: flex-end;
156
- }
157
- .fw-chart-bar {
158
- width: 100%;
159
- background: var(--color-accent, #3b82f6);
160
- border-radius: 2px 2px 0 0;
161
- min-height: 2px;
162
- transition: height 0.3s ease;
163
- }
164
- .fw-chart-bar-wrapper:hover .fw-chart-bar {
165
- background: var(--color-accent-hover, #2563eb);
166
- }
167
- .fw-chart-labels {
168
- display: flex;
169
- justify-content: space-between;
170
- font-size: 0.75rem;
171
- color: var(--color-text-secondary, #666);
172
- margin-top: 0.5rem;
173
- }
174
- </style>
175
-
176
- {% if error %}
177
- {{ prose({ text: error.message }) }}
178
- {% else %}
179
- <section class="fw-section">
180
- {# Tabs #}
181
- <div class="fw-tabs" role="tablist">
182
- <button class="fw-tab fw-tab--active" role="tab" data-tab="all" aria-selected="true">
183
- {{ __("funkwhale.allTime") }}
184
- </button>
185
- <button class="fw-tab" role="tab" data-tab="month" aria-selected="false">
186
- {{ __("funkwhale.thisMonth") }}
187
- </button>
188
- <button class="fw-tab" role="tab" data-tab="week" aria-selected="false">
189
- {{ __("funkwhale.thisWeek") }}
190
- </button>
191
- <button class="fw-tab" role="tab" data-tab="trends" aria-selected="false">
192
- {{ __("funkwhale.trends") }}
193
- </button>
194
- </div>
195
-
196
- {# All Time Tab #}
197
- <div class="fw-tab-content fw-tab-content--active" id="tab-all" role="tabpanel">
198
- {% set s = stats.summary.all %}
199
- <div class="fw-stats-grid">
200
- <div class="fw-stat">
201
- <span class="fw-stat-value">{{ s.totalPlays | default(0) }}</span>
202
- <span class="fw-stat-label">{{ __("funkwhale.plays") }}</span>
203
- </div>
204
- <div class="fw-stat">
205
- <span class="fw-stat-value">{{ s.uniqueTracks | default(0) }}</span>
206
- <span class="fw-stat-label">{{ __("funkwhale.tracks") }}</span>
207
- </div>
208
- <div class="fw-stat">
209
- <span class="fw-stat-value">{{ s.uniqueArtists | default(0) }}</span>
210
- <span class="fw-stat-label">{{ __("funkwhale.artists") }}</span>
211
- </div>
212
- <div class="fw-stat">
213
- <span class="fw-stat-value">{{ s.totalDurationFormatted | default("0m") }}</span>
214
- <span class="fw-stat-label">{{ __("funkwhale.listeningTime") }}</span>
215
- </div>
216
- </div>
217
-
218
- <h3>{{ __("funkwhale.topArtists") }}</h3>
219
- {% if stats.topArtists.all and stats.topArtists.all.length > 0 %}
220
- <ol class="fw-top-list">
221
- {% for artist in stats.topArtists.all %}
222
- <li>
223
- <span class="fw-rank">{{ loop.index }}</span>
224
- <span class="fw-name">{{ artist.name }}</span>
225
- <span class="fw-count">{{ artist.playCount }} {{ __("funkwhale.plays") }}</span>
226
- </li>
227
- {% endfor %}
228
- </ol>
229
- {% else %}
230
- {{ prose({ text: "No data yet" }) }}
231
- {% endif %}
232
-
233
- <h3>{{ __("funkwhale.topAlbums") }}</h3>
234
- {% if stats.topAlbums.all and stats.topAlbums.all.length > 0 %}
235
- <ul class="fw-album-grid">
236
- {% for album in stats.topAlbums.all %}
237
- <li class="fw-album">
238
- {% if album.coverUrl %}
239
- <img src="{{ album.coverUrl }}" alt="" loading="lazy">
240
- {% else %}
241
- <div class="fw-album-placeholder"></div>
242
- {% endif %}
243
- <span class="fw-album-title">{{ album.title }}</span>
244
- <span class="fw-album-artist">{{ album.artist }}</span>
245
- <span class="fw-album-plays">{{ album.playCount }} {{ __("funkwhale.plays") }}</span>
246
- </li>
247
- {% endfor %}
248
- </ul>
249
- {% else %}
250
- {{ prose({ text: "No data yet" }) }}
251
- {% endif %}
252
- </div>
253
-
254
- {# This Month Tab #}
255
- <div class="fw-tab-content" id="tab-month" role="tabpanel" hidden>
256
- {% set s = stats.summary.month %}
257
- <div class="fw-stats-grid">
258
- <div class="fw-stat">
259
- <span class="fw-stat-value">{{ s.totalPlays | default(0) }}</span>
260
- <span class="fw-stat-label">{{ __("funkwhale.plays") }}</span>
261
- </div>
262
- <div class="fw-stat">
263
- <span class="fw-stat-value">{{ s.uniqueTracks | default(0) }}</span>
264
- <span class="fw-stat-label">{{ __("funkwhale.tracks") }}</span>
265
- </div>
266
- <div class="fw-stat">
267
- <span class="fw-stat-value">{{ s.uniqueArtists | default(0) }}</span>
268
- <span class="fw-stat-label">{{ __("funkwhale.artists") }}</span>
269
- </div>
270
- <div class="fw-stat">
271
- <span class="fw-stat-value">{{ s.totalDurationFormatted | default("0m") }}</span>
272
- <span class="fw-stat-label">{{ __("funkwhale.listeningTime") }}</span>
273
- </div>
274
- </div>
275
-
276
- <h3>{{ __("funkwhale.topArtists") }}</h3>
277
- {% if stats.topArtists.month and stats.topArtists.month.length > 0 %}
278
- <ol class="fw-top-list">
279
- {% for artist in stats.topArtists.month %}
280
- <li>
281
- <span class="fw-rank">{{ loop.index }}</span>
282
- <span class="fw-name">{{ artist.name }}</span>
283
- <span class="fw-count">{{ artist.playCount }} {{ __("funkwhale.plays") }}</span>
284
- </li>
285
- {% endfor %}
286
- </ol>
287
- {% else %}
288
- {{ prose({ text: "No data yet" }) }}
289
- {% endif %}
290
-
291
- <h3>{{ __("funkwhale.topAlbums") }}</h3>
292
- {% if stats.topAlbums.month and stats.topAlbums.month.length > 0 %}
293
- <ul class="fw-album-grid">
294
- {% for album in stats.topAlbums.month %}
295
- <li class="fw-album">
296
- {% if album.coverUrl %}
297
- <img src="{{ album.coverUrl }}" alt="" loading="lazy">
298
- {% else %}
299
- <div class="fw-album-placeholder"></div>
300
- {% endif %}
301
- <span class="fw-album-title">{{ album.title }}</span>
302
- <span class="fw-album-artist">{{ album.artist }}</span>
303
- <span class="fw-album-plays">{{ album.playCount }} {{ __("funkwhale.plays") }}</span>
304
- </li>
305
- {% endfor %}
306
- </ul>
307
- {% else %}
308
- {{ prose({ text: "No data yet" }) }}
309
- {% endif %}
310
- </div>
311
-
312
- {# This Week Tab #}
313
- <div class="fw-tab-content" id="tab-week" role="tabpanel" hidden>
314
- {% set s = stats.summary.week %}
315
- <div class="fw-stats-grid">
316
- <div class="fw-stat">
317
- <span class="fw-stat-value">{{ s.totalPlays | default(0) }}</span>
318
- <span class="fw-stat-label">{{ __("funkwhale.plays") }}</span>
319
- </div>
320
- <div class="fw-stat">
321
- <span class="fw-stat-value">{{ s.uniqueTracks | default(0) }}</span>
322
- <span class="fw-stat-label">{{ __("funkwhale.tracks") }}</span>
323
- </div>
324
- <div class="fw-stat">
325
- <span class="fw-stat-value">{{ s.uniqueArtists | default(0) }}</span>
326
- <span class="fw-stat-label">{{ __("funkwhale.artists") }}</span>
327
- </div>
328
- <div class="fw-stat">
329
- <span class="fw-stat-value">{{ s.totalDurationFormatted | default("0m") }}</span>
330
- <span class="fw-stat-label">{{ __("funkwhale.listeningTime") }}</span>
331
- </div>
332
- </div>
333
-
334
- <h3>{{ __("funkwhale.topArtists") }}</h3>
335
- {% if stats.topArtists.week and stats.topArtists.week.length > 0 %}
336
- <ol class="fw-top-list">
337
- {% for artist in stats.topArtists.week %}
338
- <li>
339
- <span class="fw-rank">{{ loop.index }}</span>
340
- <span class="fw-name">{{ artist.name }}</span>
341
- <span class="fw-count">{{ artist.playCount }} {{ __("funkwhale.plays") }}</span>
342
- </li>
343
- {% endfor %}
344
- </ol>
345
- {% else %}
346
- {{ prose({ text: "No data yet" }) }}
347
- {% endif %}
348
-
349
- <h3>{{ __("funkwhale.topAlbums") }}</h3>
350
- {% if stats.topAlbums.week and stats.topAlbums.week.length > 0 %}
351
- <ul class="fw-album-grid">
352
- {% for album in stats.topAlbums.week %}
353
- <li class="fw-album">
354
- {% if album.coverUrl %}
355
- <img src="{{ album.coverUrl }}" alt="" loading="lazy">
356
- {% else %}
357
- <div class="fw-album-placeholder"></div>
358
- {% endif %}
359
- <span class="fw-album-title">{{ album.title }}</span>
360
- <span class="fw-album-artist">{{ album.artist }}</span>
361
- <span class="fw-album-plays">{{ album.playCount }} {{ __("funkwhale.plays") }}</span>
362
- </li>
363
- {% endfor %}
364
- </ul>
365
- {% else %}
366
- {{ prose({ text: "No data yet" }) }}
367
- {% endif %}
368
- </div>
369
-
370
- {# Trends Tab #}
371
- <div class="fw-tab-content" id="tab-trends" role="tabpanel" hidden>
372
- <h3>{{ __("funkwhale.listeningTrend") }}</h3>
373
- {% if stats.trends and stats.trends.length > 0 %}
374
- <div class="fw-chart">
375
- {% set maxCount = 0 %}
376
- {% for day in stats.trends %}
377
- {% if day.count > maxCount %}
378
- {% set maxCount = day.count %}
379
- {% endif %}
380
- {% endfor %}
381
- <div class="fw-chart-bars">
382
- {% for day in stats.trends %}
383
- <div class="fw-chart-bar-wrapper" title="{{ day.date }}: {{ day.count }} plays">
384
- <div class="fw-chart-bar" style="height: {{ (day.count / maxCount * 100) if maxCount > 0 else 0 }}%"></div>
385
- </div>
386
- {% endfor %}
387
- </div>
388
- <div class="fw-chart-labels">
389
- <span>{{ stats.trends[0].date }}</span>
390
- <span>{{ stats.trends[stats.trends.length - 1].date }}</span>
391
- </div>
392
- </div>
393
- {% else %}
394
- {{ prose({ text: "No trend data available yet" }) }}
395
- {% endif %}
396
- </div>
397
- </section>
398
-
399
- {{ button({
400
- classes: "button--secondary",
401
- href: mountPath,
402
- text: "Back to Dashboard"
403
- }) }}
404
-
405
- <script>
406
- // Simple tab switching
407
- document.querySelectorAll('.fw-tab').forEach(tab => {
408
- tab.addEventListener('click', () => {
409
- // Update tabs
410
- document.querySelectorAll('.fw-tab').forEach(t => {
411
- t.classList.remove('fw-tab--active');
412
- t.setAttribute('aria-selected', 'false');
413
- });
414
- tab.classList.add('fw-tab--active');
415
- tab.setAttribute('aria-selected', 'true');
416
-
417
- // Update content
418
- document.querySelectorAll('.fw-tab-content').forEach(content => {
419
- content.classList.remove('fw-tab-content--active');
420
- content.hidden = true;
421
- });
422
- const targetId = 'tab-' + tab.dataset.tab;
423
- const target = document.getElementById(targetId);
424
- if (target) {
425
- target.classList.add('fw-tab-content--active');
426
- target.hidden = false;
427
- }
428
- });
429
- });
430
- </script>
431
- {% endif %}
432
- {% endblock %}