@rmdes/indiekit-endpoint-rss 1.0.8 → 1.0.10
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 +289 -0
- package/lib/controllers/dashboard.js +58 -28
- package/package.json +2 -1
- package/views/layouts/rss.njk +6 -0
- package/views/rss.njk +44 -376
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
/* Feed list */
|
|
79
|
+
.rss-feed-list {
|
|
80
|
+
list-style: none;
|
|
81
|
+
margin: 0;
|
|
82
|
+
padding: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.rss-feed-item {
|
|
86
|
+
align-items: center;
|
|
87
|
+
border-block-end: 1px solid var(--color-border);
|
|
88
|
+
display: flex;
|
|
89
|
+
gap: var(--space-s);
|
|
90
|
+
padding: var(--space-s) 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.rss-feed-item:last-child {
|
|
94
|
+
border-block-end: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.rss-feed-icon {
|
|
98
|
+
border-radius: var(--radius-s);
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
height: 32px;
|
|
101
|
+
object-fit: cover;
|
|
102
|
+
width: 32px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.rss-feed-icon-placeholder {
|
|
106
|
+
align-items: center;
|
|
107
|
+
background: var(--color-border);
|
|
108
|
+
border-radius: var(--radius-s);
|
|
109
|
+
color: var(--color-text-secondary);
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
font-size: var(--step-0);
|
|
113
|
+
height: 32px;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
width: 32px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.rss-feed-info {
|
|
119
|
+
flex: 1;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.rss-feed-title {
|
|
124
|
+
font-weight: var(--font-weight-medium);
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
text-overflow: ellipsis;
|
|
127
|
+
white-space: nowrap;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.rss-feed-meta {
|
|
131
|
+
color: var(--color-text-secondary);
|
|
132
|
+
display: flex;
|
|
133
|
+
font-size: var(--step--2);
|
|
134
|
+
gap: var(--space-xs);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.rss-feed-actions {
|
|
138
|
+
align-items: center;
|
|
139
|
+
display: flex;
|
|
140
|
+
gap: var(--space-xs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.rss-feed-error {
|
|
144
|
+
color: #d32f2f;
|
|
145
|
+
font-size: var(--step--2);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Item list */
|
|
149
|
+
.rss-item-list {
|
|
150
|
+
list-style: none;
|
|
151
|
+
margin: 0;
|
|
152
|
+
padding: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.rss-item {
|
|
156
|
+
border-block-end: 1px solid var(--color-border);
|
|
157
|
+
display: flex;
|
|
158
|
+
gap: var(--space-s);
|
|
159
|
+
padding: var(--space-s) 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.rss-item:last-child {
|
|
163
|
+
border-block-end: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.rss-item-image {
|
|
167
|
+
border-radius: var(--radius-s);
|
|
168
|
+
flex-shrink: 0;
|
|
169
|
+
height: 60px;
|
|
170
|
+
object-fit: cover;
|
|
171
|
+
width: 60px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.rss-item-placeholder {
|
|
175
|
+
background: var(--color-border);
|
|
176
|
+
border-radius: var(--radius-s);
|
|
177
|
+
flex-shrink: 0;
|
|
178
|
+
height: 60px;
|
|
179
|
+
width: 60px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.rss-item-info {
|
|
183
|
+
flex: 1;
|
|
184
|
+
min-width: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.rss-item-title {
|
|
188
|
+
color: inherit;
|
|
189
|
+
display: block;
|
|
190
|
+
font-weight: var(--font-weight-medium);
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
text-overflow: ellipsis;
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.rss-item-title:hover {
|
|
198
|
+
text-decoration: underline;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.rss-item-description {
|
|
202
|
+
color: var(--color-text-secondary);
|
|
203
|
+
display: -webkit-box;
|
|
204
|
+
font-size: var(--step--1);
|
|
205
|
+
margin: var(--space-3xs) 0;
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
-webkit-box-orient: vertical;
|
|
208
|
+
-webkit-line-clamp: 2;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.rss-item-meta {
|
|
212
|
+
color: var(--color-text-secondary);
|
|
213
|
+
font-size: var(--step--2);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Toggle switch */
|
|
217
|
+
.rss-toggle {
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
height: 22px;
|
|
220
|
+
position: relative;
|
|
221
|
+
width: 40px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.rss-toggle input {
|
|
225
|
+
height: 0;
|
|
226
|
+
opacity: 0;
|
|
227
|
+
width: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.rss-toggle-slider {
|
|
231
|
+
background-color: #ccc;
|
|
232
|
+
border-radius: 22px;
|
|
233
|
+
bottom: 0;
|
|
234
|
+
left: 0;
|
|
235
|
+
position: absolute;
|
|
236
|
+
right: 0;
|
|
237
|
+
top: 0;
|
|
238
|
+
transition: 0.3s;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.rss-toggle-slider::before {
|
|
242
|
+
background-color: white;
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
bottom: 3px;
|
|
245
|
+
content: "";
|
|
246
|
+
height: 16px;
|
|
247
|
+
left: 3px;
|
|
248
|
+
position: absolute;
|
|
249
|
+
transition: 0.3s;
|
|
250
|
+
width: 16px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.rss-toggle input:checked + .rss-toggle-slider {
|
|
254
|
+
background-color: var(--color-accent);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.rss-toggle input:checked + .rss-toggle-slider::before {
|
|
258
|
+
transform: translateX(18px);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Delete button */
|
|
262
|
+
.rss-delete-btn {
|
|
263
|
+
background: none;
|
|
264
|
+
border: none;
|
|
265
|
+
color: #d32f2f;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
font-size: var(--step-0);
|
|
268
|
+
opacity: 0.6;
|
|
269
|
+
padding: var(--space-3xs);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.rss-delete-btn:hover {
|
|
273
|
+
opacity: 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* Public link */
|
|
277
|
+
.rss-public-link {
|
|
278
|
+
align-items: center;
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-wrap: wrap;
|
|
281
|
+
gap: var(--space-m);
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.rss-public-link p {
|
|
286
|
+
color: var(--color-text-secondary);
|
|
287
|
+
font-size: var(--step--1);
|
|
288
|
+
margin: 0;
|
|
289
|
+
}
|
|
@@ -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.10",
|
|
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,58 +25,62 @@
|
|
|
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
|
-
<
|
|
400
|
-
<form class="rss-add-form" action="{{ mountPath }}/api/feeds" method="post" id="add-feed-form">
|
|
65
|
+
{% call section({ title: __("rss.addFeed") }) %}
|
|
66
|
+
<form class="input-button-group" action="{{ mountPath }}/api/feeds" method="post" id="add-feed-form">
|
|
401
67
|
<input
|
|
68
|
+
class="input"
|
|
402
69
|
type="url"
|
|
403
70
|
name="url"
|
|
404
71
|
placeholder="{{ __("rss.feedUrlPlaceholder") }}"
|
|
405
72
|
required
|
|
406
73
|
>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
74
|
+
{{ button({
|
|
75
|
+
type: "submit",
|
|
76
|
+
text: __("rss.addFeed")
|
|
77
|
+
}) }}
|
|
410
78
|
</form>
|
|
411
|
-
|
|
79
|
+
{% endcall %}
|
|
412
80
|
|
|
413
81
|
{# Feeds List #}
|
|
414
82
|
{% if feeds and feeds.length > 0 %}
|
|
415
|
-
|
|
416
|
-
<h2>{{ __("rss.feeds") }}</h2>
|
|
83
|
+
{% call section({ title: __("rss.feeds") }) %}
|
|
417
84
|
<ul class="rss-feed-list">
|
|
418
85
|
{% for feed in feeds %}
|
|
419
86
|
<li class="rss-feed-item" data-feed-id="{{ feed.id }}">
|
|
@@ -453,13 +120,12 @@
|
|
|
453
120
|
</li>
|
|
454
121
|
{% endfor %}
|
|
455
122
|
</ul>
|
|
456
|
-
|
|
123
|
+
{% endcall %}
|
|
457
124
|
{% endif %}
|
|
458
125
|
|
|
459
126
|
{# Recent Items #}
|
|
460
127
|
{% if recentItems and recentItems.length > 0 %}
|
|
461
|
-
|
|
462
|
-
<h2>{{ __("rss.recentItems") }}</h2>
|
|
128
|
+
{% call section({ title: __("rss.recentItems") }) %}
|
|
463
129
|
<ul class="rss-item-list">
|
|
464
130
|
{% for item in recentItems %}
|
|
465
131
|
<li class="rss-item">
|
|
@@ -485,20 +151,22 @@
|
|
|
485
151
|
</li>
|
|
486
152
|
{% endfor %}
|
|
487
153
|
</ul>
|
|
488
|
-
|
|
154
|
+
{% endcall %}
|
|
489
155
|
{% endif %}
|
|
490
156
|
|
|
491
157
|
{# Public Page Link #}
|
|
492
|
-
|
|
158
|
+
{% call section({ title: __("rss.widget.title") if __("rss.widget.title") else "Public page" }) %}
|
|
493
159
|
<div class="rss-public-link">
|
|
494
160
|
<p>{{ __("rss.widget.description") }}</p>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
161
|
+
{{ button({
|
|
162
|
+
classes: "button--secondary",
|
|
163
|
+
href: "/news/",
|
|
164
|
+
text: __("rss.widget.view"),
|
|
165
|
+
target: "_blank"
|
|
166
|
+
}) }}
|
|
498
167
|
</div>
|
|
499
|
-
|
|
168
|
+
{% endcall %}
|
|
500
169
|
{% endif %}
|
|
501
|
-
</div>
|
|
502
170
|
|
|
503
171
|
<script>
|
|
504
172
|
// Handle add feed form
|