Sources page: one set of sources for every profile on the account

The app kept a copy of the addon config per profile, and this page read
and edited only the first one. Sources set here therefore never reached
the other profiles: the owner's Kids profile went on with a Czech addon
address that had no webshare in it.

The page now opens every copy and shows one merged answer (the newest
value per field, and the longest Czech addon URL whatever its age, since
that is the one carrying webshare's credentials too). Saving writes the
same sources to every copy and creates one for a profile without. The
app does the same from 1.3.0+37.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-09-13 01:33:09 +02:00
parent 4c8e02f48b
commit db50af0c69

View file

@ -1111,15 +1111,44 @@ $("adUnlock").onclick=async function(){
$("acChange").disabled=false;
};
// One account, one set of sources. The app used to keep a copy per profile,
// so an account that ever switched profile holds several copies that drifted
// apart: sources edited here reached only the first, and a child profile went
// on without webshare. Read every copy and show one answer. The newest value
// wins per field, except the Czech addon URL, where the longer one wins: it is
// the one that carries webshare's credentials as well as prehraj.to's.
function mergeConfigs(list){
var sorted=list.slice().sort(function(a,b){
return String(b.updatedAt||"").localeCompare(String(a.updatedAt||"")); });
var out={};
sorted.forEach(function(e){
Object.keys(e.cfg||{}).forEach(function(k){
var v=e.cfg[k];
if ((out[k]==null || out[k]==="") && v!=null && v!=="") out[k]=v;
});
});
var longest="";
list.forEach(function(e){
var u=(e.cfg||{}).czechAddonUrl||"";
if (u.length>longest.length) longest=u;
});
if (longest) out.czechAddonUrl=longest;
return out;
}
async function loadAddons(){
var r=await api("GET","/api/collections/addon_config/records?perPage=200");
CONFIGS = r.ok ? (r.data.items||[]) : [];
cfgRec = CONFIGS[0] || null;
cfg=null;
if (cfgRec){
try { cfg=JSON.parse(await decryptBlob(cfgRec.blob, PASSWORD, cfgRec.salt)); }
catch(_){ cfg=null; }
var opened=[];
for (var i=0;i<CONFIGS.length;i++){
try {
opened.push({ cfg: JSON.parse(await decryptBlob(CONFIGS[i].blob, PASSWORD, CONFIGS[i].salt)),
updatedAt: CONFIGS[i].updatedAt });
} catch(_){}
}
if (opened.length) cfg=mergeConfigs(opened);
var configured = cfg && (cfg.addonUrl || cfg.czechAddonUrl);
$("adSetup").className = configured ? "hidden" : "";
$("adEdit").className = configured ? "" : "hidden";
@ -1222,24 +1251,36 @@ $("byGo").onclick=async function(){
await saveConfig(out, $("byGo"));
};
// The same sources go to every profile's copy, and a profile without one gets
// one: a device reads the copy of whichever profile it is on, and an older app
// that found none would make its own from whatever it had.
async function saveConfig(next, btn){
var merged=Object.assign({}, cfg||{}, next);
if (!PROFILES.length && !CONFIGS.length){ setMsg($("adMsg"),"Účet nemá profil.","err");
if(btn) busy(btn,false); return; }
var plain=JSON.stringify(Object.assign({}, cfg||{}, next));
var now=new Date().toISOString();
var wrote=0, failed=0, covered={};
for (var i=0;i<CONFIGS.length;i++){
var rec=CONFIGS[i];
covered[rec.profile]=true;
var pr=await api("PATCH","/api/collections/addon_config/records/"+rec.id,
{ blob:await encryptWithSalt(plain, PASSWORD, rec.salt), salt:rec.salt,
kdf:KDF_ID, updatedAt:now });
if (pr.ok) wrote++; else failed++;
}
var salt=(cfgRec && cfgRec.salt) || newSaltB64();
var blob=await encryptWithSalt(JSON.stringify(merged), PASSWORD, salt);
var body={ blob:blob, salt:salt, kdf:KDF_ID, updatedAt:new Date().toISOString() };
var r;
if (cfgRec){
r=await api("PATCH","/api/collections/addon_config/records/"+cfgRec.id, body);
} else {
if (!PROFILES.length){ setMsg($("adMsg"),"Účet nemá profil.","err");
if(btn) busy(btn,false); return; }
body.profile=PROFILES[0].id;
r=await api("POST","/api/collections/addon_config/records", body);
for (var j=0;j<PROFILES.length;j++){
if (covered[PROFILES[j].id]) continue;
var cr=await api("POST","/api/collections/addon_config/records",
{ profile:PROFILES[j].id, blob:await encryptWithSalt(plain, PASSWORD, salt),
salt:salt, kdf:KDF_ID, updatedAt:now });
if (cr.ok) wrote++; else failed++;
}
if(btn) busy(btn,false);
setMsg($("adMsg"), r.ok ? "Zdroje uloženy. Aplikace si je stáhne sama."
: "Uložení selhalo.", r.ok?"ok":"err");
if (r.ok) await loadAddons();
var ok = wrote>0 && failed===0;
setMsg($("adMsg"), ok ? "Zdroje uloženy. Aplikace si je stáhne sama."
: "Uložení selhalo.", ok?"ok":"err");
if (wrote>0) await loadAddons();
}
$("adSave").onclick=async function(){