@rmdes/indiekit-endpoint-funkwhale 1.0.3 → 1.0.5
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 +6 -15
- package/lib/controllers/dashboard.js +18 -11
- package/lib/sync.js +19 -0
- package/locales/en.json +2 -2
- package/package.json +1 -2
- package/views/funkwhale.njk +25 -15
- package/assets/styles.css +0 -453
- package/views/favorites.njk +0 -124
- package/views/listenings.njk +0 -129
- 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 -432
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);
|
|
@@ -112,6 +100,9 @@ export default class FunkwhaleEndpoint {
|
|
|
112
100
|
Indiekit.config.application.funkwhaleConfig = this.options;
|
|
113
101
|
Indiekit.config.application.funkwhaleEndpoint = this.mountPath;
|
|
114
102
|
|
|
103
|
+
// Store database getter for controller access
|
|
104
|
+
Indiekit.config.application.getFunkwhaleDb = () => Indiekit.database;
|
|
105
|
+
|
|
115
106
|
// Start background sync if database is available
|
|
116
107
|
if (Indiekit.config.application.mongodbUrl) {
|
|
117
108
|
startSync(Indiekit, this.options);
|
|
@@ -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, refreshStatsCache } 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,23 @@ export const dashboardController = {
|
|
|
64
63
|
});
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
// Get stats
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
// Get stats from cache (same source as public API)
|
|
67
|
+
// If cache is empty, try to refresh it from database
|
|
68
|
+
let cachedStats = getCachedStats();
|
|
69
|
+
if (!cachedStats) {
|
|
70
|
+
const getDb = request.app.locals.application.getFunkwhaleDb;
|
|
71
|
+
if (getDb) {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
if (db) {
|
|
74
|
+
cachedStats = await refreshStatsCache(db, limits);
|
|
75
|
+
}
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
const summary = cachedStats?.summary?.all || null;
|
|
79
|
+
|
|
80
|
+
// Determine public frontend URL (strip 'api' from mount path)
|
|
81
|
+
// e.g., /funkwhaleapi -> /funkwhale
|
|
82
|
+
const publicUrl = funkwhaleEndpoint ? funkwhaleEndpoint.replace(/api$/, "") : "/funkwhale";
|
|
77
83
|
|
|
78
84
|
response.render("funkwhale", {
|
|
79
85
|
title: response.locals.__("funkwhale.title"),
|
|
@@ -81,6 +87,7 @@ export const dashboardController = {
|
|
|
81
87
|
listenings: listenings.slice(0, 5),
|
|
82
88
|
favorites: favorites.slice(0, 5),
|
|
83
89
|
summary,
|
|
90
|
+
publicUrl,
|
|
84
91
|
mountPath: request.baseUrl,
|
|
85
92
|
});
|
|
86
93
|
} catch (error) {
|
package/lib/sync.js
CHANGED
|
@@ -30,6 +30,25 @@ export function setCachedStats(stats) {
|
|
|
30
30
|
cachedStatsTime = Date.now();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Refresh stats cache from database (for when cache is empty)
|
|
35
|
+
* @param {object} db - MongoDB database instance
|
|
36
|
+
* @param {object} limits - Limits for top lists
|
|
37
|
+
* @returns {Promise<object|null>} - Stats or null if failed
|
|
38
|
+
*/
|
|
39
|
+
export async function refreshStatsCache(db, limits = {}) {
|
|
40
|
+
if (!db) return null;
|
|
41
|
+
try {
|
|
42
|
+
const stats = await getAllStats(db, limits);
|
|
43
|
+
setCachedStats(stats);
|
|
44
|
+
console.log("[Funkwhale] Stats cache refreshed on-demand");
|
|
45
|
+
return stats;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error("[Funkwhale] Failed to refresh stats cache:", err.message);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
/**
|
|
34
53
|
* Start background sync process
|
|
35
54
|
* @param {object} Indiekit - Indiekit instance
|
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.5",
|
|
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
|
@@ -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
|
-
}
|