@rmdes/indiekit-endpoint-rss 1.0.8 → 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 +311 -0
- package/lib/controllers/dashboard.js +58 -28
- package/package.json +2 -1
- package/views/layouts/rss.njk +6 -0
- package/views/rss.njk +42 -375
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/* RSS endpoint styles */
|
|
2
|
+
|
|
3
|
+
/* Stats grid */
|
|
4
|
+
.rss-stats {
|
|
5
|
+
display: grid;
|
|
6
|
+
gap: var(--space-s);
|
|
7
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.rss-stat {
|
|
11
|
+
align-items: center;
|
|
12
|
+
background: var(--color-offset);
|
|
13
|
+
border-radius: var(--radius-m);
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
padding: var(--space-s);
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.rss-stat__value {
|
|
21
|
+
color: var(--color-accent);
|
|
22
|
+
font-size: var(--step-2);
|
|
23
|
+
font-weight: var(--font-weight-bold);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.rss-stat__label {
|
|
27
|
+
color: var(--color-text-secondary);
|
|
28
|
+
font-size: var(--step--2);
|
|
29
|
+
letter-spacing: 0.05em;
|
|
30
|
+
text-transform: uppercase;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Sync status bar */
|
|
34
|
+
.rss-sync-bar {
|
|
35
|
+
align-items: center;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
gap: var(--space-m);
|
|
39
|
+
justify-content: space-between;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.rss-sync-info {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: var(--space-3xs);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.rss-sync-label {
|
|
49
|
+
color: var(--color-text-secondary);
|
|
50
|
+
font-size: var(--step--2);
|
|
51
|
+
text-transform: uppercase;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.rss-sync-time {
|
|
55
|
+
font-weight: var(--font-weight-medium);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.rss-syncing {
|
|
59
|
+
align-items: center;
|
|
60
|
+
color: var(--color-accent);
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
gap: var(--space-xs);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.rss-spinner {
|
|
66
|
+
animation: rss-spin 0.8s linear infinite;
|
|
67
|
+
border: 2px solid currentColor;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
border-top-color: transparent;
|
|
70
|
+
height: 16px;
|
|
71
|
+
width: 16px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes rss-spin {
|
|
75
|
+
to { transform: rotate(360deg); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Add feed form */
|
|
79
|
+
.rss-add-form {
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: var(--space-xs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.rss-add-form input {
|
|
85
|
+
appearance: none;
|
|
86
|
+
background-color: var(--color-background);
|
|
87
|
+
border: 1px solid var(--color-border);
|
|
88
|
+
border-radius: var(--radius-s);
|
|
89
|
+
flex: 1;
|
|
90
|
+
font-size: var(--step--1);
|
|
91
|
+
padding: var(--space-2xs) var(--space-s);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.rss-add-form input:focus {
|
|
95
|
+
border-color: var(--color-accent);
|
|
96
|
+
outline: 2px solid var(--color-accent);
|
|
97
|
+
outline-offset: 1px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Feed list */
|
|
101
|
+
.rss-feed-list {
|
|
102
|
+
list-style: none;
|
|
103
|
+
margin: 0;
|
|
104
|
+
padding: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.rss-feed-item {
|
|
108
|
+
align-items: center;
|
|
109
|
+
border-block-end: 1px solid var(--color-border);
|
|
110
|
+
display: flex;
|
|
111
|
+
gap: var(--space-s);
|
|
112
|
+
padding: var(--space-s) 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.rss-feed-item:last-child {
|
|
116
|
+
border-block-end: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.rss-feed-icon {
|
|
120
|
+
border-radius: var(--radius-s);
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
height: 32px;
|
|
123
|
+
object-fit: cover;
|
|
124
|
+
width: 32px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.rss-feed-icon-placeholder {
|
|
128
|
+
align-items: center;
|
|
129
|
+
background: var(--color-border);
|
|
130
|
+
border-radius: var(--radius-s);
|
|
131
|
+
color: var(--color-text-secondary);
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-shrink: 0;
|
|
134
|
+
font-size: var(--step-0);
|
|
135
|
+
height: 32px;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
width: 32px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.rss-feed-info {
|
|
141
|
+
flex: 1;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.rss-feed-title {
|
|
146
|
+
font-weight: var(--font-weight-medium);
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
text-overflow: ellipsis;
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.rss-feed-meta {
|
|
153
|
+
color: var(--color-text-secondary);
|
|
154
|
+
display: flex;
|
|
155
|
+
font-size: var(--step--2);
|
|
156
|
+
gap: var(--space-xs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.rss-feed-actions {
|
|
160
|
+
align-items: center;
|
|
161
|
+
display: flex;
|
|
162
|
+
gap: var(--space-xs);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.rss-feed-error {
|
|
166
|
+
color: #d32f2f;
|
|
167
|
+
font-size: var(--step--2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Item list */
|
|
171
|
+
.rss-item-list {
|
|
172
|
+
list-style: none;
|
|
173
|
+
margin: 0;
|
|
174
|
+
padding: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.rss-item {
|
|
178
|
+
border-block-end: 1px solid var(--color-border);
|
|
179
|
+
display: flex;
|
|
180
|
+
gap: var(--space-s);
|
|
181
|
+
padding: var(--space-s) 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.rss-item:last-child {
|
|
185
|
+
border-block-end: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.rss-item-image {
|
|
189
|
+
border-radius: var(--radius-s);
|
|
190
|
+
flex-shrink: 0;
|
|
191
|
+
height: 60px;
|
|
192
|
+
object-fit: cover;
|
|
193
|
+
width: 60px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.rss-item-placeholder {
|
|
197
|
+
background: var(--color-border);
|
|
198
|
+
border-radius: var(--radius-s);
|
|
199
|
+
flex-shrink: 0;
|
|
200
|
+
height: 60px;
|
|
201
|
+
width: 60px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.rss-item-info {
|
|
205
|
+
flex: 1;
|
|
206
|
+
min-width: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.rss-item-title {
|
|
210
|
+
color: inherit;
|
|
211
|
+
display: block;
|
|
212
|
+
font-weight: var(--font-weight-medium);
|
|
213
|
+
overflow: hidden;
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
text-overflow: ellipsis;
|
|
216
|
+
white-space: nowrap;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.rss-item-title:hover {
|
|
220
|
+
text-decoration: underline;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.rss-item-description {
|
|
224
|
+
color: var(--color-text-secondary);
|
|
225
|
+
display: -webkit-box;
|
|
226
|
+
font-size: var(--step--1);
|
|
227
|
+
margin: var(--space-3xs) 0;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
-webkit-box-orient: vertical;
|
|
230
|
+
-webkit-line-clamp: 2;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.rss-item-meta {
|
|
234
|
+
color: var(--color-text-secondary);
|
|
235
|
+
font-size: var(--step--2);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Toggle switch */
|
|
239
|
+
.rss-toggle {
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
height: 22px;
|
|
242
|
+
position: relative;
|
|
243
|
+
width: 40px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.rss-toggle input {
|
|
247
|
+
height: 0;
|
|
248
|
+
opacity: 0;
|
|
249
|
+
width: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.rss-toggle-slider {
|
|
253
|
+
background-color: #ccc;
|
|
254
|
+
border-radius: 22px;
|
|
255
|
+
bottom: 0;
|
|
256
|
+
left: 0;
|
|
257
|
+
position: absolute;
|
|
258
|
+
right: 0;
|
|
259
|
+
top: 0;
|
|
260
|
+
transition: 0.3s;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.rss-toggle-slider::before {
|
|
264
|
+
background-color: white;
|
|
265
|
+
border-radius: 50%;
|
|
266
|
+
bottom: 3px;
|
|
267
|
+
content: "";
|
|
268
|
+
height: 16px;
|
|
269
|
+
left: 3px;
|
|
270
|
+
position: absolute;
|
|
271
|
+
transition: 0.3s;
|
|
272
|
+
width: 16px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.rss-toggle input:checked + .rss-toggle-slider {
|
|
276
|
+
background-color: var(--color-accent);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.rss-toggle input:checked + .rss-toggle-slider::before {
|
|
280
|
+
transform: translateX(18px);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Delete button */
|
|
284
|
+
.rss-delete-btn {
|
|
285
|
+
background: none;
|
|
286
|
+
border: none;
|
|
287
|
+
color: #d32f2f;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
font-size: var(--step-0);
|
|
290
|
+
opacity: 0.6;
|
|
291
|
+
padding: var(--space-3xs);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.rss-delete-btn:hover {
|
|
295
|
+
opacity: 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Public link */
|
|
299
|
+
.rss-public-link {
|
|
300
|
+
align-items: center;
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-wrap: wrap;
|
|
303
|
+
gap: var(--space-m);
|
|
304
|
+
justify-content: space-between;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.rss-public-link p {
|
|
308
|
+
color: var(--color-text-secondary);
|
|
309
|
+
font-size: var(--step--1);
|
|
310
|
+
margin: 0;
|
|
311
|
+
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { getSyncState, runSync } from "../sync.js";
|
|
2
2
|
import { formatFeed, formatItem } from "../utils.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Extract and clear flash messages from session
|
|
6
|
+
* Returns { success, error } for Indiekit's native notificationBanner
|
|
7
|
+
*/
|
|
8
|
+
function consumeFlashMessage(request) {
|
|
9
|
+
const result = {};
|
|
10
|
+
if (request.session?.messages?.length) {
|
|
11
|
+
const msg = request.session.messages[0];
|
|
12
|
+
if (msg.type === "success") result.success = msg.content;
|
|
13
|
+
else if (msg.type === "error" || msg.type === "warning")
|
|
14
|
+
result.error = msg.content;
|
|
15
|
+
request.session.messages = null;
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
export const dashboardController = {
|
|
5
21
|
/**
|
|
6
22
|
* Render admin dashboard
|
|
@@ -41,6 +57,9 @@ export const dashboardController = {
|
|
|
41
57
|
|
|
42
58
|
const syncState = getSyncState();
|
|
43
59
|
|
|
60
|
+
// Extract flash messages for native Indiekit notification banner
|
|
61
|
+
const flash = consumeFlashMessage(request);
|
|
62
|
+
|
|
44
63
|
response.render("rss", {
|
|
45
64
|
title: response.__("rss.title"),
|
|
46
65
|
feeds: feeds.map(formatFeed),
|
|
@@ -54,12 +73,7 @@ export const dashboardController = {
|
|
|
54
73
|
},
|
|
55
74
|
mountPath: request.baseUrl,
|
|
56
75
|
publicUrl: rssEndpoint,
|
|
57
|
-
|
|
58
|
-
syncedFeeds: request.query.feeds,
|
|
59
|
-
syncedItems: request.query.items,
|
|
60
|
-
syncedPruned: request.query.pruned,
|
|
61
|
-
syncInProgress: request.query.syncing,
|
|
62
|
-
queryError: request.query.error,
|
|
76
|
+
...flash,
|
|
63
77
|
});
|
|
64
78
|
} catch (error) {
|
|
65
79
|
next(error);
|
|
@@ -128,45 +142,61 @@ export const dashboardController = {
|
|
|
128
142
|
const { rssConfig, getRssDb } = request.app.locals.application;
|
|
129
143
|
|
|
130
144
|
if (!rssConfig) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
request.session.messages = [
|
|
146
|
+
{ type: "error", content: "Not configured" },
|
|
147
|
+
];
|
|
148
|
+
return response.redirect(request.baseUrl);
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
const db = getRssDb?.();
|
|
137
152
|
if (!db) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
153
|
+
request.session.messages = [
|
|
154
|
+
{ type: "error", content: "Database not available" },
|
|
155
|
+
];
|
|
156
|
+
return response.redirect(request.baseUrl);
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
const syncState = getSyncState();
|
|
146
160
|
if (syncState.syncing) {
|
|
147
|
-
|
|
161
|
+
request.session.messages = [
|
|
162
|
+
{ type: "warning", content: "A sync is already in progress" },
|
|
163
|
+
];
|
|
164
|
+
return response.redirect(request.baseUrl);
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
const result = await runSync(db, rssConfig);
|
|
151
168
|
|
|
152
169
|
if (result.error) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
request.session.messages = [
|
|
171
|
+
{ type: "error", content: result.error },
|
|
172
|
+
];
|
|
173
|
+
return response.redirect(request.baseUrl);
|
|
156
174
|
}
|
|
157
175
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const itemsAdded = result.itemsAdded || 0;
|
|
177
|
+
const feedsProcessed = result.feedsProcessed || 0;
|
|
178
|
+
const itemsPruned = result.itemsPruned || 0;
|
|
179
|
+
|
|
180
|
+
let message;
|
|
181
|
+
if (itemsAdded > 0) {
|
|
182
|
+
message = `Synced ${itemsAdded} new items from ${feedsProcessed} feeds`;
|
|
183
|
+
if (itemsPruned > 0) {
|
|
184
|
+
message += ` (${itemsPruned} old items pruned)`;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
message = "Feeds are up to date, nothing new to sync";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
request.session.messages = [
|
|
191
|
+
{ type: "success", content: message },
|
|
192
|
+
];
|
|
193
|
+
response.redirect(request.baseUrl);
|
|
165
194
|
} catch (error) {
|
|
166
195
|
console.error("[RSS] Sync error:", error.message);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
196
|
+
request.session.messages = [
|
|
197
|
+
{ type: "error", content: error.message },
|
|
198
|
+
];
|
|
199
|
+
response.redirect(request.baseUrl);
|
|
170
200
|
}
|
|
171
201
|
},
|
|
172
202
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-rss",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "RSS feed reader endpoint for Indiekit. Aggregates multiple feeds, caches in MongoDB, displays on frontend.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
".": "./index.js"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
+
"assets",
|
|
37
38
|
"lib",
|
|
38
39
|
"locales",
|
|
39
40
|
"views",
|
package/views/rss.njk
CHANGED
|
@@ -1,346 +1,9 @@
|
|
|
1
|
-
{% extends "
|
|
1
|
+
{% extends "layouts/rss.njk" %}
|
|
2
2
|
|
|
3
|
-
{% block
|
|
4
|
-
<style>
|
|
5
|
-
.rss-dashboard {
|
|
6
|
-
display: flex;
|
|
7
|
-
flex-direction: column;
|
|
8
|
-
gap: var(--space-xl, 2rem);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.rss-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
|
-
.rss-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
|
-
/* Notifications */
|
|
25
|
-
.rss-notification {
|
|
26
|
-
border-radius: var(--border-radius-small, 0.25rem);
|
|
27
|
-
padding: var(--space-s, 0.75rem) var(--space-m, 1rem);
|
|
28
|
-
margin-block-end: var(--space-s, 0.75rem);
|
|
29
|
-
}
|
|
30
|
-
.rss-notification--success {
|
|
31
|
-
background: var(--color-success, #d4edda);
|
|
32
|
-
color: var(--color-on-success, #155724);
|
|
33
|
-
}
|
|
34
|
-
.rss-notification--warning {
|
|
35
|
-
background: #fff3cd;
|
|
36
|
-
color: #856404;
|
|
37
|
-
}
|
|
38
|
-
.rss-notification--error {
|
|
39
|
-
background: var(--color-error, #f8d7da);
|
|
40
|
-
color: var(--color-on-error, #721c24);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Stats grid */
|
|
44
|
-
.rss-stats-grid {
|
|
45
|
-
display: grid;
|
|
46
|
-
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
47
|
-
gap: var(--space-s, 0.75rem);
|
|
48
|
-
}
|
|
49
|
-
.rss-stat {
|
|
50
|
-
display: flex;
|
|
51
|
-
flex-direction: column;
|
|
52
|
-
align-items: center;
|
|
53
|
-
padding: var(--space-s, 0.75rem);
|
|
54
|
-
background: var(--color-background, #fff);
|
|
55
|
-
border-radius: var(--border-radius-small, 0.5rem);
|
|
56
|
-
text-align: center;
|
|
57
|
-
}
|
|
58
|
-
.rss-stat-value {
|
|
59
|
-
font-size: 1.5rem;
|
|
60
|
-
font-weight: 700;
|
|
61
|
-
color: var(--color-accent, #ff6600);
|
|
62
|
-
}
|
|
63
|
-
.rss-stat-label {
|
|
64
|
-
font-size: 0.75rem;
|
|
65
|
-
color: var(--color-on-offset, #666);
|
|
66
|
-
text-transform: uppercase;
|
|
67
|
-
letter-spacing: 0.05em;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* Sync status */
|
|
71
|
-
.rss-sync-bar {
|
|
72
|
-
display: flex;
|
|
73
|
-
align-items: center;
|
|
74
|
-
justify-content: space-between;
|
|
75
|
-
gap: var(--space-m, 1rem);
|
|
76
|
-
flex-wrap: wrap;
|
|
77
|
-
}
|
|
78
|
-
.rss-sync-info {
|
|
79
|
-
display: flex;
|
|
80
|
-
flex-direction: column;
|
|
81
|
-
gap: 0.25rem;
|
|
82
|
-
}
|
|
83
|
-
.rss-sync-label {
|
|
84
|
-
font-size: 0.75rem;
|
|
85
|
-
color: var(--color-on-offset, #666);
|
|
86
|
-
text-transform: uppercase;
|
|
87
|
-
}
|
|
88
|
-
.rss-sync-time {
|
|
89
|
-
font-weight: 500;
|
|
90
|
-
}
|
|
91
|
-
.rss-syncing {
|
|
92
|
-
display: inline-flex;
|
|
93
|
-
align-items: center;
|
|
94
|
-
gap: 0.5rem;
|
|
95
|
-
color: var(--color-accent, #ff6600);
|
|
96
|
-
}
|
|
97
|
-
.rss-spinner {
|
|
98
|
-
width: 16px;
|
|
99
|
-
height: 16px;
|
|
100
|
-
border: 2px solid currentColor;
|
|
101
|
-
border-top-color: transparent;
|
|
102
|
-
border-radius: 50%;
|
|
103
|
-
animation: rss-spin 0.8s linear infinite;
|
|
104
|
-
}
|
|
105
|
-
@keyframes rss-spin {
|
|
106
|
-
to { transform: rotate(360deg); }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* Add feed form */
|
|
110
|
-
.rss-add-form {
|
|
111
|
-
display: flex;
|
|
112
|
-
gap: 0.5rem;
|
|
113
|
-
}
|
|
114
|
-
.rss-add-form input {
|
|
115
|
-
flex: 1;
|
|
116
|
-
appearance: none;
|
|
117
|
-
background-color: var(--color-background, #fff);
|
|
118
|
-
border: 1px solid var(--color-outline-variant, #ccc);
|
|
119
|
-
border-radius: var(--border-radius-small, 0.25rem);
|
|
120
|
-
font: var(--font-body, 0.875rem/1.4 sans-serif);
|
|
121
|
-
padding: calc(var(--space-s, 0.75rem) / 2) var(--space-s, 0.75rem);
|
|
122
|
-
}
|
|
123
|
-
.rss-add-form input:focus {
|
|
124
|
-
border-color: var(--color-primary, #0066cc);
|
|
125
|
-
outline: 2px solid var(--color-primary, #0066cc);
|
|
126
|
-
outline-offset: 1px;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/* Feed list */
|
|
130
|
-
.rss-feed-list {
|
|
131
|
-
list-style: none;
|
|
132
|
-
padding: 0;
|
|
133
|
-
margin: 0;
|
|
134
|
-
}
|
|
135
|
-
.rss-feed-item {
|
|
136
|
-
display: flex;
|
|
137
|
-
align-items: center;
|
|
138
|
-
gap: 0.75rem;
|
|
139
|
-
padding: 0.75rem 0;
|
|
140
|
-
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
141
|
-
}
|
|
142
|
-
.rss-feed-item:last-child { border-bottom: none; }
|
|
143
|
-
.rss-feed-icon {
|
|
144
|
-
width: 32px;
|
|
145
|
-
height: 32px;
|
|
146
|
-
object-fit: cover;
|
|
147
|
-
border-radius: 0.25rem;
|
|
148
|
-
flex-shrink: 0;
|
|
149
|
-
}
|
|
150
|
-
.rss-feed-icon-placeholder {
|
|
151
|
-
width: 32px;
|
|
152
|
-
height: 32px;
|
|
153
|
-
background: var(--color-outline-variant, #ddd);
|
|
154
|
-
border-radius: 0.25rem;
|
|
155
|
-
flex-shrink: 0;
|
|
156
|
-
display: flex;
|
|
157
|
-
align-items: center;
|
|
158
|
-
justify-content: center;
|
|
159
|
-
color: var(--color-on-offset, #666);
|
|
160
|
-
font-size: 1rem;
|
|
161
|
-
}
|
|
162
|
-
.rss-feed-info {
|
|
163
|
-
flex: 1;
|
|
164
|
-
min-width: 0;
|
|
165
|
-
}
|
|
166
|
-
.rss-feed-title {
|
|
167
|
-
font-weight: 500;
|
|
168
|
-
white-space: nowrap;
|
|
169
|
-
overflow: hidden;
|
|
170
|
-
text-overflow: ellipsis;
|
|
171
|
-
}
|
|
172
|
-
.rss-feed-meta {
|
|
173
|
-
display: flex;
|
|
174
|
-
gap: 0.5rem;
|
|
175
|
-
font-size: 0.75rem;
|
|
176
|
-
color: var(--color-on-offset, #666);
|
|
177
|
-
}
|
|
178
|
-
.rss-feed-actions {
|
|
179
|
-
display: flex;
|
|
180
|
-
gap: 0.5rem;
|
|
181
|
-
align-items: center;
|
|
182
|
-
}
|
|
183
|
-
.rss-feed-error {
|
|
184
|
-
color: #d32f2f;
|
|
185
|
-
font-size: 0.75rem;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* Item list */
|
|
189
|
-
.rss-item-list {
|
|
190
|
-
list-style: none;
|
|
191
|
-
padding: 0;
|
|
192
|
-
margin: 0;
|
|
193
|
-
}
|
|
194
|
-
.rss-item {
|
|
195
|
-
display: flex;
|
|
196
|
-
gap: 0.75rem;
|
|
197
|
-
padding: 0.75rem 0;
|
|
198
|
-
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
199
|
-
}
|
|
200
|
-
.rss-item:last-child { border-bottom: none; }
|
|
201
|
-
.rss-item-image {
|
|
202
|
-
width: 60px;
|
|
203
|
-
height: 60px;
|
|
204
|
-
object-fit: cover;
|
|
205
|
-
border-radius: 0.25rem;
|
|
206
|
-
flex-shrink: 0;
|
|
207
|
-
}
|
|
208
|
-
.rss-item-placeholder {
|
|
209
|
-
width: 60px;
|
|
210
|
-
height: 60px;
|
|
211
|
-
background: var(--color-outline-variant, #ddd);
|
|
212
|
-
border-radius: 0.25rem;
|
|
213
|
-
flex-shrink: 0;
|
|
214
|
-
}
|
|
215
|
-
.rss-item-info {
|
|
216
|
-
flex: 1;
|
|
217
|
-
min-width: 0;
|
|
218
|
-
}
|
|
219
|
-
.rss-item-title {
|
|
220
|
-
font-weight: 500;
|
|
221
|
-
text-decoration: none;
|
|
222
|
-
color: inherit;
|
|
223
|
-
display: block;
|
|
224
|
-
white-space: nowrap;
|
|
225
|
-
overflow: hidden;
|
|
226
|
-
text-overflow: ellipsis;
|
|
227
|
-
}
|
|
228
|
-
.rss-item-title:hover { text-decoration: underline; }
|
|
229
|
-
.rss-item-description {
|
|
230
|
-
font-size: 0.875rem;
|
|
231
|
-
color: var(--color-on-offset, #666);
|
|
232
|
-
margin: 0.25rem 0;
|
|
233
|
-
display: -webkit-box;
|
|
234
|
-
-webkit-line-clamp: 2;
|
|
235
|
-
-webkit-box-orient: vertical;
|
|
236
|
-
overflow: hidden;
|
|
237
|
-
}
|
|
238
|
-
.rss-item-meta {
|
|
239
|
-
font-size: 0.75rem;
|
|
240
|
-
color: var(--color-on-offset, #666);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/* Toggle switch */
|
|
244
|
-
.rss-toggle {
|
|
245
|
-
position: relative;
|
|
246
|
-
width: 40px;
|
|
247
|
-
height: 22px;
|
|
248
|
-
cursor: pointer;
|
|
249
|
-
}
|
|
250
|
-
.rss-toggle input {
|
|
251
|
-
opacity: 0;
|
|
252
|
-
width: 0;
|
|
253
|
-
height: 0;
|
|
254
|
-
}
|
|
255
|
-
.rss-toggle-slider {
|
|
256
|
-
position: absolute;
|
|
257
|
-
top: 0; left: 0; right: 0; bottom: 0;
|
|
258
|
-
background-color: #ccc;
|
|
259
|
-
border-radius: 22px;
|
|
260
|
-
transition: 0.3s;
|
|
261
|
-
}
|
|
262
|
-
.rss-toggle-slider:before {
|
|
263
|
-
position: absolute;
|
|
264
|
-
content: "";
|
|
265
|
-
height: 16px;
|
|
266
|
-
width: 16px;
|
|
267
|
-
left: 3px;
|
|
268
|
-
bottom: 3px;
|
|
269
|
-
background-color: white;
|
|
270
|
-
border-radius: 50%;
|
|
271
|
-
transition: 0.3s;
|
|
272
|
-
}
|
|
273
|
-
.rss-toggle input:checked + .rss-toggle-slider {
|
|
274
|
-
background-color: var(--color-accent, #ff6600);
|
|
275
|
-
}
|
|
276
|
-
.rss-toggle input:checked + .rss-toggle-slider:before {
|
|
277
|
-
transform: translateX(18px);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/* Delete button */
|
|
281
|
-
.rss-delete-btn {
|
|
282
|
-
background: none;
|
|
283
|
-
border: none;
|
|
284
|
-
color: #d32f2f;
|
|
285
|
-
cursor: pointer;
|
|
286
|
-
padding: 0.25rem;
|
|
287
|
-
font-size: 1rem;
|
|
288
|
-
opacity: 0.6;
|
|
289
|
-
}
|
|
290
|
-
.rss-delete-btn:hover { opacity: 1; }
|
|
291
|
-
|
|
292
|
-
/* Public link */
|
|
293
|
-
.rss-public-link {
|
|
294
|
-
display: flex;
|
|
295
|
-
align-items: center;
|
|
296
|
-
justify-content: space-between;
|
|
297
|
-
gap: var(--space-m, 1rem);
|
|
298
|
-
flex-wrap: wrap;
|
|
299
|
-
}
|
|
300
|
-
.rss-public-link p {
|
|
301
|
-
margin: 0;
|
|
302
|
-
color: var(--color-on-offset, #666);
|
|
303
|
-
font: var(--font-caption, 0.875rem/1.4 sans-serif);
|
|
304
|
-
}
|
|
305
|
-
</style>
|
|
306
|
-
|
|
307
|
-
{# Notifications #}
|
|
308
|
-
{% if synced %}
|
|
309
|
-
<div class="rss-notification rss-notification--success">
|
|
310
|
-
{% if syncedItems and syncedItems != "0" %}
|
|
311
|
-
Synced {{ syncedItems }} new items from {{ syncedFeeds }} feeds
|
|
312
|
-
{% else %}
|
|
313
|
-
Feeds are up to date, nothing new to sync
|
|
314
|
-
{% endif %}
|
|
315
|
-
{% if syncedPruned and syncedPruned != "0" %}
|
|
316
|
-
({{ syncedPruned }} old items pruned)
|
|
317
|
-
{% endif %}
|
|
318
|
-
</div>
|
|
319
|
-
{% endif %}
|
|
320
|
-
|
|
321
|
-
{% if syncInProgress %}
|
|
322
|
-
<div class="rss-notification rss-notification--warning">
|
|
323
|
-
A sync is already in progress, please wait
|
|
324
|
-
</div>
|
|
325
|
-
{% endif %}
|
|
326
|
-
|
|
327
|
-
{% if queryError %}
|
|
328
|
-
<div class="rss-notification rss-notification--error">
|
|
329
|
-
{{ queryError }}
|
|
330
|
-
</div>
|
|
331
|
-
{% endif %}
|
|
332
|
-
|
|
333
|
-
{% if configError %}
|
|
334
|
-
<div class="rss-notification rss-notification--error">
|
|
335
|
-
{{ configError }}
|
|
336
|
-
</div>
|
|
337
|
-
{% endif %}
|
|
338
|
-
|
|
339
|
-
<div class="rss-dashboard">
|
|
3
|
+
{% block rss %}
|
|
340
4
|
{% if not configError %}
|
|
341
5
|
{# Sync Status & Actions #}
|
|
342
|
-
|
|
343
|
-
<h2>{{ __("rss.syncStatus") }}</h2>
|
|
6
|
+
{% call section({ title: __("rss.syncStatus") }) %}
|
|
344
7
|
<div class="rss-sync-bar">
|
|
345
8
|
<div class="rss-sync-info">
|
|
346
9
|
<span class="rss-sync-label">{{ __("rss.lastSynced") }}</span>
|
|
@@ -362,41 +25,44 @@
|
|
|
362
25
|
</div>
|
|
363
26
|
<div class="button-group" style="display: flex; gap: 0.5rem;">
|
|
364
27
|
<form action="{{ mountPath }}/sync" method="post" style="display: inline;">
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
28
|
+
{{ button({
|
|
29
|
+
type: "submit",
|
|
30
|
+
text: __("rss.syncNow"),
|
|
31
|
+
disabled: syncState.syncing
|
|
32
|
+
}) }}
|
|
368
33
|
</form>
|
|
369
34
|
<form action="{{ mountPath }}/clear-resync" method="post" style="display: inline;" id="clear-resync-form">
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
35
|
+
{{ button({
|
|
36
|
+
classes: "button--secondary",
|
|
37
|
+
type: "submit",
|
|
38
|
+
text: __("rss.clearResync"),
|
|
39
|
+
disabled: syncState.syncing
|
|
40
|
+
}) }}
|
|
373
41
|
</form>
|
|
374
42
|
</div>
|
|
375
43
|
</div>
|
|
376
|
-
|
|
44
|
+
{% endcall %}
|
|
377
45
|
|
|
378
46
|
{# Stats #}
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
<div class="rss-stats-grid">
|
|
47
|
+
{% call section({ title: __("rss.status") }) %}
|
|
48
|
+
<div class="rss-stats">
|
|
382
49
|
<div class="rss-stat">
|
|
383
|
-
<span class="rss-
|
|
384
|
-
<span class="rss-
|
|
50
|
+
<span class="rss-stat__value">{{ totalFeeds }}</span>
|
|
51
|
+
<span class="rss-stat__label">{{ __("rss.feedsCount") }}</span>
|
|
385
52
|
</div>
|
|
386
53
|
<div class="rss-stat">
|
|
387
|
-
<span class="rss-
|
|
388
|
-
<span class="rss-
|
|
54
|
+
<span class="rss-stat__value">{{ totalItems }}</span>
|
|
55
|
+
<span class="rss-stat__label">{{ __("rss.itemsCount") }}</span>
|
|
389
56
|
</div>
|
|
390
57
|
<div class="rss-stat">
|
|
391
|
-
<span class="rss-
|
|
392
|
-
<span class="rss-
|
|
58
|
+
<span class="rss-stat__value">{{ feeds | selectattr("enabled") | list | length }}</span>
|
|
59
|
+
<span class="rss-stat__label">{{ __("rss.enabled") }}</span>
|
|
393
60
|
</div>
|
|
394
61
|
</div>
|
|
395
|
-
|
|
62
|
+
{% endcall %}
|
|
396
63
|
|
|
397
64
|
{# Add Feed Form #}
|
|
398
|
-
|
|
399
|
-
<h2>{{ __("rss.addFeed") }}</h2>
|
|
65
|
+
{% call section({ title: __("rss.addFeed") }) %}
|
|
400
66
|
<form class="rss-add-form" action="{{ mountPath }}/api/feeds" method="post" id="add-feed-form">
|
|
401
67
|
<input
|
|
402
68
|
type="url"
|
|
@@ -404,16 +70,16 @@
|
|
|
404
70
|
placeholder="{{ __("rss.feedUrlPlaceholder") }}"
|
|
405
71
|
required
|
|
406
72
|
>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
73
|
+
{{ button({
|
|
74
|
+
type: "submit",
|
|
75
|
+
text: __("rss.addFeed")
|
|
76
|
+
}) }}
|
|
410
77
|
</form>
|
|
411
|
-
|
|
78
|
+
{% endcall %}
|
|
412
79
|
|
|
413
80
|
{# Feeds List #}
|
|
414
81
|
{% if feeds and feeds.length > 0 %}
|
|
415
|
-
|
|
416
|
-
<h2>{{ __("rss.feeds") }}</h2>
|
|
82
|
+
{% call section({ title: __("rss.feeds") }) %}
|
|
417
83
|
<ul class="rss-feed-list">
|
|
418
84
|
{% for feed in feeds %}
|
|
419
85
|
<li class="rss-feed-item" data-feed-id="{{ feed.id }}">
|
|
@@ -453,13 +119,12 @@
|
|
|
453
119
|
</li>
|
|
454
120
|
{% endfor %}
|
|
455
121
|
</ul>
|
|
456
|
-
|
|
122
|
+
{% endcall %}
|
|
457
123
|
{% endif %}
|
|
458
124
|
|
|
459
125
|
{# Recent Items #}
|
|
460
126
|
{% if recentItems and recentItems.length > 0 %}
|
|
461
|
-
|
|
462
|
-
<h2>{{ __("rss.recentItems") }}</h2>
|
|
127
|
+
{% call section({ title: __("rss.recentItems") }) %}
|
|
463
128
|
<ul class="rss-item-list">
|
|
464
129
|
{% for item in recentItems %}
|
|
465
130
|
<li class="rss-item">
|
|
@@ -485,20 +150,22 @@
|
|
|
485
150
|
</li>
|
|
486
151
|
{% endfor %}
|
|
487
152
|
</ul>
|
|
488
|
-
|
|
153
|
+
{% endcall %}
|
|
489
154
|
{% endif %}
|
|
490
155
|
|
|
491
156
|
{# Public Page Link #}
|
|
492
|
-
|
|
157
|
+
{% call section({ title: __("rss.widget.title") if __("rss.widget.title") else "Public page" }) %}
|
|
493
158
|
<div class="rss-public-link">
|
|
494
159
|
<p>{{ __("rss.widget.description") }}</p>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
160
|
+
{{ button({
|
|
161
|
+
classes: "button--secondary",
|
|
162
|
+
href: "/news/",
|
|
163
|
+
text: __("rss.widget.view"),
|
|
164
|
+
target: "_blank"
|
|
165
|
+
}) }}
|
|
498
166
|
</div>
|
|
499
|
-
|
|
167
|
+
{% endcall %}
|
|
500
168
|
{% endif %}
|
|
501
|
-
</div>
|
|
502
169
|
|
|
503
170
|
<script>
|
|
504
171
|
// Handle add feed form
|