@rmdes/indiekit-endpoint-funkwhale 1.0.2 → 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 +3 -15
- package/lib/controllers/dashboard.js +10 -13
- package/locales/en.json +2 -2
- package/package.json +1 -2
- package/views/funkwhale.njk +199 -59
- package/assets/styles.css +0 -453
- package/views/favorites.njk +0 -60
- package/views/listenings.njk +0 -65
- package/views/partials/stats-summary.njk +0 -18
- package/views/partials/top-albums.njk +0 -20
- package/views/partials/top-artists.njk +0 -13
- package/views/stats.njk +0 -130
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
|
-
*
|
|
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
|
|
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
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
34
|
-
"view": "View
|
|
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
|
+
"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",
|
package/views/funkwhale.njk
CHANGED
|
@@ -1,16 +1,169 @@
|
|
|
1
1
|
{% extends "document.njk" %}
|
|
2
2
|
|
|
3
3
|
{% block content %}
|
|
4
|
+
<style>
|
|
5
|
+
/* Inline styles to ensure proper rendering */
|
|
6
|
+
.fw-section { margin-bottom: 2rem; }
|
|
7
|
+
.fw-section h2 { margin-bottom: 1rem; }
|
|
8
|
+
|
|
9
|
+
/* Stats grid */
|
|
10
|
+
.fw-stats-grid {
|
|
11
|
+
display: grid;
|
|
12
|
+
grid-template-columns: repeat(3, 1fr);
|
|
13
|
+
gap: 1rem;
|
|
14
|
+
margin-bottom: 1rem;
|
|
15
|
+
}
|
|
16
|
+
.fw-stat {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
align-items: center;
|
|
20
|
+
padding: 1rem;
|
|
21
|
+
background: var(--color-offset, #f5f5f5);
|
|
22
|
+
border-radius: 0.5rem;
|
|
23
|
+
text-align: center;
|
|
24
|
+
}
|
|
25
|
+
.fw-stat-value {
|
|
26
|
+
font-size: 1.5rem;
|
|
27
|
+
font-weight: 700;
|
|
28
|
+
color: var(--color-accent, #3b82f6);
|
|
29
|
+
}
|
|
30
|
+
.fw-stat-label {
|
|
31
|
+
font-size: 0.75rem;
|
|
32
|
+
color: var(--color-text-secondary, #666);
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
letter-spacing: 0.05em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Featured track */
|
|
38
|
+
.fw-featured {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 1rem;
|
|
41
|
+
padding: 1rem;
|
|
42
|
+
background: var(--color-offset, #f5f5f5);
|
|
43
|
+
border-radius: 0.5rem;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
}
|
|
46
|
+
.fw-featured img {
|
|
47
|
+
width: 80px;
|
|
48
|
+
height: 80px;
|
|
49
|
+
object-fit: cover;
|
|
50
|
+
border-radius: 0.25rem;
|
|
51
|
+
flex-shrink: 0;
|
|
52
|
+
}
|
|
53
|
+
.fw-featured-info { flex: 1; }
|
|
54
|
+
.fw-featured-title {
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
color: inherit;
|
|
58
|
+
display: block;
|
|
59
|
+
}
|
|
60
|
+
.fw-featured-title:hover { text-decoration: underline; }
|
|
61
|
+
.fw-featured-album {
|
|
62
|
+
margin: 0.25rem 0;
|
|
63
|
+
color: var(--color-text-secondary, #666);
|
|
64
|
+
font-size: 0.875rem;
|
|
65
|
+
}
|
|
66
|
+
.fw-meta {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: 0.5rem;
|
|
69
|
+
color: var(--color-text-secondary, #666);
|
|
70
|
+
font-size: 0.75rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Now playing animation */
|
|
74
|
+
.fw-playing-status {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 0.5rem;
|
|
78
|
+
color: #22c55e;
|
|
79
|
+
}
|
|
80
|
+
.fw-bars {
|
|
81
|
+
display: flex;
|
|
82
|
+
gap: 2px;
|
|
83
|
+
align-items: flex-end;
|
|
84
|
+
height: 16px;
|
|
85
|
+
}
|
|
86
|
+
.fw-bars span {
|
|
87
|
+
width: 3px;
|
|
88
|
+
background: currentColor;
|
|
89
|
+
animation: fw-bar 0.5s infinite ease-in-out alternate;
|
|
90
|
+
}
|
|
91
|
+
.fw-bars span:nth-child(2) { animation-delay: 0.2s; }
|
|
92
|
+
.fw-bars span:nth-child(3) { animation-delay: 0.4s; }
|
|
93
|
+
@keyframes fw-bar {
|
|
94
|
+
from { height: 4px; }
|
|
95
|
+
to { height: 16px; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* List */
|
|
99
|
+
.fw-list {
|
|
100
|
+
list-style: none;
|
|
101
|
+
padding: 0;
|
|
102
|
+
margin: 0 0 1rem 0;
|
|
103
|
+
}
|
|
104
|
+
.fw-list-item {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 0.75rem;
|
|
108
|
+
padding: 0.75rem 0;
|
|
109
|
+
border-bottom: 1px solid var(--color-border, #eee);
|
|
110
|
+
}
|
|
111
|
+
.fw-list-item:last-child { border-bottom: none; }
|
|
112
|
+
.fw-list-item img {
|
|
113
|
+
width: 48px;
|
|
114
|
+
height: 48px;
|
|
115
|
+
object-fit: cover;
|
|
116
|
+
border-radius: 0.25rem;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
}
|
|
119
|
+
.fw-list-item-placeholder {
|
|
120
|
+
width: 48px;
|
|
121
|
+
height: 48px;
|
|
122
|
+
background: var(--color-border, #ddd);
|
|
123
|
+
border-radius: 0.25rem;
|
|
124
|
+
flex-shrink: 0;
|
|
125
|
+
}
|
|
126
|
+
.fw-list-info {
|
|
127
|
+
flex: 1;
|
|
128
|
+
min-width: 0;
|
|
129
|
+
}
|
|
130
|
+
.fw-list-title {
|
|
131
|
+
display: block;
|
|
132
|
+
font-weight: 500;
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
color: inherit;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
text-overflow: ellipsis;
|
|
138
|
+
}
|
|
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
|
+
}
|
|
155
|
+
</style>
|
|
156
|
+
|
|
4
157
|
{% if error %}
|
|
5
158
|
{{ prose({ text: error.message }) }}
|
|
6
159
|
{% else %}
|
|
7
160
|
{# Now Playing / Recently Played #}
|
|
8
161
|
{% if nowPlaying %}
|
|
9
|
-
<section class="
|
|
162
|
+
<section class="fw-section">
|
|
10
163
|
<h2>
|
|
11
164
|
{% if nowPlaying.status == "now-playing" %}
|
|
12
|
-
<span class="
|
|
13
|
-
<span class="
|
|
165
|
+
<span class="fw-playing-status">
|
|
166
|
+
<span class="fw-bars">
|
|
14
167
|
<span></span><span></span><span></span>
|
|
15
168
|
</span>
|
|
16
169
|
{{ __("funkwhale.nowPlaying") }}
|
|
@@ -19,20 +172,18 @@
|
|
|
19
172
|
{{ __("funkwhale.recentlyPlayed") }}
|
|
20
173
|
{% endif %}
|
|
21
174
|
</h2>
|
|
22
|
-
<article class="
|
|
175
|
+
<article class="fw-featured">
|
|
23
176
|
{% if nowPlaying.coverUrl %}
|
|
24
|
-
<img src="{{ nowPlaying.coverUrl }}" alt=""
|
|
25
|
-
{% else %}
|
|
26
|
-
<div class="funkwhale-track__cover funkwhale-track__cover--placeholder"></div>
|
|
177
|
+
<img src="{{ nowPlaying.coverUrl }}" alt="" loading="lazy">
|
|
27
178
|
{% endif %}
|
|
28
|
-
<div class="
|
|
29
|
-
<a href="{{ nowPlaying.trackUrl }}" class="
|
|
179
|
+
<div class="fw-featured-info">
|
|
180
|
+
<a href="{{ nowPlaying.trackUrl }}" class="fw-featured-title" target="_blank" rel="noopener">
|
|
30
181
|
{{ nowPlaying.artist }} - {{ nowPlaying.track }}
|
|
31
182
|
</a>
|
|
32
183
|
{% if nowPlaying.album %}
|
|
33
|
-
<p class="
|
|
184
|
+
<p class="fw-featured-album">{{ nowPlaying.album }}</p>
|
|
34
185
|
{% endif %}
|
|
35
|
-
<small class="
|
|
186
|
+
<small class="fw-meta">
|
|
36
187
|
<span>{{ nowPlaying.duration }}</span>
|
|
37
188
|
<span>{{ nowPlaying.relativeTime }}</span>
|
|
38
189
|
</small>
|
|
@@ -43,49 +194,42 @@
|
|
|
43
194
|
|
|
44
195
|
{# Quick Stats Summary #}
|
|
45
196
|
{% if summary %}
|
|
46
|
-
<section class="
|
|
197
|
+
<section class="fw-section">
|
|
47
198
|
<h2>{{ __("funkwhale.stats") }}</h2>
|
|
48
|
-
<div class="
|
|
49
|
-
<div class="
|
|
50
|
-
<span class="
|
|
51
|
-
<span class="
|
|
199
|
+
<div class="fw-stats-grid">
|
|
200
|
+
<div class="fw-stat">
|
|
201
|
+
<span class="fw-stat-value">{{ summary.totalPlays | default(0) }}</span>
|
|
202
|
+
<span class="fw-stat-label">{{ __("funkwhale.plays") }}</span>
|
|
52
203
|
</div>
|
|
53
|
-
<div class="
|
|
54
|
-
<span class="
|
|
55
|
-
<span class="
|
|
204
|
+
<div class="fw-stat">
|
|
205
|
+
<span class="fw-stat-value">{{ summary.uniqueTracks | default(0) }}</span>
|
|
206
|
+
<span class="fw-stat-label">{{ __("funkwhale.tracks") }}</span>
|
|
56
207
|
</div>
|
|
57
|
-
<div class="
|
|
58
|
-
<span class="
|
|
59
|
-
<span class="
|
|
208
|
+
<div class="fw-stat">
|
|
209
|
+
<span class="fw-stat-value">{{ summary.uniqueArtists | default(0) }}</span>
|
|
210
|
+
<span class="fw-stat-label">{{ __("funkwhale.artists") }}</span>
|
|
60
211
|
</div>
|
|
61
212
|
</div>
|
|
62
|
-
<div class="button-grid">
|
|
63
|
-
{{ button({
|
|
64
|
-
classes: "button--secondary",
|
|
65
|
-
href: mountPath + "/stats",
|
|
66
|
-
text: __("funkwhale.viewStats")
|
|
67
|
-
}) }}
|
|
68
|
-
</div>
|
|
69
213
|
</section>
|
|
70
214
|
{% endif %}
|
|
71
215
|
|
|
72
216
|
{# Recent Listenings #}
|
|
73
|
-
<section class="
|
|
217
|
+
<section class="fw-section">
|
|
74
218
|
<h2>{{ __("funkwhale.listenings") }}</h2>
|
|
75
219
|
{% if listenings and listenings.length > 0 %}
|
|
76
|
-
<ul class="
|
|
220
|
+
<ul class="fw-list">
|
|
77
221
|
{% for listening in listenings %}
|
|
78
|
-
<li class="
|
|
222
|
+
<li class="fw-list-item">
|
|
79
223
|
{% if listening.coverUrl %}
|
|
80
|
-
<img src="{{ listening.coverUrl }}" alt=""
|
|
224
|
+
<img src="{{ listening.coverUrl }}" alt="" loading="lazy">
|
|
81
225
|
{% else %}
|
|
82
|
-
<div class="
|
|
226
|
+
<div class="fw-list-item-placeholder"></div>
|
|
83
227
|
{% endif %}
|
|
84
|
-
<div class="
|
|
85
|
-
<a href="{{ listening.trackUrl }}" class="
|
|
228
|
+
<div class="fw-list-info">
|
|
229
|
+
<a href="{{ listening.trackUrl }}" class="fw-list-title" target="_blank" rel="noopener">
|
|
86
230
|
{{ listening.artist }} - {{ listening.track }}
|
|
87
231
|
</a>
|
|
88
|
-
<small class="
|
|
232
|
+
<small class="fw-meta">{{ listening.relativeTime }}</small>
|
|
89
233
|
</div>
|
|
90
234
|
{% if listening.status == "now-playing" %}
|
|
91
235
|
{{ badge({ color: "green", text: __("funkwhale.nowPlaying") }) }}
|
|
@@ -95,51 +239,47 @@
|
|
|
95
239
|
</li>
|
|
96
240
|
{% endfor %}
|
|
97
241
|
</ul>
|
|
98
|
-
<div class="button-grid">
|
|
99
|
-
{{ button({
|
|
100
|
-
classes: "button--secondary",
|
|
101
|
-
href: mountPath + "/listenings",
|
|
102
|
-
text: __("funkwhale.viewAll")
|
|
103
|
-
}) }}
|
|
104
|
-
</div>
|
|
105
242
|
{% else %}
|
|
106
243
|
{{ prose({ text: __("funkwhale.noRecentPlays") }) }}
|
|
107
244
|
{% endif %}
|
|
108
245
|
</section>
|
|
109
246
|
|
|
110
247
|
{# Favorites #}
|
|
111
|
-
<section class="
|
|
248
|
+
<section class="fw-section">
|
|
112
249
|
<h2>{{ __("funkwhale.favorites") }}</h2>
|
|
113
250
|
{% if favorites and favorites.length > 0 %}
|
|
114
|
-
<ul class="
|
|
251
|
+
<ul class="fw-list">
|
|
115
252
|
{% for favorite in favorites %}
|
|
116
|
-
<li class="
|
|
253
|
+
<li class="fw-list-item">
|
|
117
254
|
{% if favorite.coverUrl %}
|
|
118
|
-
<img src="{{ favorite.coverUrl }}" alt=""
|
|
255
|
+
<img src="{{ favorite.coverUrl }}" alt="" loading="lazy">
|
|
119
256
|
{% else %}
|
|
120
|
-
<div class="
|
|
257
|
+
<div class="fw-list-item-placeholder"></div>
|
|
121
258
|
{% endif %}
|
|
122
|
-
<div class="
|
|
123
|
-
<a href="{{ favorite.trackUrl }}" class="
|
|
259
|
+
<div class="fw-list-info">
|
|
260
|
+
<a href="{{ favorite.trackUrl }}" class="fw-list-title" target="_blank" rel="noopener">
|
|
124
261
|
{{ favorite.artist }} - {{ favorite.track }}
|
|
125
262
|
</a>
|
|
126
263
|
{% if favorite.album %}
|
|
127
|
-
<small class="
|
|
264
|
+
<small class="fw-meta">{{ favorite.album }}</small>
|
|
128
265
|
{% endif %}
|
|
129
266
|
</div>
|
|
130
267
|
</li>
|
|
131
268
|
{% endfor %}
|
|
132
269
|
</ul>
|
|
133
|
-
<div class="button-grid">
|
|
134
|
-
{{ button({
|
|
135
|
-
classes: "button--secondary",
|
|
136
|
-
href: mountPath + "/favorites",
|
|
137
|
-
text: __("funkwhale.viewAll")
|
|
138
|
-
}) }}
|
|
139
|
-
</div>
|
|
140
270
|
{% else %}
|
|
141
271
|
{{ prose({ text: __("funkwhale.noFavorites") }) }}
|
|
142
272
|
{% endif %}
|
|
143
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>
|
|
144
284
|
{% endif %}
|
|
145
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
|
-
}
|
package/views/favorites.njk
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{% extends "document.njk" %}
|
|
2
|
-
|
|
3
|
-
{% block content %}
|
|
4
|
-
{% if error %}
|
|
5
|
-
{{ prose({ text: error.message }) }}
|
|
6
|
-
{% else %}
|
|
7
|
-
<section class="funkwhale-section">
|
|
8
|
-
{% if favorites and favorites.length > 0 %}
|
|
9
|
-
<ul class="funkwhale-list funkwhale-list--full">
|
|
10
|
-
{% for favorite in favorites %}
|
|
11
|
-
<li class="funkwhale-list__item">
|
|
12
|
-
{% if favorite.coverUrl %}
|
|
13
|
-
<img src="{{ favorite.coverUrl }}" alt="" class="funkwhale-list__cover" loading="lazy">
|
|
14
|
-
{% else %}
|
|
15
|
-
<div class="funkwhale-list__cover funkwhale-list__cover--placeholder"></div>
|
|
16
|
-
{% endif %}
|
|
17
|
-
<div class="funkwhale-list__info">
|
|
18
|
-
<a href="{{ favorite.trackUrl }}" class="funkwhale-list__title" target="_blank" rel="noopener">
|
|
19
|
-
{{ favorite.artist }} - {{ favorite.track }}
|
|
20
|
-
</a>
|
|
21
|
-
{% if favorite.album %}
|
|
22
|
-
<p class="funkwhale-list__album">{{ favorite.album }}</p>
|
|
23
|
-
{% endif %}
|
|
24
|
-
<small class="funkwhale-meta">
|
|
25
|
-
<span>{{ favorite.duration }}</span>
|
|
26
|
-
<span>Favorited {{ favorite.relativeTime }}</span>
|
|
27
|
-
</small>
|
|
28
|
-
</div>
|
|
29
|
-
</li>
|
|
30
|
-
{% endfor %}
|
|
31
|
-
</ul>
|
|
32
|
-
|
|
33
|
-
{# Pagination #}
|
|
34
|
-
{% if pagination %}
|
|
35
|
-
<nav class="funkwhale-pagination">
|
|
36
|
-
{% if pagination.hasPrev %}
|
|
37
|
-
<a href="?page={{ pagination.current - 1 }}" class="button button--secondary">Previous</a>
|
|
38
|
-
{% endif %}
|
|
39
|
-
<span class="funkwhale-pagination__info">
|
|
40
|
-
Page {{ pagination.current }} of {{ pagination.total }}
|
|
41
|
-
</span>
|
|
42
|
-
{% if pagination.hasNext %}
|
|
43
|
-
<a href="?page={{ pagination.current + 1 }}" class="button button--secondary">Next</a>
|
|
44
|
-
{% endif %}
|
|
45
|
-
</nav>
|
|
46
|
-
{% endif %}
|
|
47
|
-
{% else %}
|
|
48
|
-
{{ prose({ text: __("funkwhale.noFavorites") }) }}
|
|
49
|
-
{% endif %}
|
|
50
|
-
</section>
|
|
51
|
-
|
|
52
|
-
<div class="button-grid">
|
|
53
|
-
{{ button({
|
|
54
|
-
classes: "button--secondary",
|
|
55
|
-
href: mountPath,
|
|
56
|
-
text: "Back to Dashboard"
|
|
57
|
-
}) }}
|
|
58
|
-
</div>
|
|
59
|
-
{% endif %}
|
|
60
|
-
{% endblock %}
|
package/views/listenings.njk
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
{% extends "document.njk" %}
|
|
2
|
-
|
|
3
|
-
{% block content %}
|
|
4
|
-
{% if error %}
|
|
5
|
-
{{ prose({ text: error.message }) }}
|
|
6
|
-
{% else %}
|
|
7
|
-
<section class="funkwhale-section">
|
|
8
|
-
{% if listenings and listenings.length > 0 %}
|
|
9
|
-
<ul class="funkwhale-list funkwhale-list--full">
|
|
10
|
-
{% for listening in listenings %}
|
|
11
|
-
<li class="funkwhale-list__item">
|
|
12
|
-
{% if listening.coverUrl %}
|
|
13
|
-
<img src="{{ listening.coverUrl }}" alt="" class="funkwhale-list__cover" loading="lazy">
|
|
14
|
-
{% else %}
|
|
15
|
-
<div class="funkwhale-list__cover funkwhale-list__cover--placeholder"></div>
|
|
16
|
-
{% endif %}
|
|
17
|
-
<div class="funkwhale-list__info">
|
|
18
|
-
<a href="{{ listening.trackUrl }}" class="funkwhale-list__title" target="_blank" rel="noopener">
|
|
19
|
-
{{ listening.artist }} - {{ listening.track }}
|
|
20
|
-
</a>
|
|
21
|
-
{% if listening.album %}
|
|
22
|
-
<p class="funkwhale-list__album">{{ listening.album }}</p>
|
|
23
|
-
{% endif %}
|
|
24
|
-
<small class="funkwhale-meta">
|
|
25
|
-
<span>{{ listening.duration }}</span>
|
|
26
|
-
<span>{{ listening.relativeTime }}</span>
|
|
27
|
-
</small>
|
|
28
|
-
</div>
|
|
29
|
-
{% if listening.status == "now-playing" %}
|
|
30
|
-
{{ badge({ color: "green", text: __("funkwhale.nowPlaying") }) }}
|
|
31
|
-
{% elif listening.status == "recently-played" %}
|
|
32
|
-
{{ badge({ color: "blue", text: __("funkwhale.recentlyPlayed") }) }}
|
|
33
|
-
{% endif %}
|
|
34
|
-
</li>
|
|
35
|
-
{% endfor %}
|
|
36
|
-
</ul>
|
|
37
|
-
|
|
38
|
-
{# Pagination #}
|
|
39
|
-
{% if pagination %}
|
|
40
|
-
<nav class="funkwhale-pagination">
|
|
41
|
-
{% if pagination.hasPrev %}
|
|
42
|
-
<a href="?page={{ pagination.current - 1 }}" class="button button--secondary">Previous</a>
|
|
43
|
-
{% endif %}
|
|
44
|
-
<span class="funkwhale-pagination__info">
|
|
45
|
-
Page {{ pagination.current }} of {{ pagination.total }}
|
|
46
|
-
</span>
|
|
47
|
-
{% if pagination.hasNext %}
|
|
48
|
-
<a href="?page={{ pagination.current + 1 }}" class="button button--secondary">Next</a>
|
|
49
|
-
{% endif %}
|
|
50
|
-
</nav>
|
|
51
|
-
{% endif %}
|
|
52
|
-
{% else %}
|
|
53
|
-
{{ prose({ text: __("funkwhale.noRecentPlays") }) }}
|
|
54
|
-
{% endif %}
|
|
55
|
-
</section>
|
|
56
|
-
|
|
57
|
-
<div class="button-grid">
|
|
58
|
-
{{ button({
|
|
59
|
-
classes: "button--secondary",
|
|
60
|
-
href: mountPath,
|
|
61
|
-
text: "Back to Dashboard"
|
|
62
|
-
}) }}
|
|
63
|
-
</div>
|
|
64
|
-
{% endif %}
|
|
65
|
-
{% 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,130 +0,0 @@
|
|
|
1
|
-
{% extends "document.njk" %}
|
|
2
|
-
|
|
3
|
-
{% block content %}
|
|
4
|
-
{% if error %}
|
|
5
|
-
{{ prose({ text: error.message }) }}
|
|
6
|
-
{% else %}
|
|
7
|
-
<section class="funkwhale-section">
|
|
8
|
-
{# Tabs #}
|
|
9
|
-
<div class="funkwhale-tabs" role="tablist">
|
|
10
|
-
<button class="funkwhale-tab funkwhale-tab--active" role="tab" data-tab="all" aria-selected="true">
|
|
11
|
-
{{ __("funkwhale.allTime") }}
|
|
12
|
-
</button>
|
|
13
|
-
<button class="funkwhale-tab" role="tab" data-tab="month" aria-selected="false">
|
|
14
|
-
{{ __("funkwhale.thisMonth") }}
|
|
15
|
-
</button>
|
|
16
|
-
<button class="funkwhale-tab" role="tab" data-tab="week" aria-selected="false">
|
|
17
|
-
{{ __("funkwhale.thisWeek") }}
|
|
18
|
-
</button>
|
|
19
|
-
<button class="funkwhale-tab" role="tab" data-tab="trends" aria-selected="false">
|
|
20
|
-
{{ __("funkwhale.trends") }}
|
|
21
|
-
</button>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
{# All Time Tab #}
|
|
25
|
-
<div class="funkwhale-tab-content funkwhale-tab-content--active" id="tab-all" role="tabpanel">
|
|
26
|
-
{% set summary = stats.summary.all %}
|
|
27
|
-
{% include "partials/stats-summary.njk" %}
|
|
28
|
-
|
|
29
|
-
<h3>{{ __("funkwhale.topArtists") }}</h3>
|
|
30
|
-
{% set topArtists = stats.topArtists.all %}
|
|
31
|
-
{% include "partials/top-artists.njk" %}
|
|
32
|
-
|
|
33
|
-
<h3>{{ __("funkwhale.topAlbums") }}</h3>
|
|
34
|
-
{% set topAlbums = stats.topAlbums.all %}
|
|
35
|
-
{% include "partials/top-albums.njk" %}
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
{# This Month Tab #}
|
|
39
|
-
<div class="funkwhale-tab-content" id="tab-month" role="tabpanel" hidden>
|
|
40
|
-
{% set summary = stats.summary.month %}
|
|
41
|
-
{% include "partials/stats-summary.njk" %}
|
|
42
|
-
|
|
43
|
-
<h3>{{ __("funkwhale.topArtists") }}</h3>
|
|
44
|
-
{% set topArtists = stats.topArtists.month %}
|
|
45
|
-
{% include "partials/top-artists.njk" %}
|
|
46
|
-
|
|
47
|
-
<h3>{{ __("funkwhale.topAlbums") }}</h3>
|
|
48
|
-
{% set topAlbums = stats.topAlbums.month %}
|
|
49
|
-
{% include "partials/top-albums.njk" %}
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
{# This Week Tab #}
|
|
53
|
-
<div class="funkwhale-tab-content" id="tab-week" role="tabpanel" hidden>
|
|
54
|
-
{% set summary = stats.summary.week %}
|
|
55
|
-
{% include "partials/stats-summary.njk" %}
|
|
56
|
-
|
|
57
|
-
<h3>{{ __("funkwhale.topArtists") }}</h3>
|
|
58
|
-
{% set topArtists = stats.topArtists.week %}
|
|
59
|
-
{% include "partials/top-artists.njk" %}
|
|
60
|
-
|
|
61
|
-
<h3>{{ __("funkwhale.topAlbums") }}</h3>
|
|
62
|
-
{% set topAlbums = stats.topAlbums.week %}
|
|
63
|
-
{% include "partials/top-albums.njk" %}
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
{# Trends Tab #}
|
|
67
|
-
<div class="funkwhale-tab-content" id="tab-trends" role="tabpanel" hidden>
|
|
68
|
-
<h3>{{ __("funkwhale.listeningTrend") }}</h3>
|
|
69
|
-
{% if stats.trends and stats.trends.length > 0 %}
|
|
70
|
-
<div class="funkwhale-chart">
|
|
71
|
-
{% set maxCount = 0 %}
|
|
72
|
-
{% for day in stats.trends %}
|
|
73
|
-
{% if day.count > maxCount %}
|
|
74
|
-
{% set maxCount = day.count %}
|
|
75
|
-
{% endif %}
|
|
76
|
-
{% endfor %}
|
|
77
|
-
<div class="funkwhale-chart__bars">
|
|
78
|
-
{% for day in stats.trends %}
|
|
79
|
-
<div class="funkwhale-chart__bar-wrapper" title="{{ day.date }}: {{ day.count }} plays">
|
|
80
|
-
<div class="funkwhale-chart__bar" style="height: {{ (day.count / maxCount * 100) if maxCount > 0 else 0 }}%"></div>
|
|
81
|
-
</div>
|
|
82
|
-
{% endfor %}
|
|
83
|
-
</div>
|
|
84
|
-
<div class="funkwhale-chart__labels">
|
|
85
|
-
<span>{{ stats.trends[0].date }}</span>
|
|
86
|
-
<span>{{ stats.trends[stats.trends.length - 1].date }}</span>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
{% else %}
|
|
90
|
-
{{ prose({ text: "No trend data available yet" }) }}
|
|
91
|
-
{% endif %}
|
|
92
|
-
</div>
|
|
93
|
-
</section>
|
|
94
|
-
|
|
95
|
-
<div class="button-grid">
|
|
96
|
-
{{ button({
|
|
97
|
-
classes: "button--secondary",
|
|
98
|
-
href: mountPath,
|
|
99
|
-
text: "Back to Dashboard"
|
|
100
|
-
}) }}
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<script>
|
|
104
|
-
// Simple tab switching
|
|
105
|
-
document.querySelectorAll('.funkwhale-tab').forEach(tab => {
|
|
106
|
-
tab.addEventListener('click', () => {
|
|
107
|
-
// Update tabs
|
|
108
|
-
document.querySelectorAll('.funkwhale-tab').forEach(t => {
|
|
109
|
-
t.classList.remove('funkwhale-tab--active');
|
|
110
|
-
t.setAttribute('aria-selected', 'false');
|
|
111
|
-
});
|
|
112
|
-
tab.classList.add('funkwhale-tab--active');
|
|
113
|
-
tab.setAttribute('aria-selected', 'true');
|
|
114
|
-
|
|
115
|
-
// Update content
|
|
116
|
-
document.querySelectorAll('.funkwhale-tab-content').forEach(content => {
|
|
117
|
-
content.classList.remove('funkwhale-tab-content--active');
|
|
118
|
-
content.hidden = true;
|
|
119
|
-
});
|
|
120
|
-
const targetId = 'tab-' + tab.dataset.tab;
|
|
121
|
-
const target = document.getElementById(targetId);
|
|
122
|
-
if (target) {
|
|
123
|
-
target.classList.add('funkwhale-tab-content--active');
|
|
124
|
-
target.hidden = false;
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
</script>
|
|
129
|
-
{% endif %}
|
|
130
|
-
{% endblock %}
|