@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 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.2",
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",
@@ -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="funkwhale-section funkwhale-now-playing">
162
+ <section class="fw-section">
10
163
  <h2>
11
164
  {% if nowPlaying.status == "now-playing" %}
12
- <span class="funkwhale-status funkwhale-status--playing">
13
- <span class="funkwhale-bars">
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="funkwhale-track funkwhale-track--featured">
175
+ <article class="fw-featured">
23
176
  {% if nowPlaying.coverUrl %}
24
- <img src="{{ nowPlaying.coverUrl }}" alt="" class="funkwhale-track__cover" loading="lazy">
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="funkwhale-track__info">
29
- <a href="{{ nowPlaying.trackUrl }}" class="funkwhale-track__title" target="_blank" rel="noopener">
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="funkwhale-track__album">{{ nowPlaying.album }}</p>
184
+ <p class="fw-featured-album">{{ nowPlaying.album }}</p>
34
185
  {% endif %}
35
- <small class="funkwhale-meta">
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="funkwhale-section funkwhale-summary">
197
+ <section class="fw-section">
47
198
  <h2>{{ __("funkwhale.stats") }}</h2>
48
- <div class="funkwhale-stats-grid">
49
- <div class="funkwhale-stat">
50
- <span class="funkwhale-stat__value">{{ summary.totalPlays }}</span>
51
- <span class="funkwhale-stat__label">{{ __("funkwhale.plays") }}</span>
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="funkwhale-stat">
54
- <span class="funkwhale-stat__value">{{ summary.uniqueTracks }}</span>
55
- <span class="funkwhale-stat__label">{{ __("funkwhale.tracks") }}</span>
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="funkwhale-stat">
58
- <span class="funkwhale-stat__value">{{ summary.uniqueArtists }}</span>
59
- <span class="funkwhale-stat__label">{{ __("funkwhale.artists") }}</span>
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="funkwhale-section">
217
+ <section class="fw-section">
74
218
  <h2>{{ __("funkwhale.listenings") }}</h2>
75
219
  {% if listenings and listenings.length > 0 %}
76
- <ul class="funkwhale-list">
220
+ <ul class="fw-list">
77
221
  {% for listening in listenings %}
78
- <li class="funkwhale-list__item">
222
+ <li class="fw-list-item">
79
223
  {% if listening.coverUrl %}
80
- <img src="{{ listening.coverUrl }}" alt="" class="funkwhale-list__cover" loading="lazy">
224
+ <img src="{{ listening.coverUrl }}" alt="" loading="lazy">
81
225
  {% else %}
82
- <div class="funkwhale-list__cover funkwhale-list__cover--placeholder"></div>
226
+ <div class="fw-list-item-placeholder"></div>
83
227
  {% endif %}
84
- <div class="funkwhale-list__info">
85
- <a href="{{ listening.trackUrl }}" class="funkwhale-list__title" target="_blank" rel="noopener">
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="funkwhale-meta">{{ listening.relativeTime }}</small>
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="funkwhale-section">
248
+ <section class="fw-section">
112
249
  <h2>{{ __("funkwhale.favorites") }}</h2>
113
250
  {% if favorites and favorites.length > 0 %}
114
- <ul class="funkwhale-list">
251
+ <ul class="fw-list">
115
252
  {% for favorite in favorites %}
116
- <li class="funkwhale-list__item">
253
+ <li class="fw-list-item">
117
254
  {% if favorite.coverUrl %}
118
- <img src="{{ favorite.coverUrl }}" alt="" class="funkwhale-list__cover" loading="lazy">
255
+ <img src="{{ favorite.coverUrl }}" alt="" loading="lazy">
119
256
  {% else %}
120
- <div class="funkwhale-list__cover funkwhale-list__cover--placeholder"></div>
257
+ <div class="fw-list-item-placeholder"></div>
121
258
  {% endif %}
122
- <div class="funkwhale-list__info">
123
- <a href="{{ favorite.trackUrl }}" class="funkwhale-list__title" target="_blank" rel="noopener">
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="funkwhale-meta">{{ favorite.album }}</small>
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
- }
@@ -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 %}
@@ -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 %}