pinokiod 8.0.39 → 8.0.41
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/hf/index.js +2 -2
- package/kernel/api/index.js +12 -3
- package/kernel/connect/index.js +2 -2
- package/kernel/connect/providers/huggingface/index.js +14 -5
- package/kernel/index.js +14 -2
- package/kernel/shell.js +3 -2
- package/kernel/shells.js +6 -0
- package/kernel/vault/constants.js +13 -0
- package/kernel/vault/hash_worker.js +9 -2
- package/kernel/vault/index.js +1856 -316
- package/kernel/vault/registry.js +526 -52
- package/kernel/vault/snapshot.js +29 -0
- package/kernel/vault/sweeper.js +480 -210
- package/kernel/vault/walker.js +142 -0
- package/package.json +1 -1
- package/server/index.js +79 -90
- package/server/lib/privacy_filter_cache.js +4 -1
- package/server/public/storage-size.js +12 -0
- package/server/public/style.css +13 -0
- package/server/public/tab-link-popover.js +3 -0
- package/server/public/vault-nav.js +96 -0
- package/server/public/vault.css +1079 -0
- package/server/public/vault.js +1835 -0
- package/server/views/app.ejs +93 -16
- package/server/views/connect/huggingface.ejs +7 -9
- package/server/views/partials/main_sidebar.ejs +6 -1
- package/server/views/partials/vault_workspace.ejs +55 -0
- package/server/views/vault.ejs +5 -1532
- package/server/views/vault_app.ejs +22 -0
- package/test/hf-api.test.js +4 -2
- package/test/huggingface-connect.test.js +7 -11
- package/test/huggingface-token-validity.test.js +135 -0
- package/test/plugin-action-functions.test.js +19 -0
- package/test/privacy-filter-cache.test.js +1 -0
- package/test/vault-engine.test.js +1276 -26
- package/test/vault-sweep.test.js +1043 -35
- package/test/vault-ui.test.js +2184 -65
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { ENTRY_BATCH_SIZE } = require('./constants')
|
|
4
|
+
|
|
5
|
+
const isMissingError = (error) => !!(error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
6
|
+
const isHandledError = (options, error, target) => !!(
|
|
7
|
+
!isMissingError(error) && options.onError && options.onError(error, target)
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// One traversal primitive for Scan and Repair. It owns only directory
|
|
11
|
+
// discovery and symlink policy; callers decide how files are classified.
|
|
12
|
+
async function * walkBatches(root, options = {}) {
|
|
13
|
+
const resolvedRoot = path.resolve(root)
|
|
14
|
+
const concurrency = Math.max(1, Number(options.concurrency) || 1)
|
|
15
|
+
const entryBatchSize = Math.max(1, Number(options.entryBatchSize) || ENTRY_BATCH_SIZE)
|
|
16
|
+
const statConcurrency = Math.max(1, Number(options.statConcurrency) || 32)
|
|
17
|
+
const skipDirectory = options.skipDirectory || (() => false)
|
|
18
|
+
const strictErrors = !!options.strictErrors
|
|
19
|
+
const pending = [{ dir: resolvedRoot, followRootSymlink: true }]
|
|
20
|
+
const active = []
|
|
21
|
+
try {
|
|
22
|
+
while (pending.length || active.length) {
|
|
23
|
+
while (active.length < concurrency && pending.length) {
|
|
24
|
+
const next = pending.pop()
|
|
25
|
+
const dir = next.dir
|
|
26
|
+
try {
|
|
27
|
+
let st = await fs.promises.lstat(dir)
|
|
28
|
+
if (next.followRootSymlink && st.isSymbolicLink()) {
|
|
29
|
+
st = await fs.promises.stat(dir)
|
|
30
|
+
}
|
|
31
|
+
if (!st.isDirectory()) {
|
|
32
|
+
yield [{ dir, entries: null, files: [], discoveredDirs: 0, discoveredFiles: 0, firstChunk: true }]
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
active.push({ dir, handle: await fs.promises.opendir(dir), firstChunk: true })
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (isMissingError(error) && dir === resolvedRoot && options.onRootMissing) {
|
|
38
|
+
options.onRootMissing(error, dir)
|
|
39
|
+
}
|
|
40
|
+
if (strictErrors && !isMissingError(error) && !isHandledError(options, error, dir)) throw error
|
|
41
|
+
yield [{ dir, entries: null, files: [], discoveredDirs: 0, discoveredFiles: 0, firstChunk: true }]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!active.length) continue
|
|
45
|
+
const batch = await Promise.all(active.map(async (task) => {
|
|
46
|
+
const entries = []
|
|
47
|
+
let done = false
|
|
48
|
+
try {
|
|
49
|
+
while (entries.length < entryBatchSize) {
|
|
50
|
+
const entry = await task.handle.read()
|
|
51
|
+
if (!entry) {
|
|
52
|
+
done = true
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
entries.push(entry)
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (isMissingError(error) && task.dir === resolvedRoot && options.onRootMissing) {
|
|
59
|
+
options.onRootMissing(error, task.dir)
|
|
60
|
+
}
|
|
61
|
+
if (strictErrors && !isMissingError(error) && !isHandledError(options, error, task.dir)) throw error
|
|
62
|
+
done = true
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
task, dir: task.dir, entries, done, firstChunk: task.firstChunk,
|
|
66
|
+
files: [], discoveredDirs: 0, discoveredFiles: 0
|
|
67
|
+
}
|
|
68
|
+
}))
|
|
69
|
+
const unknown = []
|
|
70
|
+
for (const group of batch) {
|
|
71
|
+
group.task.firstChunk = false
|
|
72
|
+
for (const entry of group.entries) {
|
|
73
|
+
if (entry.isSymbolicLink()) continue
|
|
74
|
+
const full = path.resolve(group.dir, entry.name)
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
if (skipDirectory(full)) continue
|
|
77
|
+
pending.push({ dir: full, followRootSymlink: false })
|
|
78
|
+
group.discoveredDirs += 1
|
|
79
|
+
} else if (entry.isFile()) {
|
|
80
|
+
group.files.push({ path: full, entry })
|
|
81
|
+
group.discoveredFiles += 1
|
|
82
|
+
} else {
|
|
83
|
+
unknown.push({ group, entry, full })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (unknown.length) {
|
|
88
|
+
const stats = await statMany(
|
|
89
|
+
unknown.map((item) => item.full),
|
|
90
|
+
statConcurrency,
|
|
91
|
+
null,
|
|
92
|
+
{ followSymlinks: false, strictErrors, onError: options.onError }
|
|
93
|
+
)
|
|
94
|
+
for (let index = 0; index < unknown.length; index++) {
|
|
95
|
+
const item = unknown[index]
|
|
96
|
+
const st = stats[index]
|
|
97
|
+
if (!st || st.isSymbolicLink()) continue
|
|
98
|
+
if (st.isDirectory()) {
|
|
99
|
+
if (skipDirectory(item.full)) continue
|
|
100
|
+
pending.push({ dir: item.full, followRootSymlink: false })
|
|
101
|
+
item.group.discoveredDirs += 1
|
|
102
|
+
} else if (st.isFile()) {
|
|
103
|
+
item.group.files.push({ path: item.full, entry: item.entry })
|
|
104
|
+
item.group.discoveredFiles += 1
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
yield batch.map(({ task, done, ...group }) => group)
|
|
109
|
+
for (let index = active.length - 1; index >= 0; index--) {
|
|
110
|
+
if (!batch[index].done) continue
|
|
111
|
+
await active[index].handle.close().catch(() => {})
|
|
112
|
+
active.splice(index, 1)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
await Promise.all(active.map((task) => task.handle.close().catch(() => {})))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const statMany = async (paths, concurrency = 32, onSettled = null, options = {}) => {
|
|
121
|
+
const results = new Array(paths.length)
|
|
122
|
+
const readMetadata = options.followSymlinks === false ? fs.promises.lstat : fs.promises.stat
|
|
123
|
+
let cursor = 0
|
|
124
|
+
const workers = Array.from({ length: Math.min(concurrency, paths.length) }, async () => {
|
|
125
|
+
while (cursor < paths.length) {
|
|
126
|
+
const index = cursor++
|
|
127
|
+
try {
|
|
128
|
+
results[index] = await readMetadata(paths[index])
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (options.strictErrors && !isMissingError(error) &&
|
|
131
|
+
!isHandledError(options, error, paths[index])) throw error
|
|
132
|
+
results[index] = null
|
|
133
|
+
} finally {
|
|
134
|
+
if (onSettled) onSettled(index, results[index])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
await Promise.all(workers)
|
|
139
|
+
return results
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { walkBatches, statMany }
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1990,6 +1990,9 @@ class Server {
|
|
|
1990
1990
|
result.hasSnapshots = !!(options && options.hasSnapshots)
|
|
1991
1991
|
result.pendingSnapshotId = options && options.pendingSnapshotId ? String(options.pendingSnapshotId) : null
|
|
1992
1992
|
result.registryEnabled = registryEnabled
|
|
1993
|
+
const vault = this.kernel.vault
|
|
1994
|
+
if (vault && vault.ready) await vault.ready
|
|
1995
|
+
result.vault_enabled = !!(vault && vault.enabled)
|
|
1993
1996
|
if (!registryEnabled) {
|
|
1994
1997
|
result.pendingSnapshotId = null
|
|
1995
1998
|
}
|
|
@@ -6664,6 +6667,10 @@ class Server {
|
|
|
6664
6667
|
|
|
6665
6668
|
this.started = false
|
|
6666
6669
|
this.app = express();
|
|
6670
|
+
this.app.use((req, res, next) => {
|
|
6671
|
+
res.locals.vaultEnabled = !!(this.kernel.vault && this.kernel.vault.enabled)
|
|
6672
|
+
next()
|
|
6673
|
+
})
|
|
6667
6674
|
this.app.use(cors({
|
|
6668
6675
|
origin: '*'
|
|
6669
6676
|
}));
|
|
@@ -15732,21 +15739,55 @@ class Server {
|
|
|
15732
15739
|
// }))
|
|
15733
15740
|
|
|
15734
15741
|
|
|
15735
|
-
// Vault dashboard data:
|
|
15736
|
-
//
|
|
15742
|
+
// Vault dashboard data: registry-backed with bounded per-blob stats,
|
|
15743
|
+
// never a discovery walk. Scans run ONLY via the explicit action below.
|
|
15744
|
+
this.app.get("/info/dedup/nav", ex(async (req, res) => {
|
|
15745
|
+
if (!privacyFilterCache.isSameOriginRequest(req)) {
|
|
15746
|
+
res.sendStatus(403)
|
|
15747
|
+
return
|
|
15748
|
+
}
|
|
15749
|
+
const vault = this.kernel.vault
|
|
15750
|
+
if (vault && vault.ready) await vault.ready
|
|
15751
|
+
if (!vault || !vault.enabled) {
|
|
15752
|
+
res.sendStatus(404)
|
|
15753
|
+
return
|
|
15754
|
+
}
|
|
15755
|
+
res.set("Cache-Control", "no-store")
|
|
15756
|
+
const appName = req.query && typeof req.query.app === "string"
|
|
15757
|
+
? req.query.app
|
|
15758
|
+
: null
|
|
15759
|
+
res.json(appName
|
|
15760
|
+
? vault.navigationStatusForApp(appName)
|
|
15761
|
+
: vault.navigationStatus())
|
|
15762
|
+
}))
|
|
15737
15763
|
this.app.get("/info/dedup", ex(async (req, res) => {
|
|
15764
|
+
if (!privacyFilterCache.isSameOriginRequest(req)) {
|
|
15765
|
+
res.sendStatus(403)
|
|
15766
|
+
return
|
|
15767
|
+
}
|
|
15738
15768
|
const vault = this.kernel.vault
|
|
15769
|
+
if (vault && vault.ready) await vault.ready
|
|
15739
15770
|
if (!vault || !vault.enabled) {
|
|
15740
|
-
res.
|
|
15771
|
+
res.sendStatus(404)
|
|
15741
15772
|
return
|
|
15742
15773
|
}
|
|
15774
|
+
res.set("Cache-Control", "no-store")
|
|
15775
|
+
const scopeId = req.query && typeof req.query.scope_id === "string"
|
|
15776
|
+
? req.query.scope_id
|
|
15777
|
+
: null
|
|
15743
15778
|
if (req.query && req.query.progress === "1") {
|
|
15744
|
-
res.json(vault.progressStatus())
|
|
15779
|
+
res.json(vault.progressStatus(scopeId))
|
|
15745
15780
|
return
|
|
15746
15781
|
}
|
|
15747
|
-
res.json(await vault.status())
|
|
15782
|
+
res.json(await vault.status(scopeId))
|
|
15748
15783
|
}))
|
|
15749
15784
|
this.app.get("/vault", ex(async (req, res) => {
|
|
15785
|
+
const vault = this.kernel.vault
|
|
15786
|
+
if (vault && vault.ready) await vault.ready
|
|
15787
|
+
if (!vault || !vault.enabled) {
|
|
15788
|
+
res.sendStatus(404)
|
|
15789
|
+
return
|
|
15790
|
+
}
|
|
15750
15791
|
const { install_required, requirements_pending } = await this.kernel.bin.check({
|
|
15751
15792
|
bin: this.kernel.bin.preset("dev"),
|
|
15752
15793
|
})
|
|
@@ -15754,100 +15795,48 @@ class Server {
|
|
|
15754
15795
|
res.redirect(`/setup/dev?callback=${encodeURIComponent(req.originalUrl)}`)
|
|
15755
15796
|
return
|
|
15756
15797
|
}
|
|
15798
|
+
await vault.ensureInitialized()
|
|
15757
15799
|
res.render("vault", { theme: this.theme, agent: req.agent })
|
|
15758
15800
|
}))
|
|
15801
|
+
this.app.get("/vault/app/:name", ex(async (req, res) => {
|
|
15802
|
+
if (!privacyFilterCache.isSameOriginRequest(req)) {
|
|
15803
|
+
res.sendStatus(403)
|
|
15804
|
+
return
|
|
15805
|
+
}
|
|
15806
|
+
const vault = this.kernel.vault
|
|
15807
|
+
if (vault && vault.ready) await vault.ready
|
|
15808
|
+
if (!vault || !vault.enabled) {
|
|
15809
|
+
res.sendStatus(404)
|
|
15810
|
+
return
|
|
15811
|
+
}
|
|
15812
|
+
await vault.ensureInitialized()
|
|
15813
|
+
await vault.refreshSources()
|
|
15814
|
+
const source = vault.sources().find((item) =>
|
|
15815
|
+
item.kind === "app" && item.app === req.params.name && item.available)
|
|
15816
|
+
if (!source) {
|
|
15817
|
+
res.sendStatus(404)
|
|
15818
|
+
return
|
|
15819
|
+
}
|
|
15820
|
+
res.render("vault_app", {
|
|
15821
|
+
theme: this.theme,
|
|
15822
|
+
agent: req.agent,
|
|
15823
|
+
scope_id: source.id
|
|
15824
|
+
})
|
|
15825
|
+
}))
|
|
15759
15826
|
this.app.post("/vault/action", ex(async (req, res) => {
|
|
15827
|
+
if (!privacyFilterCache.isSameOriginRequest(req)) {
|
|
15828
|
+
res.sendStatus(403)
|
|
15829
|
+
return
|
|
15830
|
+
}
|
|
15760
15831
|
const vault = this.kernel.vault
|
|
15832
|
+
if (vault && vault.ready) await vault.ready
|
|
15761
15833
|
if (!vault || !vault.enabled) {
|
|
15762
|
-
res.
|
|
15834
|
+
res.sendStatus(404)
|
|
15763
15835
|
return
|
|
15764
15836
|
}
|
|
15765
15837
|
const body = req.body || {}
|
|
15766
15838
|
try {
|
|
15767
|
-
|
|
15768
|
-
case "add_source": {
|
|
15769
|
-
const result = await vault.addExternalSource(body.path)
|
|
15770
|
-
res.json({
|
|
15771
|
-
created: result.created,
|
|
15772
|
-
source: {
|
|
15773
|
-
id: result.source.id,
|
|
15774
|
-
label: result.source.label,
|
|
15775
|
-
target_path: result.source.root,
|
|
15776
|
-
shareable: !!result.source.shareable
|
|
15777
|
-
}
|
|
15778
|
-
})
|
|
15779
|
-
return
|
|
15780
|
-
}
|
|
15781
|
-
case "deduplicate": {
|
|
15782
|
-
const scopeId = typeof body.scope_id === "string" ? body.scope_id : null
|
|
15783
|
-
if (scopeId) {
|
|
15784
|
-
const source = vault.sources().find((item) => item.id === scopeId && item.kind !== "virtual")
|
|
15785
|
-
if (!source) {
|
|
15786
|
-
res.json({ error: "That location is no longer available. Scan again to refresh it." })
|
|
15787
|
-
return
|
|
15788
|
-
}
|
|
15789
|
-
if (!source.shareable) {
|
|
15790
|
-
res.json({ error: "This location is on a disk that cannot share space with this vault." })
|
|
15791
|
-
return
|
|
15792
|
-
}
|
|
15793
|
-
if (source.kind === "app") {
|
|
15794
|
-
const appRoot = this.kernel.path("api", source.app) + path.sep
|
|
15795
|
-
const running = this.kernel.api && this.kernel.api.running ? this.kernel.api.running : {}
|
|
15796
|
-
const busy = Object.keys(running).some((k) => running[k] && k.startsWith(appRoot))
|
|
15797
|
-
if (busy) {
|
|
15798
|
-
res.json({ error: "This app has a running script. Stop it first, then deduplicate." })
|
|
15799
|
-
return
|
|
15800
|
-
}
|
|
15801
|
-
}
|
|
15802
|
-
res.json(await vault.sweeper.convertPending(undefined, {
|
|
15803
|
-
scope_id: scopeId, batch_id: `batch-${Date.now()}`
|
|
15804
|
-
}))
|
|
15805
|
-
return
|
|
15806
|
-
}
|
|
15807
|
-
const app = Object.prototype.hasOwnProperty.call(body, "app") ? (body.app || null) : undefined
|
|
15808
|
-
if (app) {
|
|
15809
|
-
const appRoot = this.kernel.path("api", app) + path.sep
|
|
15810
|
-
const running = this.kernel.api && this.kernel.api.running ? this.kernel.api.running : {}
|
|
15811
|
-
const busy = Object.keys(running).some((k) => running[k] && k.startsWith(appRoot))
|
|
15812
|
-
if (busy) {
|
|
15813
|
-
res.json({ error: "This app has a running script. Stop it first, then deduplicate." })
|
|
15814
|
-
return
|
|
15815
|
-
}
|
|
15816
|
-
}
|
|
15817
|
-
res.json(await vault.sweeper.convertPending(app, { batch_id: `batch-${Date.now()}` }))
|
|
15818
|
-
return
|
|
15819
|
-
}
|
|
15820
|
-
case "reclaim":
|
|
15821
|
-
res.json(await vault.reclaim(body.hash))
|
|
15822
|
-
return
|
|
15823
|
-
case "reclaim_all":
|
|
15824
|
-
res.json(await vault.reclaimAll())
|
|
15825
|
-
return
|
|
15826
|
-
case "scan":
|
|
15827
|
-
vault.sweeper.scan().catch(() => {})
|
|
15828
|
-
res.json({ started: true })
|
|
15829
|
-
return
|
|
15830
|
-
case "repair":
|
|
15831
|
-
case "rebuild":
|
|
15832
|
-
if (vault.sweeper && vault.sweeper.state.active) {
|
|
15833
|
-
res.json({ error: "Wait for the current scan to finish before repairing the index." })
|
|
15834
|
-
return
|
|
15835
|
-
}
|
|
15836
|
-
await vault.rebuild()
|
|
15837
|
-
res.json({ done: true })
|
|
15838
|
-
return
|
|
15839
|
-
case "undo":
|
|
15840
|
-
res.json(await vault.undoBatch(body.batch_id))
|
|
15841
|
-
return
|
|
15842
|
-
case "detach":
|
|
15843
|
-
res.json(await vault.detach(body.path))
|
|
15844
|
-
return
|
|
15845
|
-
case "reshare":
|
|
15846
|
-
res.json(await vault.reshare(body.path))
|
|
15847
|
-
return
|
|
15848
|
-
default:
|
|
15849
|
-
res.json({ error: "unknown action" })
|
|
15850
|
-
}
|
|
15839
|
+
res.json(await vault.perform(body.action, body))
|
|
15851
15840
|
} catch (e) {
|
|
15852
15841
|
res.json({ error: e && e.message ? e.message : String(e) })
|
|
15853
15842
|
}
|
|
@@ -66,7 +66,7 @@ function assertKnownAsset(relativePath) {
|
|
|
66
66
|
return normalized
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function
|
|
69
|
+
function isSameOriginRequest(req) {
|
|
70
70
|
const fetchSite = String(req && typeof req.get === 'function' ? req.get('Sec-Fetch-Site') || '' : '').trim().toLowerCase()
|
|
71
71
|
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
|
|
72
72
|
return false
|
|
@@ -90,6 +90,8 @@ function isInstallRequestAllowed(req) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const isInstallRequestAllowed = isSameOriginRequest
|
|
94
|
+
|
|
93
95
|
function assetPath(kernel, relativePath) {
|
|
94
96
|
const normalized = assertKnownAsset(relativePath)
|
|
95
97
|
return path.resolve(modelRoot(kernel), ...normalized.split('/'))
|
|
@@ -280,6 +282,7 @@ module.exports = {
|
|
|
280
282
|
normalizeDtype,
|
|
281
283
|
cacheRoot,
|
|
282
284
|
modelRoot,
|
|
285
|
+
isSameOriginRequest,
|
|
283
286
|
isInstallRequestAllowed,
|
|
284
287
|
status,
|
|
285
288
|
ensure
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
(function (root) {
|
|
2
|
+
const base = 1000
|
|
3
|
+
const units = ["B", "KB", "MB", "GB", "TB"]
|
|
4
|
+
|
|
5
|
+
root.PinokioFormatStorageSize = (value) => {
|
|
6
|
+
const bytes = Number(value)
|
|
7
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
|
|
8
|
+
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(base)))
|
|
9
|
+
const scaled = bytes / Math.pow(base, index)
|
|
10
|
+
return `${Number(scaled.toFixed(index ? 2 : 0))} ${units[index]}`
|
|
11
|
+
}
|
|
12
|
+
})(window)
|
package/server/public/style.css
CHANGED
|
@@ -1567,6 +1567,14 @@ body.main-sidebar-page aside.main-sidebar .tab.selected .main-sidebar-action-ico
|
|
|
1567
1567
|
background: rgba(45, 180, 92, 0.14);
|
|
1568
1568
|
color: #247a43;
|
|
1569
1569
|
}
|
|
1570
|
+
.main-sidebar [data-main-sidebar-vault-tab][data-vault-state="new"]:not(.selected) {
|
|
1571
|
+
background: var(--pinokio-sidebar-tab-hover);
|
|
1572
|
+
}
|
|
1573
|
+
.main-sidebar .main-sidebar-status-badge[data-state="new"] {
|
|
1574
|
+
border: 1px solid rgba(71, 85, 105, 0.22);
|
|
1575
|
+
background: transparent;
|
|
1576
|
+
color: rgba(31, 41, 55, 0.72);
|
|
1577
|
+
}
|
|
1570
1578
|
body.dark .main-sidebar .main-sidebar-status-badge {
|
|
1571
1579
|
background: rgba(255, 255, 255, 0.08);
|
|
1572
1580
|
color: rgba(229, 231, 235, 0.68);
|
|
@@ -1575,6 +1583,11 @@ body.dark .main-sidebar .main-sidebar-status-badge[data-state="on"] {
|
|
|
1575
1583
|
background: rgba(74, 222, 128, 0.14);
|
|
1576
1584
|
color: #86efac;
|
|
1577
1585
|
}
|
|
1586
|
+
body.dark .main-sidebar .main-sidebar-status-badge[data-state="new"] {
|
|
1587
|
+
border-color: rgba(226, 232, 240, 0.22);
|
|
1588
|
+
background: transparent;
|
|
1589
|
+
color: rgba(248, 250, 252, 0.76);
|
|
1590
|
+
}
|
|
1578
1591
|
.main-sidebar .tab.submenu {
|
|
1579
1592
|
padding-left: 14px !important;
|
|
1580
1593
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (window.__pinokioVaultNavigationStatusInit) return
|
|
3
|
+
window.__pinokioVaultNavigationStatusInit = true
|
|
4
|
+
|
|
5
|
+
const formatBytes = (value) => {
|
|
6
|
+
const bytes = Number(value)
|
|
7
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return ""
|
|
8
|
+
const units = ["B", "KB", "MB", "GB", "TB"]
|
|
9
|
+
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1000)))
|
|
10
|
+
const scaled = bytes / Math.pow(1000, index)
|
|
11
|
+
const digits = scaled >= 10 || index === 0 ? 0 : 1
|
|
12
|
+
return `${Number(scaled.toFixed(digits))} ${units[index]}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const init = () => {
|
|
16
|
+
const tab = document.querySelector("[data-main-sidebar-vault-tab], [data-app-vault-tab]")
|
|
17
|
+
if (!tab) return
|
|
18
|
+
const appName = tab.dataset.appVaultName || ""
|
|
19
|
+
const appMode = !!appName
|
|
20
|
+
const status = tab.querySelector(appMode
|
|
21
|
+
? "[data-app-vault-status]"
|
|
22
|
+
: "[data-main-sidebar-vault-status]")
|
|
23
|
+
const prompt = tab.querySelector("[data-app-vault-prompt]")
|
|
24
|
+
if (!status) return
|
|
25
|
+
let requestId = 0
|
|
26
|
+
|
|
27
|
+
const render = (data) => {
|
|
28
|
+
let state = ""
|
|
29
|
+
let label = ""
|
|
30
|
+
let title = "Save space"
|
|
31
|
+
if (data && data.scanning) {
|
|
32
|
+
state = "scanning"
|
|
33
|
+
label = "Scanning"
|
|
34
|
+
title = appMode ? "Save space, scanning this app" : "Save space, scanning"
|
|
35
|
+
} else if (!data || !data.any_scan) {
|
|
36
|
+
state = "new"
|
|
37
|
+
label = "NEW"
|
|
38
|
+
title = "Save space — find duplicate files and use less disk"
|
|
39
|
+
} else if (!data.scanned) {
|
|
40
|
+
state = "not-scanned"
|
|
41
|
+
label = "Not scanned"
|
|
42
|
+
title = appMode ? "Save space, this app has not been scanned" : "Save space, not scanned"
|
|
43
|
+
} else if (Number(data.pending_bytes) > 0) {
|
|
44
|
+
state = "pending"
|
|
45
|
+
label = `${data.incomplete ? "≥" : ""}${formatBytes(data.pending_bytes)}`
|
|
46
|
+
title = `Save space, ${data.incomplete ? "at least " : ""}${formatBytes(data.pending_bytes)} can be saved${appMode ? " in this app" : ""}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tab.dataset.vaultState = state || "ready"
|
|
50
|
+
tab.setAttribute("aria-label", title)
|
|
51
|
+
tab.setAttribute("title", title)
|
|
52
|
+
if (prompt) prompt.hidden = state !== "new"
|
|
53
|
+
status.hidden = !label
|
|
54
|
+
status.textContent = label
|
|
55
|
+
if (state) status.dataset.state = state
|
|
56
|
+
else delete status.dataset.state
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const load = () => {
|
|
60
|
+
const currentRequest = ++requestId
|
|
61
|
+
const query = appMode ? `?app=${encodeURIComponent(appName)}` : ""
|
|
62
|
+
fetch(`/info/dedup/nav${query}`, { cache: "no-store" })
|
|
63
|
+
.then((response) => {
|
|
64
|
+
if (!response.ok) throw new Error(`Save space status failed: ${response.status}`)
|
|
65
|
+
return response.json()
|
|
66
|
+
})
|
|
67
|
+
.then((data) => {
|
|
68
|
+
if (currentRequest === requestId) render(data)
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
if (currentRequest !== requestId) return
|
|
72
|
+
status.hidden = true
|
|
73
|
+
if (prompt) prompt.hidden = true
|
|
74
|
+
delete status.dataset.state
|
|
75
|
+
delete tab.dataset.vaultState
|
|
76
|
+
tab.removeAttribute("aria-label")
|
|
77
|
+
tab.removeAttribute("title")
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
document.addEventListener("pinokio:vault-status-changed", load)
|
|
82
|
+
window.addEventListener("message", (event) => {
|
|
83
|
+
if (event.origin === window.location.origin &&
|
|
84
|
+
event.data && event.data.type === "pinokio:vault-status-changed") {
|
|
85
|
+
load()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
load()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (document.readyState === "loading") {
|
|
92
|
+
document.addEventListener("DOMContentLoaded", init, { once: true })
|
|
93
|
+
} else {
|
|
94
|
+
init()
|
|
95
|
+
}
|
|
96
|
+
})()
|