my-provider-dash-manager 0.10.0 → 0.11.0
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/README.md +10 -2
- package/package.json +1 -1
- package/src/main.js +455 -41
package/README.md
CHANGED
|
@@ -251,6 +251,14 @@ encrypted with DPAPI, revocable in one click, and never leave the main process.
|
|
|
251
251
|
session, costing roughly 2 MB per open account. Dark Reader's browser extension cannot run in
|
|
252
252
|
Electron (its background worker needs APIs Electron does not expose, and its content script
|
|
253
253
|
would touch the page's own JavaScript), so the engine is bundled and driven directly.
|
|
254
|
+
- **Per-account overrides (new in 0.11.0).** Open an account's gear window and each control has a
|
|
255
|
+
three-way choice: *Follow the app-wide setting* (the default), *Always on for this account*, or
|
|
256
|
+
*Always off for this account*. An override works **in both directions** — an account can keep
|
|
257
|
+
blocking ads or stay dark while the app-wide toggle is off, and vice versa — and applies at once to
|
|
258
|
+
that account's open tab. Only turning ad-block **off** reloads that one tab, because the injected
|
|
259
|
+
hide-ads style can't be retracted any other way; dark mode and switching ad-block on never reload,
|
|
260
|
+
and no other account's tab is ever reloaded. Accounts that override anything are marked
|
|
261
|
+
**content: custom** on their Home card, and an account with no override behaves exactly as before.
|
|
254
262
|
|
|
255
263
|
### Extensibility
|
|
256
264
|
- **Add any service by URL** — it gets its own rail row and isolated per-account sessions.
|
|
@@ -373,7 +381,7 @@ npm run smoke:creds # import, per-account/manager/site lists, defaults, copy,
|
|
|
373
381
|
npm run smoke:autofill # real page: form detection, fill + request ids, undo, capture, origin guard (15)
|
|
374
382
|
npm run smoke:updates # version comparison, silent-install mode, auto-update toggle, real release-feed check (11)
|
|
375
383
|
npm run smoke:services # every built-in login page loads inside an isolated partition (42)
|
|
376
|
-
npm run smoke:dark # Dark Reader darkens a white page, stays out of the page's own JS, re-injects after navigation (
|
|
384
|
+
npm run smoke:dark # Dark Reader darkens a white page, stays out of the page's own JS, re-injects after navigation; per-account ad-block/dark overrides (26)
|
|
377
385
|
npm run capture # screenshot the running shell (--capture-out, --capture-open svc::id, --capture-js)
|
|
378
386
|
npm run spike # memory benchmark
|
|
379
387
|
npm run dist # build the Windows installer locally
|
|
@@ -382,7 +390,7 @@ npm start -- --print-paths # show the app name, version and data fold
|
|
|
382
390
|
npm start -- --open vercel::personal # open an account on launch
|
|
383
391
|
```
|
|
384
392
|
|
|
385
|
-
The suite is
|
|
393
|
+
The suite is 173 checks across eight modes. `MAM_USER_DATA=<dir>` points any command at an isolated
|
|
386
394
|
profile.
|
|
387
395
|
|
|
388
396
|
Releases are cut by pushing a `v*` tag: GitHub Actions checks the tag against `package.json`, builds
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-provider-dash-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "CLI to install and update My Provider Dash Manager: a free Windows desktop app that keeps many accounts per service (Vercel, Netlify, Supabase, Cloudflare, AWS, Google, Microsoft, GitHub, Gmail) signed in at once, each in a fully isolated session. Also available as mpdm.",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"scripts": {
|
package/src/main.js
CHANGED
|
@@ -349,6 +349,123 @@ function autoFillPlan(svc, id, url, opts = {}) {
|
|
|
349
349
|
return { kind: 'skip', reason: choices.length ? 'unresolved' : 'no-login' };
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// ---------- per-account content settings (0.11.0) ----------
|
|
353
|
+
// Ad-block and dark mode have ONE app-wide toggle each (Home) and were read from store.settings at
|
|
354
|
+
// every apply site. An account may now carry a sparse `contentOverride: { adBlock?: 'on'|'off',
|
|
355
|
+
// darkMode?: 'on'|'off' }`: ONLY the keys the user changed exist, so an untouched account's stored
|
|
356
|
+
// record is byte-identical to before (measured: the sparse shape is the only one that round-trips
|
|
357
|
+
// byte-identically; `{}` and null-filled shapes leave bytes behind). 'follow' is encoded as the
|
|
358
|
+
// ABSENT key, which is what updateContentOverride() enforces.
|
|
359
|
+
const CONTENT_NAMES = ['adBlock', 'darkMode'];
|
|
360
|
+
const CONTENT_VALUES = ['on', 'off', 'follow'];
|
|
361
|
+
|
|
362
|
+
// Modelled on autoFillPlan: pure, dependency-injected via `settings`, exported for smoke. The
|
|
363
|
+
// follow branch is derived from settings[name] — never a hardcoded default — so a future change to
|
|
364
|
+
// the app-wide default flows through every untouched account with no code change here.
|
|
365
|
+
function resolveContent(acc, name, settings) {
|
|
366
|
+
const v = acc && acc.contentOverride ? acc.contentOverride[name] : undefined;
|
|
367
|
+
if (v === 'on') return true;
|
|
368
|
+
if (v === 'off') return false;
|
|
369
|
+
return !!(settings || store.settings)[name];
|
|
370
|
+
}
|
|
371
|
+
// Sparse write: set the key when overridden, DELETE it on 'follow', and delete the whole object when
|
|
372
|
+
// it becomes empty. Nothing is written when the value is unchanged (so a no-op save touches no bytes).
|
|
373
|
+
function updateContentOverride(acc, name, value) {
|
|
374
|
+
if (!CONTENT_NAMES.includes(name)) throw new Error(`unknown content setting: ${name}`);
|
|
375
|
+
if (!CONTENT_VALUES.includes(value)) throw new Error(`invalid content value: ${value}`);
|
|
376
|
+
if (value === 'follow') {
|
|
377
|
+
if (acc.contentOverride) { delete acc.contentOverride[name]; if (!Object.keys(acc.contentOverride).length) delete acc.contentOverride; }
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!acc.contentOverride) acc.contentOverride = {};
|
|
381
|
+
acc.contentOverride[name] = value;
|
|
382
|
+
}
|
|
383
|
+
function contentOverrideFor(acc) {
|
|
384
|
+
return { adBlock: !!(acc && acc.contentOverride && acc.contentOverride.adBlock), darkMode: !!(acc && acc.contentOverride && acc.contentOverride.darkMode) };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// The two per-view react sites, in ONE place so the global toggle and a per-account override change
|
|
388
|
+
// cannot drift apart. `before`/`after` are resolved pairs; only a control whose value MOVED is acted
|
|
389
|
+
// on, so a tab that resolves the same is untouched (the equality bar for un-overridden accounts):
|
|
390
|
+
// - dark: enable/disable in that view's isolated world, no reload (measured: it restores exactly).
|
|
391
|
+
// - adBlock -> on: the network predicate is already live through its per-request closure, so only
|
|
392
|
+
// the cosmetic CSS needs re-applying.
|
|
393
|
+
// - adBlock -> off: injected CSS cannot be retracted (src/content.js is frozen and applyToPage
|
|
394
|
+
// discards its insertCSS key), so that ONE view reloads. Never any other view.
|
|
395
|
+
function applyContentChange(t, before, after) {
|
|
396
|
+
const wc = t.view.webContents;
|
|
397
|
+
if (!wc || wc.isDestroyed() || !before) return;
|
|
398
|
+
if (before.darkMode !== after.darkMode) { if (after.darkMode) dark.enable(wc); else dark.disable(wc); }
|
|
399
|
+
if (before.adBlock !== after.adBlock) {
|
|
400
|
+
if (after.adBlock) content.applyToPage(wc, { adBlock: true });
|
|
401
|
+
else wc.reload();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The did-finish-load apply for ONE document. A fresh document has no dark engine yet, so "off"
|
|
406
|
+
// needs no disable call; the cosmetic layer is applied only when resolved on (applyToPage itself
|
|
407
|
+
// also skips managed dashboards, invariant 5 — unchanged).
|
|
408
|
+
function applyContentToFreshDocument(wc, acc) {
|
|
409
|
+
content.applyToPage(wc, { adBlock: resolveContent(acc, 'adBlock', store.settings) });
|
|
410
|
+
if (resolveContent(acc, 'darkMode', store.settings)) dark.enable(wc);
|
|
411
|
+
}
|
|
412
|
+
// Re-resolve every live tab and act only where the value moved. `snapshot` maps tab key -> resolved
|
|
413
|
+
// pair captured BEFORE the change; a tab with no snapshot is left alone (it was created inside this
|
|
414
|
+
// same change, and its own did-finish-load path already used the new values).
|
|
415
|
+
function reactToContentChange(vm, snapshot) {
|
|
416
|
+
for (const t of vm.tabs.values()) {
|
|
417
|
+
const acc = findAccount(t.svc, t.id);
|
|
418
|
+
const after = { adBlock: resolveContent(acc, 'adBlock', store.settings), darkMode: resolveContent(acc, 'darkMode', store.settings) };
|
|
419
|
+
applyContentChange(t, snapshot.get(accountKey(t.svc, t.id)), after);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function contentSnapshot(vm) {
|
|
423
|
+
const snap = new Map();
|
|
424
|
+
for (const [key, t] of vm.tabs) {
|
|
425
|
+
const acc = findAccount(t.svc, t.id);
|
|
426
|
+
snap.set(key, { adBlock: resolveContent(acc, 'adBlock', store.settings), darkMode: resolveContent(acc, 'darkMode', store.settings) });
|
|
427
|
+
}
|
|
428
|
+
return snap;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// The body of the account-content-override IPC, exported so smoke drives the REAL path (the
|
|
432
|
+
// setOrClearDefault precedent). Validates, writes sparse, applies live, emits once.
|
|
433
|
+
function setAccountContentOverride(vm, svc, id, name, value) {
|
|
434
|
+
const acc = findAccount(svc, id);
|
|
435
|
+
if (!acc) throw new Error('unknown account');
|
|
436
|
+
if (!CONTENT_NAMES.includes(name)) throw new Error(`unknown content setting: ${name}`);
|
|
437
|
+
if (!CONTENT_VALUES.includes(value)) throw new Error(`invalid content value: ${value}`);
|
|
438
|
+
const snapshot = contentSnapshot(vm);
|
|
439
|
+
updateContentOverride(acc, name, value);
|
|
440
|
+
store.save();
|
|
441
|
+
reactToContentChange(vm, snapshot);
|
|
442
|
+
vm.emitState();
|
|
443
|
+
return {
|
|
444
|
+
value: (acc.contentOverride && acc.contentOverride[name]) || 'follow',
|
|
445
|
+
override: contentOverrideFor(acc),
|
|
446
|
+
resolved: { adBlock: resolveContent(acc, 'adBlock', store.settings), darkMode: resolveContent(acc, 'darkMode', store.settings) },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// The app-wide toggle's body, exported for smoke the same way (set-content-setting delegates here).
|
|
451
|
+
// 0.11.0: capture what every live tab resolves to BEFORE the change, so the reaction can skip any
|
|
452
|
+
// tab whose account overrides the control (its resolved value does not move) — for an account with
|
|
453
|
+
// no override this is exactly today's behaviour, tab for tab.
|
|
454
|
+
function setGlobalContentSetting(vm, name, value) {
|
|
455
|
+
if (!['adBlock', 'darkMode', 'saveLogins', 'autofill', 'autoUpdate'].includes(name)) throw new Error(`unknown setting: ${name}`);
|
|
456
|
+
const snapshot = (name === 'darkMode' || name === 'adBlock') ? contentSnapshot(vm) : null;
|
|
457
|
+
store.settings[name] = !!value;
|
|
458
|
+
if (name === 'autoUpdate') { store.save(); updater.setAutoUpdate(!!value); vm.emitState(); return !!value; }
|
|
459
|
+
store.save();
|
|
460
|
+
if (snapshot) {
|
|
461
|
+
// dark: one enable/disable call per affected view — no reload. ad-block: cosmetics re-apply on,
|
|
462
|
+
// and only a view whose RESOLVED value fell to off reloads (injected CSS has no other undo).
|
|
463
|
+
reactToContentChange(vm, snapshot);
|
|
464
|
+
}
|
|
465
|
+
vm.emitState();
|
|
466
|
+
return store.settings[name];
|
|
467
|
+
}
|
|
468
|
+
|
|
352
469
|
// ---------- view manager ----------
|
|
353
470
|
class ViewManager {
|
|
354
471
|
constructor() {
|
|
@@ -494,7 +611,7 @@ class ViewManager {
|
|
|
494
611
|
ses.setPermissionRequestHandler((_wc, permission, cb) => cb(ALLOWED.has(permission)));
|
|
495
612
|
ses.setPermissionCheckHandler((_wc, permission, _origin) => ALLOWED.has(permission));
|
|
496
613
|
}
|
|
497
|
-
content.attachAdBlock(ses, () =>
|
|
614
|
+
content.attachAdBlock(ses, () => resolveContent(acc, 'adBlock', store.settings)); // per-partition network blocking, resolved PER REQUEST (the account is captured, never scanned — 0.11.0)
|
|
498
615
|
if (acc && acc.proxy) this.applyProxy(view.webContents.session, acc.proxy);
|
|
499
616
|
return view;
|
|
500
617
|
}
|
|
@@ -533,14 +650,14 @@ class ViewManager {
|
|
|
533
650
|
if (input.type === 'keyDown' && input.key === 'Escape' && !this.modalOpen) this.deactivate();
|
|
534
651
|
});
|
|
535
652
|
view.webContents.on('did-finish-load', () => {
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
653
|
+
// Ad-block cosmetics + dark mode, resolved for THIS account (0.11.0). Enable dark mode once the
|
|
654
|
+
// document's styles exist: did-navigate fires too early — the dynamic engine finds no
|
|
655
|
+
// stylesheets to transform yet, so enabling there leaves the page white. This fires again after
|
|
656
|
+
// every cross-origin navigation, which is exactly what the isolated-world reset requires — the
|
|
657
|
+
// world (and Dark Reader in it) is gone by then and must be re-injected. Same-document
|
|
658
|
+
// (pushState) routing never reaches here, so it never re-injects. A fresh document never has an
|
|
659
|
+
// engine, so "resolved off" needs no disable call.
|
|
660
|
+
applyContentToFreshDocument(view.webContents, acc);
|
|
544
661
|
// refresh the address strip's key chip from the URL that actually finished loading (0.10.0).
|
|
545
662
|
this.emitState();
|
|
546
663
|
});
|
|
@@ -733,6 +850,10 @@ class ViewManager {
|
|
|
733
850
|
status: statusCache.get(accountKey(a.svc, a.id)) || null,
|
|
734
851
|
loginCount: accountLoginSummaries(a.svc, a.id, loginList).length, // "has saved login" indicator
|
|
735
852
|
noAutoFill: !!a.noAutoFill,
|
|
853
|
+
// 0.11.0: what each control ACTUALLY resolves to for this account, and whether the account
|
|
854
|
+
// overrides it — the gear window's worded state and the Home card's marker read these.
|
|
855
|
+
contentResolved: { adBlock: resolveContent(a, 'adBlock', store.settings), darkMode: resolveContent(a, 'darkMode', store.settings) },
|
|
856
|
+
contentOverridden: contentOverrideFor(a),
|
|
736
857
|
})), // live registry projection for the home grid (token presence + status summaries; never raw tokens)
|
|
737
858
|
groupTabs: !!store.settings.groupTabs,
|
|
738
859
|
collapsed: store.settings.collapsed || [],
|
|
@@ -1218,16 +1339,35 @@ async function runSmokeCreds() {
|
|
|
1218
1339
|
const sibling = copyCredentialField(otherAcct.id, 'password', 'vercel', 'team-a', 'https://vercel.com/login');
|
|
1219
1340
|
check('copy-refused-another-accounts-capture', sibling.ok === false && sibling.reason === 'other-account', `reason=${sibling.reason}`);
|
|
1220
1341
|
clipboard.clear();
|
|
1221
|
-
//
|
|
1342
|
+
// WARM-UP WRITE (required): on this machine the first writeText after clear() in a fresh
|
|
1343
|
+
// process does not yet round-trip through readText — this sentinel is what makes the very
|
|
1344
|
+
// first read-back reliable at all (proved 2026-11-03: removing it made every copy check fail).
|
|
1345
|
+
// A cold machine (the day's first run) can still be slow even with the warm-up, hence the poll.
|
|
1222
1346
|
clipboard.writeText('sentinel-before-copy');
|
|
1347
|
+
// the real copy path: main writes the clipboard, and the answer is a label — never the secret.
|
|
1348
|
+
// A cold machine can take a moment before the Win32 clipboard returns what was just written
|
|
1349
|
+
// (observed 2026-11-03: the day's first suite run read back stale values; every rerun passed),
|
|
1350
|
+
// so poll the read-back and retry the write once — a genuine write regression still fails hard.
|
|
1351
|
+
const clipboardSettles = async (doWrite, expected) => {
|
|
1352
|
+
let last = null;
|
|
1353
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1354
|
+
last = doWrite();
|
|
1355
|
+
const deadline = Date.now() + 2500;
|
|
1356
|
+
while (Date.now() < deadline) {
|
|
1357
|
+
if (clipboard.readText() === expected) return { last, text: clipboard.readText() };
|
|
1358
|
+
await new Promise(r => setTimeout(r, 50));
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return { last, text: clipboard.readText() };
|
|
1362
|
+
};
|
|
1223
1363
|
const sharedRow = siteA.rows.find(r => r.username === 'shared@example.com');
|
|
1224
|
-
const
|
|
1364
|
+
const pw = await clipboardSettles(() => copyCredentialField(sharedRow.id, 'password', 'vercel', 'team-a', 'https://vercel.com/login'), 'shared-pw');
|
|
1225
1365
|
check('copy-writes-the-right-password-to-the-clipboard',
|
|
1226
|
-
|
|
1227
|
-
`ok=${
|
|
1228
|
-
const
|
|
1229
|
-
check('copy-email-writes-the-username',
|
|
1230
|
-
`label="${
|
|
1366
|
+
pw.last.ok === true && pw.text === 'shared-pw' && !JSON.stringify(pw.last).includes('shared-pw'),
|
|
1367
|
+
`ok=${pw.last.ok} label="${pw.last.label}" clipboard-match=${pw.text === 'shared-pw'}`);
|
|
1368
|
+
const mail = await clipboardSettles(() => copyCredentialField(sharedRow.id, 'username', 'vercel', 'team-a', 'https://vercel.com/login'), 'shared@example.com');
|
|
1369
|
+
check('copy-email-writes-the-username', mail.last.ok === true && mail.text === 'shared@example.com',
|
|
1370
|
+
`label="${mail.last.label}" clipboard-match=${mail.text === 'shared@example.com'}`);
|
|
1231
1371
|
const badField = copyCredentialField(sharedRow.id, 'reveal', 'vercel', 'team-a', 'https://vercel.com/login');
|
|
1232
1372
|
check('copy-refuses-an-unknown-field', badField.ok === false && badField.reason === 'bad-field', `reason=${badField.reason}`);
|
|
1233
1373
|
clipboard.clear();
|
|
@@ -1456,6 +1596,245 @@ async function runSmokeDark() {
|
|
|
1456
1596
|
const bgNav = await bgOf();
|
|
1457
1597
|
check('re-darkens-after-cross-origin-navigation', !!bgNav && !/255,\s*255,\s*255/.test(bgNav), bgNav);
|
|
1458
1598
|
|
|
1599
|
+
// ================= 0.11.0: PER-ACCOUNT CONTENT SETTINGS =================
|
|
1600
|
+
// The nine checks above are the pre-0.11.0 goldens and stay untouched (no assertion edited). This
|
|
1601
|
+
// section is additive: dark overrides (5), ad-block (6 — the first ad-block coverage this suite
|
|
1602
|
+
// has ever had), then the data/API contracts (4).
|
|
1603
|
+
//
|
|
1604
|
+
// Every check runs offline: the dark checks use two local white pages, and the ad-block checks use
|
|
1605
|
+
// a seed-listed ad host that CANNOT resolve. With the blocker on the request dies as
|
|
1606
|
+
// net::ERR_BLOCKED_BY_CLIENT (-20); with it off the same request dies as net::ERR_NAME_NOT_RESOLVED
|
|
1607
|
+
// (-105) — measured on this machine against the repo's real src/content.js before this was written.
|
|
1608
|
+
// The two codes are therefore a real, network-free witness of the network-layer decision.
|
|
1609
|
+
const isDark = bg => !!bg && bg !== '' && !/255,\s*255,\s*255/.test(bg);
|
|
1610
|
+
const isWhite = bg => /255,\s*255,\s*255/.test(bg || '');
|
|
1611
|
+
|
|
1612
|
+
// A fresh account + a fresh white document, loaded through the REAL create path so the real
|
|
1613
|
+
// did-finish-load apply (and the real per-request closure in makeView) is what gets measured.
|
|
1614
|
+
const mk = async (id, url) => {
|
|
1615
|
+
vm.create('vercel', id, url, { show: false });
|
|
1616
|
+
const t = vm.tabs.get(accountKey('vercel', id));
|
|
1617
|
+
// create() calls loadURL without waiting, so either the load is still in flight or it already
|
|
1618
|
+
// finished (a cached file:// page) — never wait for an event that has already fired.
|
|
1619
|
+
if (t.view.webContents.isLoading()) await new Promise(r => t.view.webContents.once('did-finish-load', r));
|
|
1620
|
+
return t;
|
|
1621
|
+
};
|
|
1622
|
+
const bgOfTab = async t => {
|
|
1623
|
+
try { return await t.view.webContents.executeJavaScript(`getComputedStyle(document.documentElement).backgroundColor`); } catch { return ''; }
|
|
1624
|
+
};
|
|
1625
|
+
const engineOf = async t => {
|
|
1626
|
+
try { return await t.view.webContents.executeJavaScriptInIsolatedWorld(dark.DARK_WORLD_ID, [{ code: 'typeof window.DarkReader' }]); } catch { return undefined; }
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
// Reset the registry and the global settings this section depends on, then build a control view
|
|
1630
|
+
// with NO overrides at all (the equality baseline every per-account assertion compares against).
|
|
1631
|
+
store.accounts = store.accounts.filter(a => a.svc !== 'vercel' || !/^x-/.test(a.id));
|
|
1632
|
+
store.settings.darkMode = true; store.settings.adBlock = true;
|
|
1633
|
+
const control = await mk('x-control', toUrl(p1));
|
|
1634
|
+
await sleep(3200);
|
|
1635
|
+
const controlDark = await bgOfTab(control);
|
|
1636
|
+
const controlEngine = await engineOf(control);
|
|
1637
|
+
|
|
1638
|
+
// 1. global ON, A override off, B follows -> A white, B dark.
|
|
1639
|
+
registerAccount('vercel', 'x-a');
|
|
1640
|
+
updateContentOverride(findAccount('vercel', 'x-a'), 'darkMode', 'off');
|
|
1641
|
+
const aOff = await mk('x-a', toUrl(p1));
|
|
1642
|
+
const bFollowOn = await mk('x-b', toUrl(p1));
|
|
1643
|
+
await sleep(3200);
|
|
1644
|
+
const aOffBg = await bgOfTab(aOff); const bOnBg = await bgOfTab(bFollowOn);
|
|
1645
|
+
check('per-account-dark-override-off-while-global-on', isWhite(aOffBg) && isDark(bOnBg), `A=${aOffBg} B=${bOnBg}`);
|
|
1646
|
+
|
|
1647
|
+
// 2. global OFF, A override on, B follows -> A dark, B white (Q2: true override, both directions).
|
|
1648
|
+
store.settings.darkMode = false;
|
|
1649
|
+
const cOff = await mk('x-c', toUrl(p1));
|
|
1650
|
+
registerAccount('vercel', 'x-d');
|
|
1651
|
+
updateContentOverride(findAccount('vercel', 'x-d'), 'darkMode', 'on');
|
|
1652
|
+
const dOn = await mk('x-d', toUrl(p1));
|
|
1653
|
+
await sleep(3200);
|
|
1654
|
+
const cOffBg = await bgOfTab(cOff); const dOnBg = await bgOfTab(dOn);
|
|
1655
|
+
check('per-account-dark-override-on-while-global-off', isDark(dOnBg) && isWhite(cOffBg), `C(global-off)=${cOffBg} D(override-on)=${dOnBg}`);
|
|
1656
|
+
|
|
1657
|
+
// 3. follow-global is byte-for-byte the control view: same computed colour AND same engine presence.
|
|
1658
|
+
store.settings.darkMode = true;
|
|
1659
|
+
const follow = await mk('x-e', toUrl(p1));
|
|
1660
|
+
await sleep(3200);
|
|
1661
|
+
const followBg = await bgOfTab(follow); const followEngine = await engineOf(follow);
|
|
1662
|
+
check('per-account-dark-follow-global',
|
|
1663
|
+
followBg === controlDark && followEngine === controlEngine && followEngine === 'object',
|
|
1664
|
+
`follow=${followBg}/${followEngine} control=${controlDark}/${controlEngine}`);
|
|
1665
|
+
|
|
1666
|
+
// 4. override change applies to the OPEN document with no reload: the h1 keeps an in-page marker
|
|
1667
|
+
// (a reload would drop it), while the colour returns to white. B (untouched) keeps its colour.
|
|
1668
|
+
await follow.view.webContents.executeJavaScript(`document.querySelector('h1').setAttribute('data-marker','kept')`).catch(() => {});
|
|
1669
|
+
const bBefore4 = await bgOfTab(bFollowOn);
|
|
1670
|
+
setAccountContentOverride(vm, 'vercel', 'x-e', 'darkMode', 'off');
|
|
1671
|
+
await sleep(1800);
|
|
1672
|
+
const marker = await follow.view.webContents.executeJavaScript(`document.querySelector('h1') && document.querySelector('h1').getAttribute('data-marker')`).catch(() => null);
|
|
1673
|
+
const followBg4 = await bgOfTab(follow); const bAfter4 = await bgOfTab(bFollowOn);
|
|
1674
|
+
check('per-account-override-change-applies-without-reload',
|
|
1675
|
+
followBg4 !== '' && isWhite(followBg4) && marker === 'kept' && bAfter4 === bBefore4,
|
|
1676
|
+
`marker=${marker} bg=${followBg4} B before/after=${bBefore4}/${bAfter4}`);
|
|
1677
|
+
|
|
1678
|
+
// 5. an untouched account is unchanged by a sibling's override: compare a following tab with the
|
|
1679
|
+
// control view created before ANY override existed.
|
|
1680
|
+
const untouched = await mk('x-f', toUrl(p1));
|
|
1681
|
+
await sleep(3200);
|
|
1682
|
+
const untouchedBg = await bgOfTab(untouched); const untouchedEngine = await engineOf(untouched);
|
|
1683
|
+
check('per-account-dark-untouched-account-unchanged',
|
|
1684
|
+
untouchedBg === controlDark && untouchedEngine === controlEngine,
|
|
1685
|
+
`untouched=${untouchedBg}/${untouchedEngine} control=${controlDark}/${controlEngine}`);
|
|
1686
|
+
|
|
1687
|
+
// ---------- ad-block: the offline ERR_BLOCKED_BY_CLIENT / ERR_NAME_NOT_RESOLVED discriminator ----------
|
|
1688
|
+
// A seed-listed ad host whose SUBDOMAIN cannot resolve. hostBlocked() walks parent domains, so it
|
|
1689
|
+
// is blocked by the predicate, while DNS guarantees the "not blocked" answer is a different code.
|
|
1690
|
+
const ADHOST = 'mam011-smoke-no-such-host.doubleclick.net';
|
|
1691
|
+
const frameCode = async t => {
|
|
1692
|
+
const wc = t.view.webContents;
|
|
1693
|
+
const got = await new Promise(resolve => {
|
|
1694
|
+
const seen = [];
|
|
1695
|
+
const onFail = (_e, code, desc, _vurl, isMainFrame) => { if (!isMainFrame) seen.push(desc); };
|
|
1696
|
+
wc.on('did-fail-load', onFail);
|
|
1697
|
+
wc.executeJavaScript(`(() => { const old = document.getElementById('mam-ad-probe'); if (old) old.remove();
|
|
1698
|
+
const f = document.createElement('iframe'); f.id = 'mam-ad-probe';
|
|
1699
|
+
f.src = 'https://${ADHOST}/ad.html'; document.body.appendChild(f); return f.src; })()`).catch(() => {});
|
|
1700
|
+
setTimeout(() => { wc.removeListener('did-fail-load', onFail); resolve(seen); }, 6000);
|
|
1701
|
+
});
|
|
1702
|
+
return got[0] || 'none';
|
|
1703
|
+
};
|
|
1704
|
+
// A fresh document is what makes the check independent of earlier iframes: reload once, then probe.
|
|
1705
|
+
const reloadAndProbe = async t => {
|
|
1706
|
+
const wc = t.view.webContents;
|
|
1707
|
+
await new Promise(r => { wc.once('did-finish-load', r); wc.reload(); });
|
|
1708
|
+
await sleep(600);
|
|
1709
|
+
return frameCode(t);
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
// 6. global ON, A override off, B follows -> A is NOT blocked, B IS blocked.
|
|
1713
|
+
store.settings.adBlock = true;
|
|
1714
|
+
registerAccount('vercel', 'x-g');
|
|
1715
|
+
updateContentOverride(findAccount('vercel', 'x-g'), 'adBlock', 'off');
|
|
1716
|
+
const gOff = await mk('x-g', toUrl(p1));
|
|
1717
|
+
await sleep(800);
|
|
1718
|
+
const gCode = await reloadAndProbe(gOff);
|
|
1719
|
+
const bCode = await reloadAndProbe(bFollowOn);
|
|
1720
|
+
check('per-account-adblock-override-off-while-global-on',
|
|
1721
|
+
gCode === 'ERR_NAME_NOT_RESOLVED' && bCode === 'ERR_BLOCKED_BY_CLIENT', `G(override-off)=${gCode} B(follow)=${bCode}`);
|
|
1722
|
+
|
|
1723
|
+
// 7. global OFF, D override on (D already exists with a dark override), E follows -> D blocked, C not.
|
|
1724
|
+
store.settings.adBlock = false;
|
|
1725
|
+
updateContentOverride(findAccount('vercel', 'x-d'), 'adBlock', 'on');
|
|
1726
|
+
const dCode = await reloadAndProbe(dOn);
|
|
1727
|
+
const cCode2 = await reloadAndProbe(cOff);
|
|
1728
|
+
check('per-account-adblock-override-on-while-global-off',
|
|
1729
|
+
dCode === 'ERR_BLOCKED_BY_CLIENT' && cCode2 === 'ERR_NAME_NOT_RESOLVED', `D(override-on)=${dCode} C(global-off)=${cCode2}`);
|
|
1730
|
+
|
|
1731
|
+
// 8. follow-global: an un-overridden account produces exactly the code the global dictates. With
|
|
1732
|
+
// the global back on, that is BLOCKED — the same answer the control view would give.
|
|
1733
|
+
store.settings.adBlock = true;
|
|
1734
|
+
const fCode = await reloadAndProbe(follow); // x-e has only a darkMode override: adBlock follows
|
|
1735
|
+
check('per-account-adblock-follow-global', fCode === 'ERR_BLOCKED_BY_CLIENT', `E(follow, global on)=${fCode}`);
|
|
1736
|
+
|
|
1737
|
+
// 9. a LIVE flip changes the next request's outcome with no reload: x-g is overridden off; clearing
|
|
1738
|
+
// the override (it follows the on global) blocks the SAME host, in the SAME document. A page
|
|
1739
|
+
// marker proves the document survived — a URL comparison alone would NOT, because a reload keeps
|
|
1740
|
+
// the URL (mutation 4 caught exactly that: it reloaded on the on-flip and still passed).
|
|
1741
|
+
await gOff.view.webContents.executeJavaScript(`window.__mamFlipMark = 'alive'`).catch(() => {});
|
|
1742
|
+
const urlBefore9 = gOff.view.webContents.getURL();
|
|
1743
|
+
setAccountContentOverride(vm, 'vercel', 'x-g', 'adBlock', 'follow');
|
|
1744
|
+
await sleep(400);
|
|
1745
|
+
const gCodeLive = await frameCode(gOff);
|
|
1746
|
+
const urlAfter9 = gOff.view.webContents.getURL();
|
|
1747
|
+
const flipMark = await gOff.view.webContents.executeJavaScript(`window.__mamFlipMark || 'gone'`).catch(() => 'gone');
|
|
1748
|
+
check('adblock-live-flip-no-reload',
|
|
1749
|
+
gCodeLive === 'ERR_BLOCKED_BY_CLIENT' && urlBefore9 === urlAfter9 && flipMark === 'alive',
|
|
1750
|
+
`after=${gCodeLive} url ${urlBefore9 === urlAfter9 ? 'unchanged' : 'CHANGED'} document=${flipMark}`);
|
|
1751
|
+
|
|
1752
|
+
// 10. cosmetic per account: applyToPage hides the ad slot for an on-account and leaves it for an
|
|
1753
|
+
// off-account. The seed list carries no generic cosmetics (they come from a fetched EasyList),
|
|
1754
|
+
// so the check installs ONE known selector into the live filter set for the duration of the
|
|
1755
|
+
// assertion and restores it in a finally — the module's bytes are untouched (it is frozen), and
|
|
1756
|
+
// the check then measures the real did-finish-load apply for two accounts that resolve
|
|
1757
|
+
// differently. `display:none` vs `display:block` is the observable.
|
|
1758
|
+
const cosFile = path.join(app.getPath('temp'), 'mam-content-cosmetic.html');
|
|
1759
|
+
fs.writeFileSync(cosFile, '<!doctype html><html><head><style>#ad-slot{display:block;height:10px;width:10px}</style></head><body><div id="ad-slot"></div><h1>probe</h1></body></html>');
|
|
1760
|
+
const savedCosmetic = content.filters.cosmetic;
|
|
1761
|
+
content.filters.cosmetic = ['#ad-slot'];
|
|
1762
|
+
let cosOn = ''; let cosOff = ''; let cosOffTab = null;
|
|
1763
|
+
try {
|
|
1764
|
+
const onT = await mk('x-h', toUrl(cosFile));
|
|
1765
|
+
await sleep(900);
|
|
1766
|
+
cosOn = await onT.view.webContents.executeJavaScript(`getComputedStyle(document.getElementById('ad-slot')).display`).catch(() => '');
|
|
1767
|
+
registerAccount('vercel', 'x-i');
|
|
1768
|
+
updateContentOverride(findAccount('vercel', 'x-i'), 'adBlock', 'off');
|
|
1769
|
+
const offT = await mk('x-i', toUrl(cosFile));
|
|
1770
|
+
await sleep(900);
|
|
1771
|
+
cosOff = await offT.view.webContents.executeJavaScript(`getComputedStyle(document.getElementById('ad-slot')).display`).catch(() => '');
|
|
1772
|
+
cosOffTab = offT; // used by check 11 below
|
|
1773
|
+
} finally { content.filters.cosmetic = savedCosmetic; }
|
|
1774
|
+
check('adblock-cosmetic-per-account', cosOn === 'none' && cosOff === 'block',
|
|
1775
|
+
`following(adBlock on)=${cosOn} overridden-off=${cosOff}`);
|
|
1776
|
+
|
|
1777
|
+
// 11. a reload caused by ANOTHER account's ad-block change never reaches this account's document.
|
|
1778
|
+
// Toggling the GLOBAL off and back on makes every FOLLOWING account's resolved value move
|
|
1779
|
+
// (off, then on: the "on" half re-applies cosmetics, the "off" half reloads), while x-i is
|
|
1780
|
+
// overridden off and resolves `false` at both points — so it must not reload. A page-global
|
|
1781
|
+
// marker survives a non-reload and dies on a reload.
|
|
1782
|
+
const offTab = cosOffTab;
|
|
1783
|
+
await gOff.view.webContents.executeJavaScript(`window.__mamMark = 'alive'`).catch(() => {});
|
|
1784
|
+
await offTab.view.webContents.executeJavaScript(`window.__mamMark = 'alive'`).catch(() => {});
|
|
1785
|
+
setGlobalContentSetting(vm, 'adBlock', false);
|
|
1786
|
+
await sleep(500);
|
|
1787
|
+
setGlobalContentSetting(vm, 'adBlock', true);
|
|
1788
|
+
await sleep(2000);
|
|
1789
|
+
const gMark = await gOff.view.webContents.executeJavaScript(`window.__mamMark || 'gone'`).catch(() => 'gone');
|
|
1790
|
+
const iMark = await offTab.view.webContents.executeJavaScript(`window.__mamMark || 'gone'`).catch(() => 'gone');
|
|
1791
|
+
check('adblock-off-account-unaffected-by-other-accounts-reload', gMark === 'gone' && iMark === 'alive',
|
|
1792
|
+
`following(${gMark}) overridden-off(${iMark})`);
|
|
1793
|
+
|
|
1794
|
+
// ---------- data / API contracts ----------
|
|
1795
|
+
// 12. sparse storage: setting one key never creates the other, and clearing deletes the key.
|
|
1796
|
+
registerAccount('vercel', 'x-j');
|
|
1797
|
+
setAccountContentOverride(vm, 'vercel', 'x-j', 'adBlock', 'off');
|
|
1798
|
+
const jAcc = findAccount('vercel', 'x-j');
|
|
1799
|
+
check('content-override-stored-sparse',
|
|
1800
|
+
JSON.stringify(jAcc.contentOverride) === JSON.stringify({ adBlock: 'off' }),
|
|
1801
|
+
JSON.stringify(jAcc.contentOverride));
|
|
1802
|
+
|
|
1803
|
+
// 13. the owner's bar, mechanically: set then clear leaves accounts.json byte-identical.
|
|
1804
|
+
const accFile = path.join(app.getPath('userData'), 'accounts.json');
|
|
1805
|
+
registerAccount('vercel', 'x-k');
|
|
1806
|
+
const before13 = fs.readFileSync(accFile);
|
|
1807
|
+
setAccountContentOverride(vm, 'vercel', 'x-k', 'darkMode', 'on');
|
|
1808
|
+
setAccountContentOverride(vm, 'vercel', 'x-k', 'darkMode', 'follow');
|
|
1809
|
+
const after13 = fs.readFileSync(accFile);
|
|
1810
|
+
check('content-override-clear-is-byte-identical', before13.equals(after13),
|
|
1811
|
+
`${before13.length} vs ${after13.length} bytes; key present after clear: ${'contentOverride' in findAccount('vercel', 'x-k')}`);
|
|
1812
|
+
|
|
1813
|
+
// 14. the IPC rejects an unknown name and an unknown account (mirrors account-no-autofill).
|
|
1814
|
+
const throws = fn => { try { fn(); return false; } catch { return true; } };
|
|
1815
|
+
check('content-override-rejects-unknown-name', throws(() => setAccountContentOverride(vm, 'vercel', 'x-j', 'network', 'on')));
|
|
1816
|
+
check('content-override-rejects-unknown-account', throws(() => setAccountContentOverride(vm, 'vercel', 'nobody', 'adBlock', 'on')));
|
|
1817
|
+
// 15. only on / off / follow are accepted — and nothing is written when the value is refused.
|
|
1818
|
+
const jBefore = JSON.stringify(findAccount('vercel', 'x-j').contentOverride);
|
|
1819
|
+
check('content-override-invalid-value-rejected',
|
|
1820
|
+
throws(() => setAccountContentOverride(vm, 'vercel', 'x-j', 'adBlock', 'yes'))
|
|
1821
|
+
&& JSON.stringify(findAccount('vercel', 'x-j').contentOverride) === jBefore,
|
|
1822
|
+
`stored=${jBefore}`);
|
|
1823
|
+
|
|
1824
|
+
// ---------- the resolver contract (MUTATION-PLUS) ----------
|
|
1825
|
+
// The follow path must be derived from the GLOBAL, never a hardcoded literal. The spec's own
|
|
1826
|
+
// formula is `!!settings[name]`, so the returned value is the global's truthiness — not the raw
|
|
1827
|
+
// value (a string fake therefore reads `true`, and asserting raw identity would be asserting
|
|
1828
|
+
// against the contract). What a literal default cannot do is FOLLOW the global in both directions:
|
|
1829
|
+
// a resolver hardcoded to true fails the first half, one hardcoded to false fails the second.
|
|
1830
|
+
const followOn = resolveContent({}, 'adBlock', { adBlock: true });
|
|
1831
|
+
const followOff = resolveContent({ contentOverride: {} }, 'adBlock', { adBlock: false });
|
|
1832
|
+
const followUnset = resolveContent({}, 'darkMode', {});
|
|
1833
|
+
const followOverridden = resolveContent({ contentOverride: { adBlock: 'on' } }, 'adBlock', { adBlock: false });
|
|
1834
|
+
check('resolver-follow-tracks-the-global-not-a-literal',
|
|
1835
|
+
followOn === true && followOff === false && followUnset === false && followOverridden === true,
|
|
1836
|
+
`global true->${followOn}, global false->${followOff}, unset->${followUnset}, override-on-over-global-off->${followOverridden}`);
|
|
1837
|
+
|
|
1459
1838
|
fs.writeFileSync(path.join(__dirname, '..', 'smoke-report.json'), JSON.stringify(results, null, 2));
|
|
1460
1839
|
app.exit(results.checks.every(c => c.ok) ? 0 : 1);
|
|
1461
1840
|
}
|
|
@@ -1615,6 +1994,55 @@ function runInteractive(captureMode = false) {
|
|
|
1615
1994
|
console.log('[capture] demo notice:', plan.kind, (plan.choices || []).length);
|
|
1616
1995
|
await new Promise(r => setTimeout(r, 700));
|
|
1617
1996
|
}
|
|
1997
|
+
// --demo-home-marker: give two demo accounts one override each through the REAL path, so the
|
|
1998
|
+
// Home grid shows the "content: custom" marker the way a user would see it (0.11.0).
|
|
1999
|
+
if (process.argv.includes('--demo-home-marker')) {
|
|
2000
|
+
try {
|
|
2001
|
+
if (findAccount('vercel', 'work')) setAccountContentOverride(vm, 'vercel', 'work', 'darkMode', 'off');
|
|
2002
|
+
if (findAccount('netlify', 'agency')) setAccountContentOverride(vm, 'netlify', 'agency', 'adBlock', 'on');
|
|
2003
|
+
} catch (err) { console.error('[demo] home marker failed:', err.message); }
|
|
2004
|
+
vm.emitState();
|
|
2005
|
+
await new Promise(r => setTimeout(r, 800));
|
|
2006
|
+
const info = await win.webContents.executeJavaScript(`JSON.stringify([...document.querySelectorAll('.card .contentcustom')].map(m => {
|
|
2007
|
+
const r = m.getBoundingClientRect();
|
|
2008
|
+
return m.closest('.card').querySelector('.label').innerText + ' :: ' + m.innerText + ' @' + Math.round(r.x) + ',' + Math.round(r.y);
|
|
2009
|
+
}))`).catch(err => 'probe failed: ' + err.message);
|
|
2010
|
+
console.log('[capture] home markers:', info);
|
|
2011
|
+
await new Promise(r => setTimeout(r, 300));
|
|
2012
|
+
}
|
|
2013
|
+
// --demo-content: give the account in --capture-open one override through the REAL path, then
|
|
2014
|
+
// open its gear window from the RENDERER, so the screenshot shows the real three-way selects,
|
|
2015
|
+
// the worded resolved state and the honest note (0.11.0). Drives state from main, never from
|
|
2016
|
+
// --capture-js (which intermittently exits 127 on this machine). Scratch demo profile only.
|
|
2017
|
+
if (process.argv.includes('--demo-content') && openKey) {
|
|
2018
|
+
const [dsvc, did] = openKey.split('::');
|
|
2019
|
+
try { setAccountContentOverride(vm, dsvc, did, 'darkMode', 'off'); } catch (err) { console.error('[demo] content override failed:', err.message); }
|
|
2020
|
+
vm.emitState();
|
|
2021
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2022
|
+
if (win.webContents && !win.webContents.isDestroyed()) {
|
|
2023
|
+
await win.webContents.executeJavaScript(`document.querySelector('[data-gear="${openKey}"]') && document.querySelector('[data-gear="${openKey}"]').click()`).catch(err => console.error('[demo] gear click failed:', err.message));
|
|
2024
|
+
}
|
|
2025
|
+
await new Promise(r => setTimeout(r, 900));
|
|
2026
|
+
const info = await win.webContents.executeJavaScript(`JSON.stringify({
|
|
2027
|
+
modalOpen: !document.getElementById('modal-back').hidden,
|
|
2028
|
+
selects: [...document.querySelectorAll('#modal select')].map(s => ({ id: s.id, value: s.value, options: s.options.length })),
|
|
2029
|
+
states: [...document.querySelectorAll('#modal .contentctl .note')].map(n => n.textContent.trim()),
|
|
2030
|
+
note: (() => { const ns = [...document.querySelectorAll('#modal .note')]; const n = ns.find(x => /both directions/.test(x.textContent)); return n ? n.textContent.trim().slice(0, 120) : ''; })(),
|
|
2031
|
+
})`).catch(err => 'probe failed: ' + err.message);
|
|
2032
|
+
console.log('[capture] demo content:', info);
|
|
2033
|
+
// drive one select through the REAL renderer → preload → main chain and read the store back
|
|
2034
|
+
const realPath = await win.webContents.executeJavaScript(`(async () => {
|
|
2035
|
+
const sel = document.getElementById('m-content-adBlock');
|
|
2036
|
+
const before = sel.value;
|
|
2037
|
+
sel.value = 'off'; sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2038
|
+
await new Promise(r => setTimeout(r, 900));
|
|
2039
|
+
return JSON.stringify({ before, after: sel.value, stateLine: document.getElementById('m-content-adBlock-state').textContent.trim() });
|
|
2040
|
+
})()`).catch(err => 'real-path probe failed: ' + err.message);
|
|
2041
|
+
console.log('[capture] demo content real path:', realPath, 'stored:', JSON.stringify((findAccount(openKey.split('::')[0], openKey.split('::')[1]) || {}).contentOverride));
|
|
2042
|
+
// scroll the modal to its end so the honest note under the selects is in frame
|
|
2043
|
+
await win.webContents.executeJavaScript(`(() => { const m = document.getElementById('modal'); if (m) m.scrollTop = m.scrollHeight; return m ? m.scrollTop : -1; })()`).catch(() => {});
|
|
2044
|
+
await new Promise(r => setTimeout(r, 400));
|
|
2045
|
+
}
|
|
1618
2046
|
// --demo-picker: open the address-strip key chip's picker from MAIN (state → emitState → the
|
|
1619
2047
|
// renderer's own click handler), so a screenshot shows the real popover and not hand-written
|
|
1620
2048
|
// DOM. The chip is drawn from state.siteLogins, so one emitState is enough. Verification aid
|
|
@@ -2052,6 +2480,14 @@ function runInteractive(captureMode = false) {
|
|
|
2052
2480
|
vm.emitState();
|
|
2053
2481
|
return acc.noAutoFill;
|
|
2054
2482
|
});
|
|
2483
|
+
// Per-account content override (0.11.0): 'follow' | 'on' | 'off' per control, stored SPARSELY on
|
|
2484
|
+
// the account record (see updateContentOverride). 'follow' deletes the key, so clearing restores
|
|
2485
|
+
// the account's stored bytes. An override is a TRUE override in both directions (owner decision
|
|
2486
|
+
// Q2): it may turn a control on while the app-wide toggle is off, and vice versa. The change is
|
|
2487
|
+
// applied to that account's live tab immediately; every other tab is re-resolved and acted on only
|
|
2488
|
+
// if ITS value actually moved (an untouched account is never reloaded or re-engaged).
|
|
2489
|
+
ipcMain.handle('account-content-override', (_e, svc, id, name, value) =>
|
|
2490
|
+
setAccountContentOverride(vm, svc, id, name, value));
|
|
2055
2491
|
// Dismiss the per-tab notice (banner X).
|
|
2056
2492
|
ipcMain.handle('dismiss-notice', (_e, key) => { vm.setNotice(key, null); return true; });
|
|
2057
2493
|
// Undo an auto-fill: the preload clears exactly what it filled and answers with what it cleared.
|
|
@@ -2154,31 +2590,7 @@ function runInteractive(captureMode = false) {
|
|
|
2154
2590
|
vm.emitState();
|
|
2155
2591
|
return true;
|
|
2156
2592
|
});
|
|
2157
|
-
ipcMain.handle('set-content-setting', (_e, name, value) =>
|
|
2158
|
-
if (!['adBlock', 'darkMode', 'saveLogins', 'autofill', 'autoUpdate'].includes(name)) throw new Error(`unknown setting: ${name}`);
|
|
2159
|
-
store.settings[name] = !!value;
|
|
2160
|
-
if (name === 'autoUpdate') { store.save(); updater.setAutoUpdate(!!value); vm.emitState(); return !!value; }
|
|
2161
|
-
store.save();
|
|
2162
|
-
if (name === 'darkMode') {
|
|
2163
|
-
// The engine runs in each view's isolated world, so a flip is one enable/disable call per
|
|
2164
|
-
// live tab — no reload. The previous extension path forced a reload of every open tab, which
|
|
2165
|
-
// threw away scroll position and anything typed into a half-filled form.
|
|
2166
|
-
const on = !!value;
|
|
2167
|
-
for (const t of vm.tabs.values()) {
|
|
2168
|
-
if (on) dark.enable(t.view.webContents);
|
|
2169
|
-
else dark.disable(t.view.webContents);
|
|
2170
|
-
}
|
|
2171
|
-
} else if (name === 'adBlock') {
|
|
2172
|
-
// the network layer flips live through its closure; cosmetic CSS needs a re-apply on, and a
|
|
2173
|
-
// reload off (injected CSS can only be undone by reloading)
|
|
2174
|
-
for (const t of vm.tabs.values()) {
|
|
2175
|
-
if (value) content.applyToPage(t.view.webContents, { adBlock: true });
|
|
2176
|
-
else t.view.webContents.reload();
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
vm.emitState();
|
|
2180
|
-
return store.settings[name];
|
|
2181
|
-
});
|
|
2593
|
+
ipcMain.handle('set-content-setting', (_e, name, value) => setGlobalContentSetting(vm, name, value));
|
|
2182
2594
|
ipcMain.handle('content-stats', () => ({
|
|
2183
2595
|
blocked: content.stats.blocked,
|
|
2184
2596
|
listSource: content.filters.loadedFrom,
|
|
@@ -2272,6 +2684,8 @@ function runInteractive(captureMode = false) {
|
|
|
2272
2684
|
module.exports = {
|
|
2273
2685
|
accountLoginSummaries, managerRows, setOrClearDefault, resolveAmong, fillCandidatesFor, autoFillPlan,
|
|
2274
2686
|
originAllowed, accountKey,
|
|
2687
|
+
resolveContent, updateContentOverride, contentOverrideFor,
|
|
2688
|
+
setAccountContentOverride, setGlobalContentSetting, applyContentToFreshDocument, reactToContentChange, contentSnapshot,
|
|
2275
2689
|
_creds: creds,
|
|
2276
2690
|
};
|
|
2277
2691
|
|