Desktop: per-store ignore (hide only the App Store or Google Play side), N ignored show toggle in sidebar and portfolio, Unignore in the app controls; ignore flags sync with the shared app list

This commit is contained in:
CRTkafa 2026-10-23 01:35:27 +08:00
parent 13b91dd423
commit 9610c9477e
14 changed files with 347 additions and 17 deletions

View file

@ -46,7 +46,12 @@ export interface UnifiedApp<P extends IdProject = IdProject> {
pairing: Pairing;
/** The keys an "ignore" puts on the list ("project:<id>" or "app:<store key>"). */
ignoreKeys: string[];
/** Ignored entirely (every store side it has). */
ignored: boolean;
/** Store sides left out while the other side stays (a half-ignored app): their ref is dropped from ios / android. */
ignoredStores: ('ios' | 'android')[];
/** The ignore key of each side (null: no such side), for the per-store toggles. */
sideKeys: { ios: string | null; android: string | null };
/** Store icon (data URL) when the folder has none. */
storeIcon: string | null;
}
@ -115,9 +120,17 @@ export function buildApps<P extends IdProject>(i: BuildInput<P>): UnifiedApp<P>[
const studio = studioOverride.get(`project:${p.id}`)
?? (p.appleAccount && accountStudio.get(p.appleAccount)) ?? (p.playAccount && accountStudio.get(p.playAccount))
?? (ios && accountStudio.get(ios.account)) ?? (android && accountStudio.get(android.account)) ?? null;
// ignored: the whole folder, or every store side it has; one side alone hides just that store (project:<id>:ios | :android)
const whole = ignored.has(`project:${p.id}`);
const sideOff = { ios: whole || ignored.has(`project:${p.id}:ios`), android: whole || ignored.has(`project:${p.id}:android`) };
const has = { ios: !!ios || !!s?.apple || !!p.appleAccount, android: !!android || !!s?.play || !!p.playAccount };
const present = (['ios', 'android'] as const).filter(k => has[k]);
const allOff = whole || (present.length > 0 && present.every(k => sideOff[k]));
const hidden = allOff ? [] : present.filter(k => sideOff[k]);
out.push({
key: p.id, target: p.id, name: p.name, project: p, ios, android, studio, pairing: 'folder',
ignoreKeys: [`project:${p.id}`], ignored: ignored.has(`project:${p.id}`), storeIcon: ios?.app.icon ?? android?.app.icon ?? null,
key: p.id, target: p.id, name: p.name, project: p, ios: hidden.includes('ios') ? null : ios, android: hidden.includes('android') ? null : android, studio, pairing: 'folder',
ignoreKeys: [`project:${p.id}`, `project:${p.id}:ios`, `project:${p.id}:android`], ignored: allOff, ignoredStores: hidden, sideKeys: { ios: `project:${p.id}:ios`, android: `project:${p.id}:android` },
storeIcon: ios?.app.icon ?? android?.app.icon ?? null,
});
}
@ -147,9 +160,14 @@ export function buildApps<P extends IdProject>(i: BuildInput<P>): UnifiedApp<P>[
const keys = [ios, android].filter((r): r is StoreRef => !!r).map(r => r.key);
const studio = keys.map(k => studioOverride.get(k)).find(Boolean)
?? (ios && accountStudio.get(ios.account)) ?? (android && accountStudio.get(android.account)) ?? null;
// a paired app is ignored when every side is; one ignored side alone hides just that store
const off = (r: StoreRef | null) => !!r && ignored.has(`app:${r.key}`);
const allOff = [ios, android].filter(Boolean).length > 0 && [ios, android].every(r => !r || off(r));
const hidden = allOff ? [] : (['ios', 'android'] as const).filter(k => off(k === 'ios' ? ios : android));
out.push({
key: target, target, name: ios?.app.name ?? android?.app.name ?? '?', project: null, ios, android, studio, pairing,
ignoreKeys: keys.map(k => `app:${k}`), ignored: keys.some(k => ignored.has(`app:${k}`)), storeIcon: ios?.app.icon ?? android?.app.icon ?? null,
key: target, target, name: ios?.app.name ?? android?.app.name ?? '?', project: null, ios: hidden.includes('ios') ? null : ios, android: hidden.includes('android') ? null : android, studio, pairing,
ignoreKeys: keys.map(k => `app:${k}`), ignored: allOff, ignoredStores: hidden, sideKeys: { ios: ios ? `app:${ios.key}` : null, android: android ? `app:${android.key}` : null },
storeIcon: ios?.app.icon ?? android?.app.icon ?? null,
});
};
for (const [a, b, how] of pairs) storeOnly(a, b, how);
@ -180,6 +198,21 @@ export function withUnpair(o: Overrides, ios: string, android: string): Override
return { ...o, pairs: o.pairs.filter(p => !samePair(p, ios, android)), unpairs: [...o.unpairs.filter(p => !samePair(p, ios, android)), { ios, android }] };
}
/** Overrides after ignoring (or restoring) ONE store side of an app. A whole-folder key is split into its sides first. */
export function withSideIgnored(o: Overrides, app: UnifiedApp, side: 'ios' | 'android', on: boolean): Overrides {
const key = app.sideKeys[side];
if (!key) return o;
const set = new Set(o.ignored);
if (app.project) {
const whole = `project:${app.project.id}`;
if (set.delete(whole)) { set.add(app.sideKeys.ios!); set.add(app.sideKeys.android!); }
// both sides ignored is the same as the whole folder
if (on) set.add(key); else set.delete(key);
if (set.has(app.sideKeys.ios!) && set.has(app.sideKeys.android!)) { set.delete(app.sideKeys.ios!); set.delete(app.sideKeys.android!); set.add(whole); }
} else if (on) set.add(key); else set.delete(key);
return { ...o, ignored: [...set] };
}
export function withIgnored(o: Overrides, keys: string[], on: boolean): Overrides {
const set = new Set(o.ignored);
for (const k of keys) { if (on) set.add(k); else set.delete(k); }