pinokiod 8.0.78 → 8.0.80
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/kernel/api/notify/index.js +2 -1
- package/kernel/vault/automatic_scans.js +6 -7
- package/kernel/vault/index.js +59 -16
- package/kernel/vault/registry.js +2 -0
- package/kernel/vault/registry_core.js +39 -0
- package/package.json +1 -1
- package/server/index.js +5 -2
- package/server/public/tab-idle-notifier.js +151 -11
- package/server/public/tab-link-popover.css +128 -0
- package/server/public/tab-link-popover.js +36 -4
- package/server/public/vault.js +69 -14
- package/server/views/app.ejs +240 -40
- package/test/launch-settings-ui.test.js +37 -5
- package/test/tab-notification-settings.test.js +135 -0
- package/test/vault-automatic-scans.test.js +1 -0
- package/test/vault-engine.test.js +151 -12
- package/test/vault-ui.test.js +150 -16
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const fs = require("fs")
|
|
2
2
|
const path = require("path")
|
|
3
3
|
const {
|
|
4
|
-
SIZE_THRESHOLD,
|
|
5
4
|
isCandidateFileSize,
|
|
6
5
|
SHA256_RE,
|
|
7
6
|
ENTRY_BATCH_SIZE
|
|
@@ -276,7 +275,7 @@ class AutomaticScans {
|
|
|
276
275
|
if (!collected.size) this.discardChangedPaths(app)
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
async candidatePolicy() {
|
|
278
|
+
async candidatePolicy(app = null) {
|
|
280
279
|
const cached = this.vault.lastScanCache &&
|
|
281
280
|
typeof this.vault.lastScanCache.get === "function"
|
|
282
281
|
? this.vault.lastScanCache.get("")
|
|
@@ -284,13 +283,13 @@ class AutomaticScans {
|
|
|
284
283
|
const scan = cached || (this.vault.registry
|
|
285
284
|
? await this.vault.registry.scanFor()
|
|
286
285
|
: null)
|
|
287
|
-
const threshold =
|
|
286
|
+
const threshold = app
|
|
287
|
+
? await this.vault.candidateSizeForApp(app)
|
|
288
|
+
: await this.vault.candidateSizeSetting(null)
|
|
288
289
|
const hasLinkableDevices = !!(
|
|
289
290
|
scan && Array.isArray(scan.linkable_devices))
|
|
290
291
|
return {
|
|
291
|
-
threshold
|
|
292
|
-
? threshold
|
|
293
|
-
: SIZE_THRESHOLD,
|
|
292
|
+
threshold,
|
|
294
293
|
linkability_known: hasLinkableDevices,
|
|
295
294
|
linkable_devices: new Set(hasLinkableDevices
|
|
296
295
|
? scan.linkable_devices
|
|
@@ -1039,7 +1038,7 @@ class AutomaticScans {
|
|
|
1039
1038
|
unstable_hashes: 0
|
|
1040
1039
|
}
|
|
1041
1040
|
markStage("policy-read")
|
|
1042
|
-
const policy = await this.candidatePolicy()
|
|
1041
|
+
const policy = await this.candidatePolicy(app)
|
|
1043
1042
|
const threshold = await this.candidateThreshold(policy)
|
|
1044
1043
|
const linkableDevices = policy.linkable_devices
|
|
1045
1044
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
package/kernel/vault/index.js
CHANGED
|
@@ -1899,6 +1899,25 @@ class Vault {
|
|
|
1899
1899
|
}
|
|
1900
1900
|
return this.runMutation(() =>
|
|
1901
1901
|
this.removeExternalSource(payload.source_id))
|
|
1902
|
+
case "set_candidate_size": {
|
|
1903
|
+
if (payload.scope_id != null &&
|
|
1904
|
+
typeof payload.scope_id !== "string") {
|
|
1905
|
+
return { error: "Choose a valid scan scope." }
|
|
1906
|
+
}
|
|
1907
|
+
const scopeId = typeof payload.scope_id === "string" && payload.scope_id
|
|
1908
|
+
? payload.scope_id
|
|
1909
|
+
: null
|
|
1910
|
+
if (scopeId) {
|
|
1911
|
+
const source = this.scanSource(scopeId)
|
|
1912
|
+
if (!source || source.kind !== "app") {
|
|
1913
|
+
return { error: "That app is no longer available." }
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return this.setCandidateSizeSetting(
|
|
1917
|
+
scopeId,
|
|
1918
|
+
payload.candidate_size
|
|
1919
|
+
)
|
|
1920
|
+
}
|
|
1902
1921
|
case "scan": {
|
|
1903
1922
|
const scanSource = payload.scope_id
|
|
1904
1923
|
? this.scanSource(payload.scope_id)
|
|
@@ -1913,22 +1932,21 @@ class Vault {
|
|
|
1913
1932
|
code: "global_scan_required"
|
|
1914
1933
|
}
|
|
1915
1934
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1921
|
-
threshold = payload.candidate_size
|
|
1922
|
-
}
|
|
1935
|
+
const settingScopeId = scanSource && scanSource.kind === "app"
|
|
1936
|
+
? scanSource.id
|
|
1937
|
+
: null
|
|
1938
|
+
const threshold = await this.candidateSizeSetting(settingScopeId)
|
|
1923
1939
|
return this.startScan(payload.scope_id || null, threshold)
|
|
1924
1940
|
}
|
|
1925
1941
|
case "cancel_scan":
|
|
1926
1942
|
return this.cancelScan()
|
|
1927
|
-
case "find_folders":
|
|
1943
|
+
case "find_folders": {
|
|
1944
|
+
const threshold = await this.candidateSizeSetting(null)
|
|
1928
1945
|
return this.startFolderDiscovery(
|
|
1929
1946
|
payload.path,
|
|
1930
|
-
|
|
1947
|
+
threshold
|
|
1931
1948
|
)
|
|
1949
|
+
}
|
|
1932
1950
|
case "cancel_find_folders":
|
|
1933
1951
|
return this.cancelFolderDiscovery()
|
|
1934
1952
|
case "clear_find_folders":
|
|
@@ -3115,6 +3133,33 @@ class Vault {
|
|
|
3115
3133
|
return scoped
|
|
3116
3134
|
}
|
|
3117
3135
|
|
|
3136
|
+
async candidateSizeSetting(scopeId = null) {
|
|
3137
|
+
const setting = await this.registry.scanSetting(scopeId)
|
|
3138
|
+
const minimum = setting && Number(setting.candidate_min_bytes)
|
|
3139
|
+
if (CANDIDATE_SIZE_OPTIONS.includes(minimum)) return minimum
|
|
3140
|
+
return SIZE_THRESHOLD
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
async setCandidateSizeSetting(scopeId = null, candidateMinBytes) {
|
|
3144
|
+
if (!Number.isSafeInteger(candidateMinBytes) ||
|
|
3145
|
+
!CANDIDATE_SIZE_OPTIONS.includes(candidateMinBytes)) {
|
|
3146
|
+
return { error: "Choose a valid minimum file size." }
|
|
3147
|
+
}
|
|
3148
|
+
const setting = await this.registry.setScanSetting(
|
|
3149
|
+
scopeId,
|
|
3150
|
+
candidateMinBytes
|
|
3151
|
+
)
|
|
3152
|
+
return {
|
|
3153
|
+
scope_id: scopeId || null,
|
|
3154
|
+
candidate_min_bytes: setting.candidate_min_bytes,
|
|
3155
|
+
updated_at: setting.updated_at
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
candidateSizeForApp(app) {
|
|
3160
|
+
return this.candidateSizeSetting(sourceId("app", app))
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3118
3163
|
sourceCountMaps(summaryRows) {
|
|
3119
3164
|
const counts = {
|
|
3120
3165
|
all: {},
|
|
@@ -3391,13 +3436,10 @@ class Vault {
|
|
|
3391
3436
|
const lastScan = await this.scanForScope(scopeId)
|
|
3392
3437
|
const globalScan = scopeId ? await this.scanForScope(null) : lastScan
|
|
3393
3438
|
const globalScanReady = this.globalScanIsReady(globalScan)
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
publishedCandidateMinimum) && publishedCandidateMinimum >= 0
|
|
3399
|
-
? publishedCandidateMinimum
|
|
3400
|
-
: SIZE_THRESHOLD
|
|
3439
|
+
const candidateMinimum = await this.candidateSizeSetting(scopeId)
|
|
3440
|
+
const globalCandidateMinimum = scopeId
|
|
3441
|
+
? await this.candidateSizeSetting(null)
|
|
3442
|
+
: candidateMinimum
|
|
3401
3443
|
const before = lastScan && Number.isFinite(lastScan.bytes_total)
|
|
3402
3444
|
? lastScan.bytes_total
|
|
3403
3445
|
: 0
|
|
@@ -3435,6 +3477,7 @@ class Vault {
|
|
|
3435
3477
|
const result = {
|
|
3436
3478
|
enabled: true,
|
|
3437
3479
|
global_scan_ready: globalScanReady,
|
|
3480
|
+
candidate_min_bytes: candidateMinimum,
|
|
3438
3481
|
global_candidate_min_bytes: globalCandidateMinimum,
|
|
3439
3482
|
mode: this.mode,
|
|
3440
3483
|
scan: this.scanStatus(),
|
package/kernel/vault/registry.js
CHANGED
|
@@ -306,6 +306,13 @@ class RegistryCore {
|
|
|
306
306
|
FROM automatic_app_scans
|
|
307
307
|
WHERE state = 'paused';
|
|
308
308
|
|
|
309
|
+
CREATE TABLE IF NOT EXISTS minimum_size_settings (
|
|
310
|
+
scope_id TEXT PRIMARY KEY,
|
|
311
|
+
candidate_min_bytes INTEGER NOT NULL CHECK (
|
|
312
|
+
candidate_min_bytes >= 0
|
|
313
|
+
),
|
|
314
|
+
updated_at INTEGER NOT NULL
|
|
315
|
+
);
|
|
309
316
|
`)
|
|
310
317
|
const automaticScanColumns = new Set(this.database.prepare(
|
|
311
318
|
"PRAGMA table_info(automatic_app_scans)"
|
|
@@ -2670,6 +2677,38 @@ class RegistryCore {
|
|
|
2670
2677
|
})
|
|
2671
2678
|
}
|
|
2672
2679
|
|
|
2680
|
+
scanSetting(scopeId = null) {
|
|
2681
|
+
const row = this.database.prepare(`
|
|
2682
|
+
SELECT candidate_min_bytes, updated_at
|
|
2683
|
+
FROM minimum_size_settings
|
|
2684
|
+
WHERE scope_id = ?
|
|
2685
|
+
`).get(scopeId || "")
|
|
2686
|
+
return row ? {
|
|
2687
|
+
candidate_min_bytes: Number(row.candidate_min_bytes),
|
|
2688
|
+
updated_at: Number(row.updated_at) || 0
|
|
2689
|
+
} : null
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
setScanSetting(scopeId = null, candidateMinBytes) {
|
|
2693
|
+
const minimum = candidateMinBytes
|
|
2694
|
+
if (!Number.isSafeInteger(minimum) || minimum < 0) {
|
|
2695
|
+
throw new Error("Invalid minimum file size setting.")
|
|
2696
|
+
}
|
|
2697
|
+
const updatedAt = Date.now()
|
|
2698
|
+
this.database.prepare(`
|
|
2699
|
+
INSERT INTO minimum_size_settings(
|
|
2700
|
+
scope_id, candidate_min_bytes, updated_at
|
|
2701
|
+
) VALUES (?, ?, ?)
|
|
2702
|
+
ON CONFLICT(scope_id) DO UPDATE SET
|
|
2703
|
+
candidate_min_bytes = excluded.candidate_min_bytes,
|
|
2704
|
+
updated_at = excluded.updated_at
|
|
2705
|
+
`).run(scopeId || "", minimum, updatedAt)
|
|
2706
|
+
return {
|
|
2707
|
+
candidate_min_bytes: minimum,
|
|
2708
|
+
updated_at: updatedAt
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2673
2712
|
automaticAppScanStates() {
|
|
2674
2713
|
return this.database.prepare(`
|
|
2675
2714
|
SELECT app, state, signature, updated_at
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1870,8 +1870,10 @@ class Server {
|
|
|
1870
1870
|
}
|
|
1871
1871
|
return ""
|
|
1872
1872
|
}
|
|
1873
|
+
const launcherPath = this.kernel.path("api", name)
|
|
1874
|
+
const launcherDisplayPath = this.formatLogsDisplayPath(launcherPath)
|
|
1873
1875
|
const launcherRemote = this.kernel.api && typeof this.kernel.api.parentGitURI === "function"
|
|
1874
|
-
? (this.kernel.api.parentGitURI(
|
|
1876
|
+
? (this.kernel.api.parentGitURI(launcherPath) || "")
|
|
1875
1877
|
: ""
|
|
1876
1878
|
const launcherRemoteUrl = buildGitRemoteWebUrl(launcherRemote)
|
|
1877
1879
|
|
|
@@ -1926,7 +1928,8 @@ class Server {
|
|
|
1926
1928
|
dev_link,
|
|
1927
1929
|
// repos,
|
|
1928
1930
|
current_urls,
|
|
1929
|
-
path:
|
|
1931
|
+
path: launcherPath,
|
|
1932
|
+
launcher_path_display: launcherDisplayPath,
|
|
1930
1933
|
log_path: this.kernel.path("api", name, "logs"),
|
|
1931
1934
|
plugin_menu: null,
|
|
1932
1935
|
portal: this.portal,
|
|
@@ -276,17 +276,24 @@
|
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
278
|
const rect = anchor.getBoundingClientRect();
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
const top = rect.bottom + scrollY + 6;
|
|
282
|
-
let left = rect.left + scrollX;
|
|
279
|
+
const viewportPadding = 12;
|
|
280
|
+
const menuGap = 6;
|
|
283
281
|
const menuWidth = menu.offsetWidth || 0;
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
const menuHeight = menu.offsetHeight || 0;
|
|
283
|
+
let left = rect.left;
|
|
284
|
+
let top = rect.bottom + menuGap;
|
|
285
|
+
if (left + menuWidth > window.innerWidth - viewportPadding) {
|
|
286
|
+
left = Math.max(viewportPadding, window.innerWidth - menuWidth - viewportPadding);
|
|
287
|
+
}
|
|
288
|
+
if (left < viewportPadding) {
|
|
289
|
+
left = viewportPadding;
|
|
290
|
+
}
|
|
291
|
+
const topAbove = rect.top - menuHeight - menuGap;
|
|
292
|
+
if (top + menuHeight > window.innerHeight - viewportPadding && topAbove >= viewportPadding) {
|
|
293
|
+
top = topAbove;
|
|
294
|
+
} else {
|
|
295
|
+
top = Math.min(top, Math.max(viewportPadding, window.innerHeight - menuHeight - viewportPadding));
|
|
296
|
+
top = Math.max(viewportPadding, top);
|
|
290
297
|
}
|
|
291
298
|
menu.style.top = `${Math.round(top)}px`;
|
|
292
299
|
menu.style.left = `${Math.round(left)}px`;
|
|
@@ -857,10 +864,13 @@
|
|
|
857
864
|
color: var(--pinokio-focus-color, #4c9afe);
|
|
858
865
|
}
|
|
859
866
|
.pinokio-notify-popover {
|
|
860
|
-
position:
|
|
867
|
+
position: fixed;
|
|
861
868
|
z-index: 2147482000;
|
|
862
869
|
min-width: 220px;
|
|
863
870
|
max-width: 280px;
|
|
871
|
+
max-height: calc(100dvh - 24px);
|
|
872
|
+
overflow-y: auto;
|
|
873
|
+
overscroll-behavior: contain;
|
|
864
874
|
color: #f8fafc;
|
|
865
875
|
background: rgba(15, 23, 42, 0.97);
|
|
866
876
|
border-radius: 10px;
|
|
@@ -980,6 +990,133 @@ const syncToggleAppearance = (toggle, enabled) => {
|
|
|
980
990
|
});
|
|
981
991
|
};
|
|
982
992
|
|
|
993
|
+
const updateNotificationPreferenceForFrame = (frameName, enabled) => {
|
|
994
|
+
const state = getOrCreateState(frameName);
|
|
995
|
+
if (!state) {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
const next = Boolean(enabled);
|
|
999
|
+
state.notifyEnabled = next;
|
|
1000
|
+
setPreference(frameName, next);
|
|
1001
|
+
syncInlineToggleStateForFrame(frameName);
|
|
1002
|
+
return next;
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const populateInlineSoundSelect = (select, options) => {
|
|
1006
|
+
if (!(select instanceof HTMLSelectElement)) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const effectiveOptions = Array.isArray(options) && options.length > 0
|
|
1010
|
+
? options
|
|
1011
|
+
: baseSoundOptions();
|
|
1012
|
+
const selectedChoice = globalSoundPreference?.choice || SOUND_DEFAULT_CHOICE;
|
|
1013
|
+
select.replaceChildren();
|
|
1014
|
+
effectiveOptions.forEach((option) => {
|
|
1015
|
+
if (!option || !option.value || !option.label) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const node = document.createElement('option');
|
|
1019
|
+
node.value = option.value;
|
|
1020
|
+
node.textContent = option.label;
|
|
1021
|
+
node.selected = option.value === selectedChoice;
|
|
1022
|
+
select.appendChild(node);
|
|
1023
|
+
});
|
|
1024
|
+
if (Array.from(select.options).some((option) => option.value === selectedChoice)) {
|
|
1025
|
+
select.value = selectedChoice;
|
|
1026
|
+
} else {
|
|
1027
|
+
select.value = SOUND_DEFAULT_CHOICE;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const mountNotificationSettingsForLink = (link, container, callbacks = {}) => {
|
|
1032
|
+
if (!(container instanceof HTMLElement)) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
const context = getNotificationMenuStateForLink(link);
|
|
1036
|
+
if (!context.available || !context.frameName) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
const state = getOrCreateState(context.frameName);
|
|
1040
|
+
if (!state) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
container.replaceChildren();
|
|
1045
|
+
|
|
1046
|
+
const toggleRow = document.createElement('div');
|
|
1047
|
+
toggleRow.className = 'tab-link-notification-setting-row';
|
|
1048
|
+
const toggleLabel = document.createElement('span');
|
|
1049
|
+
toggleLabel.className = 'tab-link-notification-setting-label';
|
|
1050
|
+
toggleLabel.textContent = 'Notifications for this tab';
|
|
1051
|
+
const toggleControl = document.createElement('div');
|
|
1052
|
+
toggleControl.className = 'tab-link-notification-toggle-control';
|
|
1053
|
+
const toggleStatus = document.createElement('span');
|
|
1054
|
+
toggleStatus.className = 'tab-link-notification-toggle-status';
|
|
1055
|
+
const toggle = document.createElement('button');
|
|
1056
|
+
toggle.type = 'button';
|
|
1057
|
+
toggle.className = 'tab-link-notification-switch';
|
|
1058
|
+
toggle.setAttribute('role', 'switch');
|
|
1059
|
+
toggle.setAttribute('aria-label', 'Notifications for this tab');
|
|
1060
|
+
const toggleThumb = document.createElement('span');
|
|
1061
|
+
toggleThumb.className = 'tab-link-notification-switch-thumb';
|
|
1062
|
+
toggle.setAttribute('aria-describedby', `${container.id || 'tab-link-notification-settings'}-status`);
|
|
1063
|
+
toggleStatus.id = `${container.id || 'tab-link-notification-settings'}-status`;
|
|
1064
|
+
toggle.appendChild(toggleThumb);
|
|
1065
|
+
toggleControl.append(toggleStatus, toggle);
|
|
1066
|
+
toggleRow.append(toggleLabel, toggleControl);
|
|
1067
|
+
|
|
1068
|
+
const soundRow = document.createElement('label');
|
|
1069
|
+
soundRow.className = 'tab-link-notification-setting-row';
|
|
1070
|
+
const soundLabel = document.createElement('span');
|
|
1071
|
+
soundLabel.className = 'tab-link-notification-setting-label';
|
|
1072
|
+
soundLabel.textContent = 'Sound';
|
|
1073
|
+
const soundSelect = document.createElement('select');
|
|
1074
|
+
soundSelect.className = 'tab-link-notification-sound-select';
|
|
1075
|
+
soundSelect.setAttribute('aria-label', 'Notification sound');
|
|
1076
|
+
soundRow.append(soundLabel, soundSelect);
|
|
1077
|
+
|
|
1078
|
+
const syncToggle = (enabled) => {
|
|
1079
|
+
toggle.setAttribute('aria-checked', enabled ? 'true' : 'false');
|
|
1080
|
+
toggle.dataset.enabled = enabled ? 'true' : 'false';
|
|
1081
|
+
toggleStatus.textContent = enabled ? 'On' : 'Off';
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
syncToggle(Boolean(state.notifyEnabled));
|
|
1085
|
+
populateInlineSoundSelect(soundSelect, soundOptionsCache || baseSoundOptions());
|
|
1086
|
+
container.append(toggleRow, soundRow);
|
|
1087
|
+
|
|
1088
|
+
toggle.addEventListener('click', () => {
|
|
1089
|
+
const current = getOrCreateState(context.frameName);
|
|
1090
|
+
if (!current) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const enabled = updateNotificationPreferenceForFrame(context.frameName, !current.notifyEnabled);
|
|
1094
|
+
if (enabled === null) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
syncToggle(enabled);
|
|
1098
|
+
if (typeof callbacks.onEnabledChange === 'function') {
|
|
1099
|
+
callbacks.onEnabledChange(enabled);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
soundSelect.addEventListener('change', () => {
|
|
1104
|
+
applySoundSelection(soundSelect.value);
|
|
1105
|
+
if (typeof callbacks.onSoundChange === 'function') {
|
|
1106
|
+
callbacks.onSoundChange(globalSoundPreference.choice);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
loadSoundOptions().then((options) => {
|
|
1111
|
+
if (!container.isConnected || !container.contains(soundSelect)) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
populateInlineSoundSelect(soundSelect, options);
|
|
1115
|
+
}).catch(() => {});
|
|
1116
|
+
|
|
1117
|
+
return true;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
983
1120
|
const getNotificationMenuStateForLink = (link) => {
|
|
984
1121
|
if (!(link instanceof HTMLElement)) {
|
|
985
1122
|
return { available: false, enabled: true, frameName: null };
|
|
@@ -1498,5 +1635,8 @@ const ensureTabAccessories = aggregateDebounce(() => {
|
|
|
1498
1635
|
openMenuForLink(link, anchor) {
|
|
1499
1636
|
return openNotificationMenuForLink(link, anchor);
|
|
1500
1637
|
},
|
|
1638
|
+
mountSettingsForLink(link, container, callbacks) {
|
|
1639
|
+
return mountNotificationSettingsForLink(link, container, callbacks);
|
|
1640
|
+
},
|
|
1501
1641
|
};
|
|
1502
1642
|
})();
|
|
@@ -220,6 +220,113 @@ body.dark .tab-link-popover .tab-link-popover-item:hover,
|
|
|
220
220
|
body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
|
|
221
221
|
background: rgba(148, 163, 184, 0.12);
|
|
222
222
|
}
|
|
223
|
+
.tab-link-popover .tab-link-popover-item[aria-expanded="true"] {
|
|
224
|
+
background: rgba(15, 23, 42, 0.04);
|
|
225
|
+
}
|
|
226
|
+
body.dark .tab-link-popover .tab-link-popover-item[aria-expanded="true"] {
|
|
227
|
+
background: rgba(148, 163, 184, 0.08);
|
|
228
|
+
}
|
|
229
|
+
.tab-link-notification-settings {
|
|
230
|
+
margin: 0 14px 10px 40px;
|
|
231
|
+
border: 1px solid rgba(15, 23, 42, 0.1);
|
|
232
|
+
border-radius: 7px;
|
|
233
|
+
background: rgba(15, 23, 42, 0.025);
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
}
|
|
236
|
+
body.dark .tab-link-notification-settings {
|
|
237
|
+
border-color: rgba(148, 163, 184, 0.18);
|
|
238
|
+
background: rgba(148, 163, 184, 0.045);
|
|
239
|
+
}
|
|
240
|
+
.tab-link-notification-setting-row {
|
|
241
|
+
min-height: 44px;
|
|
242
|
+
padding: 7px 10px;
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: 12px;
|
|
247
|
+
box-sizing: border-box;
|
|
248
|
+
color: inherit;
|
|
249
|
+
}
|
|
250
|
+
.tab-link-notification-setting-row + .tab-link-notification-setting-row {
|
|
251
|
+
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
|
252
|
+
}
|
|
253
|
+
body.dark .tab-link-notification-setting-row + .tab-link-notification-setting-row {
|
|
254
|
+
border-top-color: rgba(148, 163, 184, 0.14);
|
|
255
|
+
}
|
|
256
|
+
.tab-link-notification-setting-label {
|
|
257
|
+
min-width: 0;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
font-weight: 500;
|
|
260
|
+
color: rgba(15, 23, 42, 0.76);
|
|
261
|
+
}
|
|
262
|
+
body.dark .tab-link-notification-setting-label {
|
|
263
|
+
color: rgba(226, 232, 240, 0.86);
|
|
264
|
+
}
|
|
265
|
+
.tab-link-notification-toggle-control {
|
|
266
|
+
display: inline-flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 7px;
|
|
269
|
+
}
|
|
270
|
+
.tab-link-notification-toggle-status {
|
|
271
|
+
min-width: 18px;
|
|
272
|
+
font-size: 11px;
|
|
273
|
+
text-align: right;
|
|
274
|
+
color: rgba(15, 23, 42, 0.56);
|
|
275
|
+
}
|
|
276
|
+
body.dark .tab-link-notification-toggle-status {
|
|
277
|
+
color: rgba(226, 232, 240, 0.62);
|
|
278
|
+
}
|
|
279
|
+
.tab-link-notification-switch {
|
|
280
|
+
position: relative;
|
|
281
|
+
width: 36px;
|
|
282
|
+
height: 26px;
|
|
283
|
+
padding: 4px;
|
|
284
|
+
appearance: none;
|
|
285
|
+
border: 0;
|
|
286
|
+
border-radius: 999px;
|
|
287
|
+
background: rgba(100, 116, 139, 0.42);
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
transition: background-color 140ms ease;
|
|
290
|
+
}
|
|
291
|
+
.tab-link-notification-switch[data-enabled="true"] {
|
|
292
|
+
background: var(--pinokio-focus-color, #2563eb);
|
|
293
|
+
}
|
|
294
|
+
.tab-link-notification-switch-thumb {
|
|
295
|
+
display: block;
|
|
296
|
+
width: 18px;
|
|
297
|
+
height: 18px;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
background: #f8fafc;
|
|
300
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.24);
|
|
301
|
+
transform: translateX(0);
|
|
302
|
+
transition: transform 140ms ease;
|
|
303
|
+
}
|
|
304
|
+
.tab-link-notification-switch[data-enabled="true"] .tab-link-notification-switch-thumb {
|
|
305
|
+
transform: translateX(10px);
|
|
306
|
+
}
|
|
307
|
+
.tab-link-notification-switch:focus-visible,
|
|
308
|
+
.tab-link-notification-sound-select:focus-visible {
|
|
309
|
+
outline: 2px solid var(--pinokio-focus-color, #2563eb);
|
|
310
|
+
outline-offset: 2px;
|
|
311
|
+
}
|
|
312
|
+
.tab-link-notification-sound-select {
|
|
313
|
+
width: min(160px, 44vw);
|
|
314
|
+
min-height: 32px;
|
|
315
|
+
padding: 5px 28px 5px 9px;
|
|
316
|
+
border: 1px solid rgba(15, 23, 42, 0.14);
|
|
317
|
+
border-radius: 6px;
|
|
318
|
+
background: var(--pinokio-sidebar-tabbar-bg, #ffffff);
|
|
319
|
+
color: rgba(15, 23, 42, 0.82);
|
|
320
|
+
font: inherit;
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
}
|
|
324
|
+
body.dark .tab-link-notification-sound-select {
|
|
325
|
+
border-color: rgba(148, 163, 184, 0.22);
|
|
326
|
+
background: rgba(15, 23, 42, 0.42);
|
|
327
|
+
color: rgba(241, 245, 249, 0.9);
|
|
328
|
+
color-scheme: dark;
|
|
329
|
+
}
|
|
223
330
|
.tab-link-popover .tab-link-popover-item .label {
|
|
224
331
|
font-size: 11px;
|
|
225
332
|
font-weight: 600;
|
|
@@ -344,6 +451,27 @@ body.dark .tab-link-popover .tab-link-popover-footer:focus-visible {
|
|
|
344
451
|
padding-right: 16px;
|
|
345
452
|
padding-left: 16px;
|
|
346
453
|
}
|
|
454
|
+
.tab-link-notification-settings {
|
|
455
|
+
margin-right: 16px;
|
|
456
|
+
margin-left: 42px;
|
|
457
|
+
}
|
|
458
|
+
.tab-link-notification-setting-row {
|
|
459
|
+
padding-right: 8px;
|
|
460
|
+
padding-left: 8px;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
@media (pointer: coarse) {
|
|
464
|
+
.tab-link-notification-switch {
|
|
465
|
+
width: 44px;
|
|
466
|
+
height: 30px;
|
|
467
|
+
padding: 4px;
|
|
468
|
+
}
|
|
469
|
+
.tab-link-notification-switch[data-enabled="true"] .tab-link-notification-switch-thumb {
|
|
470
|
+
transform: translateX(14px);
|
|
471
|
+
}
|
|
472
|
+
.tab-link-notification-sound-select {
|
|
473
|
+
min-height: 44px;
|
|
474
|
+
}
|
|
347
475
|
}
|
|
348
476
|
.appcanvas > aside .menu-container .tab-link-popover-host {
|
|
349
477
|
display: flex;
|
|
@@ -266,10 +266,39 @@
|
|
|
266
266
|
if (action === "notifications") {
|
|
267
267
|
const activeLink = tabLinkActiveLink
|
|
268
268
|
const notifier = typeof window !== "undefined" ? window.PinokioIdleNotifier : null
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
const existingPanel = item.nextElementSibling
|
|
270
|
+
if (existingPanel && existingPanel.classList.contains("tab-link-notification-settings")) {
|
|
271
|
+
existingPanel.remove()
|
|
272
|
+
item.setAttribute("aria-expanded", "false")
|
|
273
|
+
item.removeAttribute("aria-controls")
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (notifier && typeof notifier.mountSettingsForLink === "function") {
|
|
277
|
+
const panel = document.createElement("div")
|
|
278
|
+
panel.className = "tab-link-notification-settings"
|
|
279
|
+
panel.id = `${TAB_LINK_POPOVER_ID}-notification-settings`
|
|
280
|
+
panel.setAttribute("role", "group")
|
|
281
|
+
panel.setAttribute("aria-label", "Desktop notification settings")
|
|
282
|
+
item.insertAdjacentElement("afterend", panel)
|
|
283
|
+
const value = item.querySelector(".value")
|
|
284
|
+
const icon = item.querySelector(".tab-link-popover-action-icon i")
|
|
285
|
+
const mounted = notifier.mountSettingsForLink(activeLink, panel, {
|
|
286
|
+
onEnabledChange(enabled) {
|
|
287
|
+
if (value) {
|
|
288
|
+
value.textContent = enabled ? "Enabled for this tab" : "Disabled for this tab"
|
|
289
|
+
}
|
|
290
|
+
if (icon) {
|
|
291
|
+
icon.classList.toggle("fa-bell", enabled)
|
|
292
|
+
icon.classList.toggle("fa-bell-slash", !enabled)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
if (mounted) {
|
|
297
|
+
item.setAttribute("aria-controls", panel.id)
|
|
298
|
+
item.setAttribute("aria-expanded", "true")
|
|
299
|
+
} else {
|
|
300
|
+
panel.remove()
|
|
301
|
+
}
|
|
273
302
|
}
|
|
274
303
|
return
|
|
275
304
|
}
|
|
@@ -1898,6 +1927,9 @@
|
|
|
1898
1927
|
if (actionItem.action) {
|
|
1899
1928
|
item.setAttribute("data-action", actionItem.action)
|
|
1900
1929
|
}
|
|
1930
|
+
if (actionItem.action === "notifications") {
|
|
1931
|
+
item.setAttribute("aria-expanded", "false")
|
|
1932
|
+
}
|
|
1901
1933
|
if (actionItem.url) {
|
|
1902
1934
|
item.setAttribute("data-url", actionItem.url)
|
|
1903
1935
|
}
|