Per-user read state and subscriptions
Read, starred and position move to entry_state; subscriptions carry each person's keywords, auto-download, explicit and per-scan limit. The feed list and unread counts are per person, and the existing library is adopted by the admin on first start. The feed URL, folder and schedule stay shared and admin-only: one file serves everyone, so they describe the file rather than a preference. Scanning merges subscribers' wants -- anyone wanting an item is enough -- via merge_policy, which is pure and tested. Also: the test fixture wiped its data directory from every Playwright worker, deleting the database out from under the running daemon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
@@ -45,6 +45,8 @@
|
||||
--shadow:0 8px 28px rgba(45,83,145,.14);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
/* A rule that sets display beats the UA's [hidden], and several below do. */
|
||||
[hidden]{display:none!important}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;background:var(--bg);color:var(--fg);
|
||||
@@ -1230,8 +1232,8 @@ function settingsModal(f){
|
||||
${f.managed?`<p class="hint" style="margin:-6px 0 12px">This feed comes from an OPML
|
||||
subscription and follows its settings. Saving anything here gives it its own entry in
|
||||
config.toml, and it stops following the subscription's settings.</p>`:''}
|
||||
<div class="field"><label>Download folder</label>
|
||||
<input type="text" id="sfolder" value="${esc(f.folder||'')}" placeholder="${esc(f.title||f.id)}"></div>
|
||||
<p class="hint" style="margin:-4px 0 10px">These are <b>your</b> settings for this feed.
|
||||
Everyone else keeps their own.</p>
|
||||
<div class="field"><label>Keywords</label>
|
||||
<input type="text" id="skw" value="${esc(f.keywords.join(', '))}">
|
||||
<span class="hint">Comma separated. Empty takes everything.</span></div>
|
||||
@@ -1243,23 +1245,34 @@ function settingsModal(f){
|
||||
<label class="check"><input type="checkbox" id="sexp" ${f.allow_explicit?'checked':''}> Allow items marked explicit</label>
|
||||
<div class="field"><label>Feed URL</label>
|
||||
<div class="inline">
|
||||
<input type="text" id="surl" value="${esc(f.url)}" spellcheck="false">
|
||||
<input type="text" id="surl" value="${esc(f.url)}" spellcheck="false" ${S.me&&S.me.admin?'':'readonly'}>
|
||||
<button type="button" class="btn" id="scopy">Copy</button>
|
||||
</div>
|
||||
<span class="hint">Editing this keeps every item and download — handy when an auth token
|
||||
in the URL is rotated. The feed is re-checked from scratch on the next scan.</span></div>
|
||||
<span class="hint">${S.me&&S.me.admin
|
||||
? `Shared with everyone reading this feed. Editing it keeps every item and download —
|
||||
handy when an auth token in the URL is rotated. The feed is re-checked from scratch
|
||||
on the next scan.`
|
||||
: `The same for everyone reading this feed, so only an admin can change it.`}</span></div>
|
||||
${S.me&&S.me.admin?`<div class="field"><label>Download folder (shared)</label>
|
||||
<input type="text" id="sfolder" value="${esc(f.folder||'')}" placeholder="${esc(f.title||f.id)}">
|
||||
<span class="hint">Where the files land. There is one copy however many people
|
||||
subscribe, so this is the same for everyone.</span></div>`:''}
|
||||
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn primary" id="ssave">Save</button></div>`);
|
||||
$('#scopy').onclick=()=>copyText($('#surl').value,$('#scopy'));
|
||||
$('#ssave').onclick=async()=>{
|
||||
const max=$('#smax').value;
|
||||
try{
|
||||
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({
|
||||
url:$('#surl').value.trim(),
|
||||
folder:$('#sfolder').value.trim()||null,
|
||||
const patch={
|
||||
keywords:$('#skw').value.split(',').map(s=>s.trim()).filter(Boolean),
|
||||
max_new_per_check:max===''?null:Number(max),
|
||||
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked})});
|
||||
auto_download:$('#sauto').checked, allow_explicit:$('#sexp').checked};
|
||||
// The shared half is an admin's to change, and the API refuses it from anyone else.
|
||||
if(S.me&&S.me.admin){
|
||||
patch.url=$('#surl').value.trim();
|
||||
patch.folder=$('#sfolder').value.trim()||null;
|
||||
}
|
||||
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify(patch)});
|
||||
closeModal(); toast('Saved — applies on the next scan');
|
||||
await loadFeeds(true); renderFeed(); loadEntries();
|
||||
}catch(e){ toast(e.message,true); }
|
||||
@@ -1285,7 +1298,8 @@ function downloadLatestModal(f){
|
||||
|
||||
function removeFeed(f){
|
||||
openModal(`<h3>Unsubscribe?</h3>
|
||||
<p style="color:var(--dim)">Removes <b>${esc(f.title||f.id)}</b> from your feeds.
|
||||
<p style="color:var(--dim)">Removes <b>${esc(f.title||f.id)}</b> from your feeds. Anyone else
|
||||
reading it keeps it, along with their own read state.
|
||||
Downloaded files and history are kept, so re-adding it will not pull the back catalogue again.</p>
|
||||
<div class="cardacts"><button class="btn" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn danger" id="rgo">Unsubscribe</button></div>`);
|
||||
|
||||
Reference in New Issue
Block a user