-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeedcycle.html
More file actions
340 lines (326 loc) · 20.1 KB
/
feedcycle.html
File metadata and controls
340 lines (326 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
<!DOCTYPE html>
<html lang="en" data-theme="system">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1, viewport-fit=cover" />
<link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#0b1e40" />
<meta name="application-name" content="IdleGames" />
<script src="assets/js/pwa.js" defer></script>
<!-- Removed Google Fonts preload/stylesheet to stop Chrome preload warnings; relying on system stack/onsite fonts instead. -->
<title>Feed Cycle 2.0.1</title>
<meta name="description" content="Feed Cycle 2.0.1 — local-first, accessible feed & podcast reader" />
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico" />
<link rel="stylesheet" href="feedcycle.css" />
<link rel="stylesheet" href="feedcycle-fullscreen.css" />
</head>
<body>
<a href="#app-main" class="skip-link">Skip to content</a>
<header class="fc-header" role="banner">
<div class="brand">
<!-- Monotone icon sprite -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="i-play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5h4v14H6zm8 0h4v14h-4z"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="currentColor"><path d="M5 20h14v-2H5v2zM12 3v12l5-5h-3V3h-4v7H7l5 5z"/></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.63-.07-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.14 7.14 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 14.9 2h-3.8a.5.5 0 0 0-.5.42l-.36 2.54c-.6.25-1.15.57-1.66.94l-2.39-.96a.5.5 0 0 0-.6.22L3.67 8.04a.5.5 0 0 0 .12.64l2.03 1.58c-.05.31-.08.64-.08.97 0 .31.02.62.07.92l-2.02 1.59a.5.5 0 0 0-.12.64l1.92 3.32c.14.24.43.34.69.22l2.39-.96c.5.38 1.05.7 1.65.95l.36 2.54c.05.24.25.42.5.42h3.8c.25 0 .46-.18.5-.42l.36-2.54c.6-.25 1.16-.57 1.66-.95l2.39.96c.26.12.55.02.69-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58ZM12 15.5A3.5 3.5 0 1 1 12 8a3.5 3.5 0 0 1 0 7.5Z"/></symbol>
<symbol id="i-filter" viewBox="0 0 24 24" fill="currentColor"><path d="M3 5h18v2l-7 8v4l-4 2v-6L3 7V5z"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24" fill="currentColor"><path d="M12 6V3L8 7l4 4V8c2.2 0 4 1.79 4 4a4 4 0 0 1-4 4 4 4 0 0 1-3.86-3H6.02A6 6 0 0 0 12 20a6 6 0 0 0 0-12z"/></symbol>
<symbol id="i-fullscreen" viewBox="0 0 24 24" fill="currentColor"><path d="M5 5h5V3H3v7h2V5zm9-2v2h5v5h2V3h-7zm5 14h-5v2h7v-7h-2v5zm-12 0H5v-5H3v7h7v-2H7z"/></symbol>
<symbol id="i-fullscreen-exit" viewBox="0 0 24 24" fill="currentColor"><path d="M5 5h5V3H3v7h2V5zm9-2v2h5v5h2V3h-7zm5 14h-5v2h7v-7h-2v5zm-12 0H5v-5H3v7h7v-2H7z"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2h6z"/></symbol>
<symbol id="i-back" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.58-5.59L12 4 4 12l8 8 1.41-1.41L7.83 13H20v-2z"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24" fill="currentColor"><path d="M6 7h12l-1 13H7L6 7zm5-4h2l1 2h5v2H4V5h5l1-2z"/></symbol>
<symbol id="i-edit" viewBox="0 0 24 24" fill="currentColor"><path d="m5 18 4-.5L19.06 7.44a1.5 1.5 0 0 0 0-2.12L16.68 2.94a1.5 1.5 0 0 0-2.12 0L6.5 11.06 6 15l-.5 3z"/></symbol>
<symbol id="i-tag" viewBox="0 0 24 24" fill="currentColor"><path d="m10 4 9 9-7 7-9-9V4h7zm-2 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol>
<symbol id="i-star" viewBox="0 0 24 24" fill="currentColor"><path d="m12 17.27 6.18 3.73-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></symbol>
<symbol id="i-star-fill" viewBox="0 0 24 24" fill="currentColor"><path d="M12 17.27 6.18 21l1.64-7.03L2 9.24l7.19-.61L12 2l2.81 6.63 7.19.61-5.82 4.73L17.82 21z"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24" fill="currentColor"><path d="M6.76 4.84 5.35 3.43 3.93 4.85l1.41 1.41L6.76 4.84zM1 13h3v-2H1v2zm10 9h2v-3h-2v3zM4.34 17.66 5.75 19.07l1.41-1.41-1.41-1.41-1.41 1.41zM20 13h3v-2h-3v2zm-9-9h2V1h-2v3zm7.66 13.66 1.41-1.41-1.41-1.41-1.41 1.41 1.41 1.41zM17.24 4.84l1.41 1.41 1.41-1.41-1.41-1.41-1.41 1.41zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24" fill="currentColor"><path d="M11.01 3a9 9 0 0 0 8.94 10.06A7 7 0 1 1 11.01 3z"/></symbol>
<symbol id="i-auto" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2 1 21h22L12 2zm0 4.84L19.53 19H4.47L12 6.84z"/></symbol>
</svg>
<h1 class="app-title" aria-level="1">Feed Cycle</h1>
</div>
<nav class="primary-actions" aria-label="Primary actions">
<button id="btn-fullscreen" class="icon-btn" aria-label="Enter fullscreen" aria-pressed="false"><svg class="icon" aria-hidden="true"><use id="fullscreenIconUse" href="#i-fullscreen"/></svg></button>
<button id="btn-settings" class="icon-btn" aria-haspopup="true"><svg class="icon" aria-hidden="true"><use href="#i-settings"/></svg><span class="visually-hidden">Settings</span></button>
<button id="btn-theme" class="icon-btn" aria-label="Toggle theme"><svg class="icon" aria-hidden="true"><use id="themeIconUse" href="#i-auto"/></svg></button>
</nav>
</header>
<section id="media-player" class="media-player" aria-label="Media player toolbar">
<div class="mp-row top">
<button id="mp-play" aria-label="Play"><svg class="icon"><use href="#i-play"/></svg></button>
<button id="mp-pause" aria-label="Pause" disabled><svg class="icon"><use href="#i-pause"/></svg></button>
<button id="mp-download" aria-label="Download media" disabled><svg class="icon"><use href="#i-download"/></svg></button>
<button id="mp-fav" aria-label="View article" title="View article" disabled><svg class="icon"><use id="favIconUse" href="#i-star"/></svg></button>
<progress id="mp-progress" value="0" max="1" aria-label="Media progress" class="mp-progress"></progress>
<span id="mp-time" class="mp-time" aria-live="off" aria-label="Current time">0:00</span>
</div>
<div class="mp-row cue-row" aria-label="Cue seek control" role="group">
<div class="cue-track" id="cueTrack">
<button id="cueBtn" class="cue-btn" aria-label="Variable skip" aria-describedby="cueHelp" tabindex="0"><svg class="icon" aria-hidden="true"><use href="#i-play"/></svg></button>
</div>
</div>
<p id="cueHelp" class="visually-hidden">Drag or flick left/right from center to skip. Further = larger jump. Keyboard: Left / Right = 10s, Shift+Arrow = 60s.</p>
</section>
<main id="app-main" class="app-main" role="main" aria-describedby="app-help">
<p id="app-help" class="visually-hidden">Use Settings and Filter buttons to navigate panels. Inside panels, use the Back button or press Escape to return. Media player slider is draggable and keyboard accessible (Left/Right/Home/End).</p>
<div id="panel-stack" class="panel-stack" aria-live="polite" aria-atomic="false"></div>
</main>
<template id="tpl-settings-main">
<section class="panel" data-panel="settingsMain" aria-labelledby="settingsTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="settingsTitle">Settings</h2>
</header>
<div class="panel-body">
<ul class="menu-list">
<li><button data-nav="settingsSubscriptions">Manage Subscriptions</button></li>
<li><button data-nav="settingsPreferences">Preferences</button></li>
<!-- Proxy & cache inputs live inside Preferences panel (Theme/Refresh section) -->
<li><button data-nav="settingsData">Data</button></li>
<li><button data-nav="settingsAbout">About</button></li>
<li><button data-nav="settingsPrivacy">Privacy & Licence</button></li>
</ul>
</div>
</section>
</template>
<template id="tpl-settings-data">
<section class="panel" data-panel="settingsData" aria-labelledby="settingsDataTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="settingsDataTitle">Data</h2>
</header>
<div class="panel-body actions-col">
<button data-action="import-opml">Import OPML</button>
<button data-action="export-opml">Export OPML</button>
<button data-action="add-sample">Add sample RSS feeds</button>
<button data-action="clear-cache" class="danger">Clear cache</button>
<button data-action="erase-all" class="danger">Erase all</button>
</div>
</section>
</template>
<template id="tpl-settings-subscriptions">
<section class="panel" data-panel="settingsSubscriptions" aria-labelledby="subsTitle" role="region">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="subsTitle">Manage Subscriptions</h2>
<div class="panel-tools">
<button data-action="add-sub" aria-label="Add subscription"><svg class="icon" aria-hidden="true"><use href="#i-plus"/></svg></button>
</div>
</header>
<div class="panel-body" id="subsBody" aria-live="polite"></div>
</section>
</template>
<template id="tpl-subscription-edit">
<section class="panel" data-panel="subscriptionEdit" aria-labelledby="subEditTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="subEditTitle">Edit Subscription</h2>
</header>
<div class="panel-body" id="subEditBody">
<form id="subEditForm" class="panel-form" novalidate>
<fieldset class="fg">
<legend>Basics</legend>
<label>Title
<input type="text" id="subEditTitleInput" required maxlength="160" />
</label>
<label>Feed URL
<input type="url" id="subEditUrlInput" required />
</label>
</fieldset>
<fieldset class="fg">
<legend>Categorisation</legend>
<label>Category
<input type="text" id="subEditCategoryInput" list="categoryOptions" maxlength="80" />
</label>
<datalist id="categoryOptions"></datalist>
</fieldset>
<fieldset class="fg">
<legend>Options</legend>
<label>Auto Refresh (minutes)
<input type="number" id="subEditRefreshInput" min="5" max="1440" step="5" placeholder="(global)" />
</label>
<label class="choice"><input type="checkbox" id="subEditEnabledInput" checked> Enabled (fetch this feed)</label>
</fieldset>
<div class="form-actions">
<button type="submit">Save</button>
<button type="button" data-action="cancel" class="back-btn">Cancel</button>
</div>
<p class="muted" id="subEditStatus" aria-live="polite"></p>
</form>
</div>
</section>
</template>
<template id="tpl-settings-preferences">
<section class="panel" data-panel="settingsPreferences" aria-labelledby="prefsTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="prefsTitle">Preferences</h2>
</header>
<div class="panel-body">
<form id="prefsForm" class="panel-form" novalidate>
<fieldset class="fg">
<legend>Theme</legend>
<label class="choice"><input type="radio" name="theme" value="system"> System</label>
<label class="choice"><input type="radio" name="theme" value="light"> Light</label>
<label class="choice"><input type="radio" name="theme" value="dark"> Dark</label>
</fieldset>
<fieldset class="fg">
<legend>Refresh</legend>
<label>Global Refresh (minutes)
<input type="number" id="globalRefreshInput" min="5" max="1440" step="5" />
</label>
<label>Cache Max Age (minutes)
<input type="number" id="cacheMaxAgeInput" min="5" max="1440" step="5" />
</label>
<label>CORS Proxy (optional)
<input type="text" id="corsProxyInput" placeholder="https://api.allorigins.win/raw?url=" />
<small class="muted">Use %s placeholder or end with = / url= to auto‑encode, otherwise raw append.</small>
</label>
</fieldset>
<div class="form-actions">
<button type="submit">Save</button>
<button type="button" class="back-btn" data-action="back">Cancel</button>
</div>
<p class="muted" id="prefsStatus" aria-live="polite"></p>
</form>
</div>
</section>
</template>
<template id="tpl-settings-about">
<section class="panel" data-panel="settingsAbout" aria-labelledby="aboutTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="aboutTitle">About</h2>
</header>
<div class="panel-body">
<h3>Feed Cycle <span class="muted">v2.0.1</span></h3>
<p>Local‑first, no‑tracking feed reader. All data stays in your browser storage. No external analytics; network only touches your chosen feeds.</p>
<p>Design goals: accessibility, clarity, offline resilience (upcoming), and ergonomic media time travel (variable skip control).</p>
<p>Roadmap highlights: podcast enclosure playback, advanced search scoring, proxy & cache resilience, offline hydration, tagging, virtualization.</p>
<p class="muted">Crafted for longevity & user agency.</p>
</div>
</section>
</template>
<template id="tpl-settings-privacy">
<section class="panel" data-panel="settingsPrivacy" aria-labelledby="privacyTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="privacyTitle">Privacy & Licence</h2>
</header>
<div class="panel-body">
<h3>Privacy</h3>
<p>No telemetry, no tracking pixels, no remote calls beyond fetching the feeds you add (and optional CORS proxy endpoints you configure).</p>
<p>All state (feeds, read markers, favourites, tags) is stored in <code>localStorage</code>. Future offline mode will additionally use the Cache API for feed bodies.</p>
<h3>Licence</h3>
<p>Open source under the existing project licence (see root <code>LICENSE</code> file).</p>
<h3>Data Portability</h3>
<p>Use OPML export to back up subscriptions. Future export for tags & read state is planned.</p>
</div>
</section>
</template>
<template id="tpl-filter-and-sort-settings">
<section class="panel" data-panel="filterAndSortSettings" aria-labelledby="filterPanelTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="filterPanelTitle">Filter & Search</h2>
</header>
<div class="panel-body">
<form id="filterForm" class="filter-form" novalidate>
<fieldset class="fg">
<legend>Search</legend>
<label for="filterSearch">Keywords</label>
<input id="filterSearch" name="q" type="search" placeholder="Search titles or summaries" autocomplete="off" />
</fieldset>
<fieldset class="fg">
<legend>Sources</legend>
<label for="filterSubscription">Subscription</label>
<select id="filterSubscription" name="subscription">
<option value="">All subscriptions</option>
</select>
<label for="filterCategory">Category</label>
<select id="filterCategory" name="category">
<option value="">All categories</option>
</select>
</fieldset>
<fieldset class="fg">
<legend>Tags</legend>
<p class="muted">Match any selected tag</p>
<div id="filterTagsWrap" class="tag-toggle-list" role="group" aria-label="Filter by tags"></div>
</fieldset>
<fieldset class="fg">
<legend>Read state</legend>
<label class="choice"><input type="radio" name="readState" value="all" checked /> All articles</label>
<label class="choice"><input type="radio" name="readState" value="unread" /> Unread only</label>
<label class="choice"><input type="radio" name="readState" value="read" /> Read only</label>
<label class="inline-checkbox"><input type="checkbox" name="favOnly" /> Favourites only</label>
</fieldset>
<fieldset class="fg">
<legend>Sort order</legend>
<label for="filterSort">Primary sort</label>
<select id="filterSort" name="sort">
<option value="published-desc">Published (newest first)</option>
<option value="published-asc">Published (oldest first)</option>
<option value="title-asc">Title (A → Z)</option>
<option value="title-desc">Title (Z → A)</option>
<option value="feed-asc">Feed (A → Z)</option>
<option value="feed-desc">Feed (Z → A)</option>
<option value="host-asc">Source host (A → Z)</option>
<option value="host-desc">Source host (Z → A)</option>
<option value="favorite">Favourites first</option>
<option value="unread">Unread first</option>
</select>
</fieldset>
<fieldset class="fg">
<legend>Published date</legend>
<label for="filterDateFrom">From</label>
<input id="filterDateFrom" name="dateFrom" type="date" />
<label for="filterDateTo">To</label>
<input id="filterDateTo" name="dateTo" type="date" />
</fieldset>
<div class="panel-actions">
<button type="button" data-action="cancel">Cancel</button>
<button type="button" data-action="reset">Reset</button>
<button type="submit" data-action="apply" class="primary">Apply</button>
</div>
</form>
</div>
</section>
</template>
<template id="tpl-articles-infinite-scrollable">
<section class="panel" data-panel="articlesInfiniteScrollable" aria-labelledby="articlesTitle" role="region">
<header class="panel-bar">
<h2 id="articlesTitle">Articles</h2>
<div class="panel-tools">
<button data-action="quick-refresh" aria-label="Refresh feeds"><svg class="icon" aria-hidden="true"><use href="#i-refresh"/></svg></button>
<button data-action="open-filter" aria-label="Open filter panel"><svg class="icon" aria-hidden="true"><use href="#i-filter"/></svg></button>
</div>
</header>
<div class="panel-body list" id="articlesList" aria-live="polite"></div>
</section>
</template>
<template id="tpl-article-viewer">
<section class="panel" data-panel="articleViewer" aria-labelledby="articleTitle" role="article">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="articleTitle">Article</h2>
</header>
<article class="panel-body" id="articleBody"></article>
</section>
</template>
<template id="tpl-video-viewer">
<section class="panel" data-panel="videoViewer" aria-labelledby="videoTitle">
<header class="panel-bar">
<button class="back-btn" data-action="back" aria-label="Back"><svg class="icon" aria-hidden="true"><use href="#i-back"/></svg></button>
<h2 id="videoTitle">Video</h2>
</header>
<div class="panel-body" id="videoBody"></div>
</section>
</template>
<input id="opmlFile" type="file" accept=".opml,.xml,text/xml,application/xml" hidden />
<script src="feedcycle.js"></script>
</body>
<script src="parental.js"></script>
</html>