@rmdes/indiekit-endpoint-lastfm 1.0.7 → 1.0.9
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/assets/styles.css +199 -0
- package/lib/controllers/dashboard.js +42 -15
- package/package.json +2 -1
- package/views/lastfm.njk +81 -339
- package/views/layouts/lastfm.njk +6 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* Last.fm endpoint styles */
|
|
2
|
+
|
|
3
|
+
/* Settings form */
|
|
4
|
+
.lastfm-form {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--space-m);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.lastfm-field {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: var(--space-3xs);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Stats grid */
|
|
17
|
+
.lastfm-stats {
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: var(--space-s);
|
|
20
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.lastfm-stat {
|
|
24
|
+
align-items: center;
|
|
25
|
+
background: var(--color-offset);
|
|
26
|
+
border-radius: var(--radius-m);
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
padding: var(--space-s);
|
|
30
|
+
text-align: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.lastfm-stat__value {
|
|
34
|
+
color: var(--color-accent);
|
|
35
|
+
font-size: var(--step-2);
|
|
36
|
+
font-weight: var(--font-weight-bold);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.lastfm-stat__label {
|
|
40
|
+
color: var(--color-text-secondary);
|
|
41
|
+
font-size: var(--step--2);
|
|
42
|
+
letter-spacing: 0.05em;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Featured track */
|
|
47
|
+
.lastfm-featured {
|
|
48
|
+
align-items: flex-start;
|
|
49
|
+
background: var(--color-offset);
|
|
50
|
+
border-radius: var(--radius-m);
|
|
51
|
+
display: flex;
|
|
52
|
+
gap: var(--space-m);
|
|
53
|
+
padding: var(--space-s);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.lastfm-featured img {
|
|
57
|
+
border-radius: var(--radius-s);
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
height: 80px;
|
|
60
|
+
object-fit: cover;
|
|
61
|
+
width: 80px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.lastfm-featured__info {
|
|
65
|
+
flex: 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.lastfm-featured__title {
|
|
69
|
+
color: inherit;
|
|
70
|
+
display: block;
|
|
71
|
+
font-weight: var(--font-weight-semibold);
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.lastfm-featured__title:hover {
|
|
76
|
+
text-decoration: underline;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.lastfm-featured__album {
|
|
80
|
+
color: var(--color-text-secondary);
|
|
81
|
+
font-size: var(--step--1);
|
|
82
|
+
margin: var(--space-3xs) 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.lastfm-meta {
|
|
86
|
+
color: var(--color-text-secondary);
|
|
87
|
+
display: flex;
|
|
88
|
+
font-size: var(--step--2);
|
|
89
|
+
gap: var(--space-xs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Now playing animation */
|
|
93
|
+
.lastfm-playing {
|
|
94
|
+
align-items: center;
|
|
95
|
+
color: #d51007;
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
gap: var(--space-xs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.lastfm-bars {
|
|
101
|
+
align-items: flex-end;
|
|
102
|
+
display: flex;
|
|
103
|
+
gap: 2px;
|
|
104
|
+
height: 16px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.lastfm-bars span {
|
|
108
|
+
animation: lastfm-bar 0.5s infinite ease-in-out alternate;
|
|
109
|
+
background: currentColor;
|
|
110
|
+
width: 3px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.lastfm-bars span:nth-child(2) {
|
|
114
|
+
animation-delay: 0.2s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.lastfm-bars span:nth-child(3) {
|
|
118
|
+
animation-delay: 0.4s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@keyframes lastfm-bar {
|
|
122
|
+
from { height: 4px; }
|
|
123
|
+
to { height: 16px; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Loved indicator */
|
|
127
|
+
.lastfm-loved {
|
|
128
|
+
color: #d51007;
|
|
129
|
+
font-size: var(--step--1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* List */
|
|
133
|
+
.lastfm-list {
|
|
134
|
+
list-style: none;
|
|
135
|
+
margin: 0;
|
|
136
|
+
padding: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.lastfm-list__item {
|
|
140
|
+
align-items: center;
|
|
141
|
+
border-block-end: 1px solid var(--color-border);
|
|
142
|
+
display: flex;
|
|
143
|
+
gap: var(--space-s);
|
|
144
|
+
padding: var(--space-s) 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.lastfm-list__item:last-child {
|
|
148
|
+
border-block-end: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.lastfm-list__item img {
|
|
152
|
+
border-radius: var(--radius-s);
|
|
153
|
+
flex-shrink: 0;
|
|
154
|
+
height: 48px;
|
|
155
|
+
object-fit: cover;
|
|
156
|
+
width: 48px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.lastfm-list__placeholder {
|
|
160
|
+
background: var(--color-border);
|
|
161
|
+
border-radius: var(--radius-s);
|
|
162
|
+
flex-shrink: 0;
|
|
163
|
+
height: 48px;
|
|
164
|
+
width: 48px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.lastfm-list__info {
|
|
168
|
+
flex: 1;
|
|
169
|
+
min-width: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.lastfm-list__title {
|
|
173
|
+
color: inherit;
|
|
174
|
+
display: block;
|
|
175
|
+
font-weight: var(--font-weight-medium);
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
text-overflow: ellipsis;
|
|
179
|
+
white-space: nowrap;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.lastfm-list__title:hover {
|
|
183
|
+
text-decoration: underline;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Public link */
|
|
187
|
+
.lastfm-public-link {
|
|
188
|
+
align-items: center;
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-wrap: wrap;
|
|
191
|
+
gap: var(--space-m);
|
|
192
|
+
justify-content: space-between;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.lastfm-public-link p {
|
|
196
|
+
color: var(--color-text-secondary);
|
|
197
|
+
font-size: var(--step--1);
|
|
198
|
+
margin: 0;
|
|
199
|
+
}
|
|
@@ -3,6 +3,22 @@ import { getEffectiveConfig } from "../config.js";
|
|
|
3
3
|
import { runSync, getCachedStats, refreshStatsCache } from "../sync.js";
|
|
4
4
|
import * as utils from "../utils.js";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Extract and clear flash messages from session
|
|
8
|
+
* Returns { success, error } for Indiekit's native notificationBanner
|
|
9
|
+
*/
|
|
10
|
+
function consumeFlashMessage(request) {
|
|
11
|
+
const result = {};
|
|
12
|
+
if (request.session?.messages?.length) {
|
|
13
|
+
const msg = request.session.messages[0];
|
|
14
|
+
if (msg.type === "success") result.success = msg.content;
|
|
15
|
+
else if (msg.type === "error" || msg.type === "warning")
|
|
16
|
+
result.error = msg.content;
|
|
17
|
+
request.session.messages = null;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
/**
|
|
7
23
|
* Dashboard controller
|
|
8
24
|
*/
|
|
@@ -27,6 +43,9 @@ export const dashboardController = {
|
|
|
27
43
|
const config = await getEffectiveConfig(db, lastfmConfig);
|
|
28
44
|
const { apiKey, username } = config;
|
|
29
45
|
|
|
46
|
+
// Extract flash messages for native Indiekit notification banner
|
|
47
|
+
const flash = consumeFlashMessage(request);
|
|
48
|
+
|
|
30
49
|
// If no credentials, show settings form only
|
|
31
50
|
if (!apiKey || !username) {
|
|
32
51
|
return response.render("lastfm", {
|
|
@@ -34,6 +53,7 @@ export const dashboardController = {
|
|
|
34
53
|
configError: response.__("lastfm.error.noConfig"),
|
|
35
54
|
settings: { apiKey: "", username: "" },
|
|
36
55
|
mountPath: request.baseUrl,
|
|
56
|
+
...flash,
|
|
37
57
|
});
|
|
38
58
|
}
|
|
39
59
|
|
|
@@ -75,6 +95,7 @@ export const dashboardController = {
|
|
|
75
95
|
configError: response.__("lastfm.error.connection"),
|
|
76
96
|
settings: { apiKey, username },
|
|
77
97
|
mountPath: request.baseUrl,
|
|
98
|
+
...flash,
|
|
78
99
|
});
|
|
79
100
|
}
|
|
80
101
|
|
|
@@ -105,9 +126,7 @@ export const dashboardController = {
|
|
|
105
126
|
publicUrl,
|
|
106
127
|
mountPath: request.baseUrl,
|
|
107
128
|
settings: { apiKey, username },
|
|
108
|
-
|
|
109
|
-
saved: request.query.saved,
|
|
110
|
-
queryError: request.query.error,
|
|
129
|
+
...flash,
|
|
111
130
|
});
|
|
112
131
|
} catch (error) {
|
|
113
132
|
console.error("[Last.fm] Dashboard error:", error);
|
|
@@ -144,12 +163,16 @@ export const dashboardController = {
|
|
|
144
163
|
);
|
|
145
164
|
|
|
146
165
|
console.log("[Last.fm] Settings saved");
|
|
147
|
-
|
|
166
|
+
request.session.messages = [
|
|
167
|
+
{ type: "success", content: request.__("lastfm.settingsSaved") },
|
|
168
|
+
];
|
|
169
|
+
response.redirect(request.baseUrl);
|
|
148
170
|
} catch (error) {
|
|
149
171
|
console.error("[Last.fm] Settings save error:", error);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
request.session.messages = [
|
|
173
|
+
{ type: "error", content: error.message },
|
|
174
|
+
];
|
|
175
|
+
response.redirect(request.baseUrl);
|
|
153
176
|
}
|
|
154
177
|
},
|
|
155
178
|
|
|
@@ -176,17 +199,21 @@ export const dashboardController = {
|
|
|
176
199
|
const syncOptions = { ...lastfmConfig, ...config };
|
|
177
200
|
|
|
178
201
|
// Build a minimal Indiekit-like object for runSync
|
|
179
|
-
const result = await runSync(
|
|
180
|
-
{ database: db },
|
|
181
|
-
syncOptions,
|
|
182
|
-
);
|
|
202
|
+
const result = await runSync({ database: db }, syncOptions);
|
|
183
203
|
|
|
184
|
-
|
|
204
|
+
request.session.messages = [
|
|
205
|
+
{
|
|
206
|
+
type: "success",
|
|
207
|
+
content: `Synced ${result.synced || 0} new scrobbles`,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
response.redirect(request.baseUrl);
|
|
185
211
|
} catch (error) {
|
|
186
212
|
console.error("[Last.fm] Manual sync error:", error);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
request.session.messages = [
|
|
214
|
+
{ type: "error", content: error.message },
|
|
215
|
+
];
|
|
216
|
+
response.redirect(request.baseUrl);
|
|
190
217
|
}
|
|
191
218
|
},
|
|
192
219
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-lastfm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Last.fm scrobble and listening activity endpoint for Indiekit. Display listening history, loved tracks, and statistics.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
".": "./index.js"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
|
+
"assets",
|
|
38
39
|
"includes",
|
|
39
40
|
"lib",
|
|
40
41
|
"locales",
|
package/views/lastfm.njk
CHANGED
|
@@ -1,414 +1,156 @@
|
|
|
1
|
-
{% extends "
|
|
1
|
+
{% extends "layouts/lastfm.njk" %}
|
|
2
2
|
|
|
3
|
-
{% block
|
|
4
|
-
<style>
|
|
5
|
-
.lfm-dashboard {
|
|
6
|
-
display: flex;
|
|
7
|
-
flex-direction: column;
|
|
8
|
-
gap: var(--space-xl, 2rem);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.lfm-section {
|
|
12
|
-
background: var(--color-offset, #f5f5f5);
|
|
13
|
-
border-radius: var(--border-radius-small, 0.5rem);
|
|
14
|
-
padding: var(--space-m, 1.5rem);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.lfm-section h2 {
|
|
18
|
-
font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
|
|
19
|
-
margin-block-end: var(--space-s, 0.75rem);
|
|
20
|
-
padding-block-end: var(--space-xs, 0.5rem);
|
|
21
|
-
border-block-end: 1px solid var(--color-outline-variant, #ddd);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.lfm-section p.lfm-hint {
|
|
25
|
-
color: var(--color-on-offset, #666);
|
|
26
|
-
font: var(--font-caption, 0.875rem/1.4 sans-serif);
|
|
27
|
-
margin-block-end: var(--space-m, 1rem);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* Notifications */
|
|
31
|
-
.lfm-notification {
|
|
32
|
-
border-radius: var(--border-radius-small, 0.25rem);
|
|
33
|
-
padding: var(--space-s, 0.75rem) var(--space-m, 1rem);
|
|
34
|
-
margin-block-end: var(--space-s, 0.75rem);
|
|
35
|
-
}
|
|
36
|
-
.lfm-notification--success {
|
|
37
|
-
background: var(--color-success, #d4edda);
|
|
38
|
-
color: var(--color-on-success, #155724);
|
|
39
|
-
}
|
|
40
|
-
.lfm-notification--error {
|
|
41
|
-
background: var(--color-error, #f8d7da);
|
|
42
|
-
color: var(--color-on-error, #721c24);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* Settings form */
|
|
46
|
-
.lfm-form {
|
|
47
|
-
display: flex;
|
|
48
|
-
flex-direction: column;
|
|
49
|
-
gap: var(--space-m, 1rem);
|
|
50
|
-
}
|
|
51
|
-
.lfm-field {
|
|
52
|
-
display: flex;
|
|
53
|
-
flex-direction: column;
|
|
54
|
-
gap: var(--space-2xs, 0.25rem);
|
|
55
|
-
}
|
|
56
|
-
.lfm-field label {
|
|
57
|
-
font: var(--font-label, bold 0.875rem/1.4 sans-serif);
|
|
58
|
-
}
|
|
59
|
-
.lfm-field .lfm-field-hint {
|
|
60
|
-
color: var(--color-on-offset, #666);
|
|
61
|
-
font: var(--font-caption, 0.875rem/1.4 sans-serif);
|
|
62
|
-
}
|
|
63
|
-
.lfm-field input {
|
|
64
|
-
appearance: none;
|
|
65
|
-
background-color: var(--color-background, #fff);
|
|
66
|
-
border: 1px solid var(--color-outline-variant, #ccc);
|
|
67
|
-
border-radius: var(--border-radius-small, 0.25rem);
|
|
68
|
-
font: var(--font-body, 0.875rem/1.4 sans-serif);
|
|
69
|
-
padding: calc(var(--space-s, 0.75rem) / 2) var(--space-s, 0.75rem);
|
|
70
|
-
width: 100%;
|
|
71
|
-
}
|
|
72
|
-
.lfm-field input:focus {
|
|
73
|
-
border-color: var(--color-primary, #0066cc);
|
|
74
|
-
outline: 2px solid var(--color-primary, #0066cc);
|
|
75
|
-
outline-offset: 1px;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/* Stats grid */
|
|
79
|
-
.lfm-stats-grid {
|
|
80
|
-
display: grid;
|
|
81
|
-
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
82
|
-
gap: var(--space-s, 0.75rem);
|
|
83
|
-
}
|
|
84
|
-
.lfm-stat {
|
|
85
|
-
display: flex;
|
|
86
|
-
flex-direction: column;
|
|
87
|
-
align-items: center;
|
|
88
|
-
padding: var(--space-s, 0.75rem);
|
|
89
|
-
background: var(--color-background, #fff);
|
|
90
|
-
border-radius: var(--border-radius-small, 0.5rem);
|
|
91
|
-
text-align: center;
|
|
92
|
-
}
|
|
93
|
-
.lfm-stat-value {
|
|
94
|
-
font-size: 1.5rem;
|
|
95
|
-
font-weight: 700;
|
|
96
|
-
color: var(--color-accent, #d51007);
|
|
97
|
-
}
|
|
98
|
-
.lfm-stat-label {
|
|
99
|
-
font-size: 0.75rem;
|
|
100
|
-
color: var(--color-on-offset, #666);
|
|
101
|
-
text-transform: uppercase;
|
|
102
|
-
letter-spacing: 0.05em;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* Featured track */
|
|
106
|
-
.lfm-featured {
|
|
107
|
-
display: flex;
|
|
108
|
-
gap: 1rem;
|
|
109
|
-
padding: var(--space-s, 0.75rem);
|
|
110
|
-
background: var(--color-background, #fff);
|
|
111
|
-
border-radius: var(--border-radius-small, 0.5rem);
|
|
112
|
-
align-items: flex-start;
|
|
113
|
-
}
|
|
114
|
-
.lfm-featured img {
|
|
115
|
-
width: 80px;
|
|
116
|
-
height: 80px;
|
|
117
|
-
object-fit: cover;
|
|
118
|
-
border-radius: 0.25rem;
|
|
119
|
-
flex-shrink: 0;
|
|
120
|
-
}
|
|
121
|
-
.lfm-featured-info { flex: 1; }
|
|
122
|
-
.lfm-featured-title {
|
|
123
|
-
font-weight: 600;
|
|
124
|
-
text-decoration: none;
|
|
125
|
-
color: inherit;
|
|
126
|
-
display: block;
|
|
127
|
-
}
|
|
128
|
-
.lfm-featured-title:hover { text-decoration: underline; }
|
|
129
|
-
.lfm-featured-album {
|
|
130
|
-
margin: 0.25rem 0;
|
|
131
|
-
color: var(--color-on-offset, #666);
|
|
132
|
-
font-size: 0.875rem;
|
|
133
|
-
}
|
|
134
|
-
.lfm-meta {
|
|
135
|
-
display: flex;
|
|
136
|
-
gap: 0.5rem;
|
|
137
|
-
color: var(--color-on-offset, #666);
|
|
138
|
-
font-size: 0.75rem;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* Now playing animation */
|
|
142
|
-
.lfm-playing-status {
|
|
143
|
-
display: inline-flex;
|
|
144
|
-
align-items: center;
|
|
145
|
-
gap: 0.5rem;
|
|
146
|
-
color: #d51007;
|
|
147
|
-
}
|
|
148
|
-
.lfm-bars {
|
|
149
|
-
display: flex;
|
|
150
|
-
gap: 2px;
|
|
151
|
-
align-items: flex-end;
|
|
152
|
-
height: 16px;
|
|
153
|
-
}
|
|
154
|
-
.lfm-bars span {
|
|
155
|
-
width: 3px;
|
|
156
|
-
background: currentColor;
|
|
157
|
-
animation: lfm-bar 0.5s infinite ease-in-out alternate;
|
|
158
|
-
}
|
|
159
|
-
.lfm-bars span:nth-child(2) { animation-delay: 0.2s; }
|
|
160
|
-
.lfm-bars span:nth-child(3) { animation-delay: 0.4s; }
|
|
161
|
-
@keyframes lfm-bar {
|
|
162
|
-
from { height: 4px; }
|
|
163
|
-
to { height: 16px; }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/* Loved indicator */
|
|
167
|
-
.lfm-loved {
|
|
168
|
-
color: #d51007;
|
|
169
|
-
font-size: 0.875rem;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/* List */
|
|
173
|
-
.lfm-list {
|
|
174
|
-
list-style: none;
|
|
175
|
-
padding: 0;
|
|
176
|
-
margin: 0;
|
|
177
|
-
}
|
|
178
|
-
.lfm-list-item {
|
|
179
|
-
display: flex;
|
|
180
|
-
align-items: center;
|
|
181
|
-
gap: 0.75rem;
|
|
182
|
-
padding: 0.75rem 0;
|
|
183
|
-
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
184
|
-
}
|
|
185
|
-
.lfm-list-item:last-child { border-bottom: none; }
|
|
186
|
-
.lfm-list-item img {
|
|
187
|
-
width: 48px;
|
|
188
|
-
height: 48px;
|
|
189
|
-
object-fit: cover;
|
|
190
|
-
border-radius: 0.25rem;
|
|
191
|
-
flex-shrink: 0;
|
|
192
|
-
}
|
|
193
|
-
.lfm-list-item-placeholder {
|
|
194
|
-
width: 48px;
|
|
195
|
-
height: 48px;
|
|
196
|
-
background: var(--color-outline-variant, #ddd);
|
|
197
|
-
border-radius: 0.25rem;
|
|
198
|
-
flex-shrink: 0;
|
|
199
|
-
}
|
|
200
|
-
.lfm-list-info {
|
|
201
|
-
flex: 1;
|
|
202
|
-
min-width: 0;
|
|
203
|
-
}
|
|
204
|
-
.lfm-list-title {
|
|
205
|
-
display: block;
|
|
206
|
-
font-weight: 500;
|
|
207
|
-
text-decoration: none;
|
|
208
|
-
color: inherit;
|
|
209
|
-
white-space: nowrap;
|
|
210
|
-
overflow: hidden;
|
|
211
|
-
text-overflow: ellipsis;
|
|
212
|
-
}
|
|
213
|
-
.lfm-list-title:hover { text-decoration: underline; }
|
|
214
|
-
|
|
215
|
-
/* Public link */
|
|
216
|
-
.lfm-public-link {
|
|
217
|
-
display: flex;
|
|
218
|
-
align-items: center;
|
|
219
|
-
justify-content: space-between;
|
|
220
|
-
gap: var(--space-m, 1rem);
|
|
221
|
-
flex-wrap: wrap;
|
|
222
|
-
}
|
|
223
|
-
.lfm-public-link p {
|
|
224
|
-
margin: 0;
|
|
225
|
-
color: var(--color-on-offset, #666);
|
|
226
|
-
font: var(--font-caption, 0.875rem/1.4 sans-serif);
|
|
227
|
-
}
|
|
228
|
-
</style>
|
|
229
|
-
|
|
230
|
-
{% if saved %}
|
|
231
|
-
<div class="lfm-notification lfm-notification--success">
|
|
232
|
-
{{ __("lastfm.settingsSaved") }}
|
|
233
|
-
</div>
|
|
234
|
-
{% endif %}
|
|
235
|
-
|
|
236
|
-
{% if synced %}
|
|
237
|
-
<div class="lfm-notification lfm-notification--success">
|
|
238
|
-
Synced {{ synced }} new scrobbles
|
|
239
|
-
</div>
|
|
240
|
-
{% endif %}
|
|
241
|
-
|
|
242
|
-
{% if queryError %}
|
|
243
|
-
<div class="lfm-notification lfm-notification--error">
|
|
244
|
-
{{ queryError }}
|
|
245
|
-
</div>
|
|
246
|
-
{% endif %}
|
|
247
|
-
|
|
248
|
-
{% if configError %}
|
|
249
|
-
<div class="lfm-notification lfm-notification--error">
|
|
250
|
-
{{ configError }}
|
|
251
|
-
</div>
|
|
252
|
-
{% endif %}
|
|
253
|
-
|
|
254
|
-
<div class="lfm-dashboard">
|
|
3
|
+
{% block lastfm %}
|
|
255
4
|
{# Settings Section #}
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
<
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
<
|
|
263
|
-
<input type="password" id="apiKey" name="apiKey" value="{{ settings.apiKey }}" aria-describedby="apiKey-hint" placeholder="Your Last.fm API key" autocomplete="off">
|
|
5
|
+
{% call section({ title: __("lastfm.settings") }) %}
|
|
6
|
+
<p class="hint">{{ __("lastfm.settingsHelp") }}</p>
|
|
7
|
+
<form method="post" action="{{ mountPath }}/settings" class="lastfm-form">
|
|
8
|
+
<div class="lastfm-field">
|
|
9
|
+
<label class="label" for="apiKey">{{ __("lastfm.apiKey") }}</label>
|
|
10
|
+
<span class="hint" id="apiKey-hint">{{ __("lastfm.apiKeyHelp") }}</span>
|
|
11
|
+
<input class="input" type="password" id="apiKey" name="apiKey" value="{{ settings.apiKey }}" aria-describedby="apiKey-hint" placeholder="Your Last.fm API key" autocomplete="off">
|
|
264
12
|
</div>
|
|
265
|
-
<div class="
|
|
266
|
-
<label for="username">{{ __("lastfm.username") }}</label>
|
|
267
|
-
<span class="
|
|
268
|
-
<input type="text" id="username" name="username" value="{{ settings.username }}" aria-describedby="username-hint" placeholder="Last.fm username">
|
|
13
|
+
<div class="lastfm-field">
|
|
14
|
+
<label class="label" for="username">{{ __("lastfm.username") }}</label>
|
|
15
|
+
<span class="hint" id="username-hint">{{ __("lastfm.usernameHelp") }}</span>
|
|
16
|
+
<input class="input" type="text" id="username" name="username" value="{{ settings.username }}" aria-describedby="username-hint" placeholder="Last.fm username">
|
|
269
17
|
</div>
|
|
270
18
|
<div>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
19
|
+
{{ button({
|
|
20
|
+
type: "submit",
|
|
21
|
+
text: __("lastfm.saveSettings")
|
|
22
|
+
}) }}
|
|
274
23
|
</div>
|
|
275
24
|
</form>
|
|
276
|
-
|
|
25
|
+
{% endcall %}
|
|
277
26
|
|
|
278
27
|
{% if not configError %}
|
|
279
28
|
{# Now Playing / Recently Played #}
|
|
280
29
|
{% if nowPlaying %}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
{{ __("lastfm.recentlyPlayed") }}
|
|
292
|
-
{% endif %}
|
|
293
|
-
</h2>
|
|
294
|
-
<article class="lfm-featured">
|
|
30
|
+
{% call section({ title: nowPlaying.status == "now-playing" and __("lastfm.nowPlaying") or __("lastfm.recentlyPlayed") }) %}
|
|
31
|
+
{% if nowPlaying.status == "now-playing" %}
|
|
32
|
+
<p class="lastfm-playing">
|
|
33
|
+
<span class="lastfm-bars">
|
|
34
|
+
<span></span><span></span><span></span>
|
|
35
|
+
</span>
|
|
36
|
+
{{ __("lastfm.nowPlaying") }}
|
|
37
|
+
</p>
|
|
38
|
+
{% endif %}
|
|
39
|
+
<article class="lastfm-featured">
|
|
295
40
|
{% if nowPlaying.coverUrl %}
|
|
296
41
|
<img src="{{ nowPlaying.coverUrl }}" alt="" loading="lazy">
|
|
297
42
|
{% endif %}
|
|
298
|
-
<div class="
|
|
299
|
-
<a href="{{ nowPlaying.trackUrl }}" class="
|
|
43
|
+
<div class="lastfm-featured__info">
|
|
44
|
+
<a href="{{ nowPlaying.trackUrl }}" class="lastfm-featured__title" target="_blank" rel="noopener">
|
|
300
45
|
{{ nowPlaying.artist }} - {{ nowPlaying.track }}
|
|
301
46
|
</a>
|
|
302
47
|
{% if nowPlaying.album %}
|
|
303
|
-
<p class="
|
|
48
|
+
<p class="lastfm-featured__album">{{ nowPlaying.album }}</p>
|
|
304
49
|
{% endif %}
|
|
305
|
-
<small class="
|
|
306
|
-
{% if nowPlaying.loved %}<span class="
|
|
50
|
+
<small class="lastfm-meta">
|
|
51
|
+
{% if nowPlaying.loved %}<span class="lastfm-loved">♥</span>{% endif %}
|
|
307
52
|
<span>{{ nowPlaying.relativeTime }}</span>
|
|
308
53
|
</small>
|
|
309
54
|
</div>
|
|
310
55
|
</article>
|
|
311
|
-
|
|
56
|
+
{% endcall %}
|
|
312
57
|
{% endif %}
|
|
313
58
|
|
|
314
59
|
{# Quick Stats #}
|
|
315
60
|
{% if hasStats %}
|
|
316
|
-
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<span class="
|
|
321
|
-
<span class="lfm-stat-label">{{ __("lastfm.plays") }}</span>
|
|
61
|
+
{% call section({ title: __("lastfm.stats") }) %}
|
|
62
|
+
<div class="lastfm-stats">
|
|
63
|
+
<div class="lastfm-stat">
|
|
64
|
+
<span class="lastfm-stat__value">{{ totalPlays }}</span>
|
|
65
|
+
<span class="lastfm-stat__label">{{ __("lastfm.plays") }}</span>
|
|
322
66
|
</div>
|
|
323
|
-
<div class="
|
|
324
|
-
<span class="
|
|
325
|
-
<span class="
|
|
67
|
+
<div class="lastfm-stat">
|
|
68
|
+
<span class="lastfm-stat__value">{{ uniqueTracks }}</span>
|
|
69
|
+
<span class="lastfm-stat__label">{{ __("lastfm.tracks") }}</span>
|
|
326
70
|
</div>
|
|
327
|
-
<div class="
|
|
328
|
-
<span class="
|
|
329
|
-
<span class="
|
|
71
|
+
<div class="lastfm-stat">
|
|
72
|
+
<span class="lastfm-stat__value">{{ uniqueArtists }}</span>
|
|
73
|
+
<span class="lastfm-stat__label">{{ __("lastfm.artists") }}</span>
|
|
330
74
|
</div>
|
|
331
75
|
</div>
|
|
332
|
-
|
|
76
|
+
{% endcall %}
|
|
333
77
|
{% endif %}
|
|
334
78
|
|
|
335
79
|
{# Recent Scrobbles #}
|
|
336
80
|
{% if scrobbles and scrobbles.length > 0 %}
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
<ul class="lfm-list">
|
|
81
|
+
{% call section({ title: __("lastfm.scrobbles") }) %}
|
|
82
|
+
<ul class="lastfm-list">
|
|
340
83
|
{% for scrobble in scrobbles %}
|
|
341
|
-
<li class="
|
|
84
|
+
<li class="lastfm-list__item">
|
|
342
85
|
{% if scrobble.coverUrl %}
|
|
343
86
|
<img src="{{ scrobble.coverUrl }}" alt="" loading="lazy">
|
|
344
87
|
{% else %}
|
|
345
|
-
<div class="
|
|
88
|
+
<div class="lastfm-list__placeholder"></div>
|
|
346
89
|
{% endif %}
|
|
347
|
-
<div class="
|
|
348
|
-
<a href="{{ scrobble.trackUrl }}" class="
|
|
90
|
+
<div class="lastfm-list__info">
|
|
91
|
+
<a href="{{ scrobble.trackUrl }}" class="lastfm-list__title" target="_blank" rel="noopener">
|
|
349
92
|
{{ scrobble.artist }} - {{ scrobble.track }}
|
|
350
93
|
</a>
|
|
351
|
-
<small class="
|
|
352
|
-
{% if scrobble.loved %}<span class="
|
|
94
|
+
<small class="lastfm-meta">
|
|
95
|
+
{% if scrobble.loved %}<span class="lastfm-loved">♥</span>{% endif %}
|
|
353
96
|
{{ scrobble.relativeTime }}
|
|
354
97
|
</small>
|
|
355
98
|
</div>
|
|
356
99
|
</li>
|
|
357
100
|
{% endfor %}
|
|
358
101
|
</ul>
|
|
359
|
-
|
|
102
|
+
{% endcall %}
|
|
360
103
|
{% endif %}
|
|
361
104
|
|
|
362
105
|
{# Loved Tracks #}
|
|
363
106
|
{% if lovedTracks and lovedTracks.length > 0 %}
|
|
364
|
-
|
|
365
|
-
<
|
|
366
|
-
<ul class="lfm-list">
|
|
107
|
+
{% call section({ title: __("lastfm.loved") }) %}
|
|
108
|
+
<ul class="lastfm-list">
|
|
367
109
|
{% for track in lovedTracks %}
|
|
368
|
-
<li class="
|
|
110
|
+
<li class="lastfm-list__item">
|
|
369
111
|
{% if track.coverUrl %}
|
|
370
112
|
<img src="{{ track.coverUrl }}" alt="" loading="lazy">
|
|
371
113
|
{% else %}
|
|
372
|
-
<div class="
|
|
114
|
+
<div class="lastfm-list__placeholder"></div>
|
|
373
115
|
{% endif %}
|
|
374
|
-
<div class="
|
|
375
|
-
<a href="{{ track.trackUrl }}" class="
|
|
116
|
+
<div class="lastfm-list__info">
|
|
117
|
+
<a href="{{ track.trackUrl }}" class="lastfm-list__title" target="_blank" rel="noopener">
|
|
376
118
|
{{ track.artist }} - {{ track.track }}
|
|
377
119
|
</a>
|
|
378
|
-
<small class="
|
|
379
|
-
<span class="
|
|
120
|
+
<small class="lastfm-meta">
|
|
121
|
+
<span class="lastfm-loved">♥</span>
|
|
380
122
|
{{ track.relativeTime }}
|
|
381
123
|
</small>
|
|
382
124
|
</div>
|
|
383
125
|
</li>
|
|
384
126
|
{% endfor %}
|
|
385
127
|
</ul>
|
|
386
|
-
|
|
128
|
+
{% endcall %}
|
|
387
129
|
{% endif %}
|
|
388
130
|
|
|
389
131
|
{# Actions #}
|
|
390
|
-
|
|
391
|
-
<
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
</div>
|
|
399
|
-
</section>
|
|
132
|
+
{% call section({ title: __("lastfm.actions") }) %}
|
|
133
|
+
<form method="post" action="{{ mountPath }}/sync">
|
|
134
|
+
{{ button({
|
|
135
|
+
type: "submit",
|
|
136
|
+
text: __("lastfm.sync")
|
|
137
|
+
}) }}
|
|
138
|
+
</form>
|
|
139
|
+
{% endcall %}
|
|
400
140
|
|
|
401
141
|
{# Public Page Link #}
|
|
402
142
|
{% if publicUrl %}
|
|
403
|
-
|
|
404
|
-
<div class="
|
|
143
|
+
{% call section({ title: __("lastfm.widget.title") if __("lastfm.widget.title") else "Public page" }) %}
|
|
144
|
+
<div class="lastfm-public-link">
|
|
405
145
|
<p>{{ __("lastfm.widget.description") }}</p>
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
146
|
+
{{ button({
|
|
147
|
+
classes: "button--secondary",
|
|
148
|
+
href: publicUrl,
|
|
149
|
+
text: __("lastfm.widget.view"),
|
|
150
|
+
target: "_blank"
|
|
151
|
+
}) }}
|
|
409
152
|
</div>
|
|
410
|
-
|
|
153
|
+
{% endcall %}
|
|
411
154
|
{% endif %}
|
|
412
155
|
{% endif %}
|
|
413
|
-
</div>
|
|
414
156
|
{% endblock %}
|