pinokiod 7.3.14 → 7.4.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/kernel/api/index.js +111 -15
- package/kernel/api/script/index.js +10 -0
- package/kernel/autolaunch.js +111 -0
- package/kernel/environment.js +89 -1
- package/kernel/git.js +9 -19
- package/kernel/index.js +142 -43
- package/kernel/launch_requirements.js +1115 -0
- package/kernel/ready.js +231 -0
- package/kernel/script.js +16 -0
- package/kernel/shells.js +9 -1
- package/kernel/workspace_status.js +111 -45
- package/package.json +1 -1
- package/server/autolaunch.js +725 -0
- package/server/index.js +244 -411
- package/server/public/logs.js +15 -1
- package/server/public/style.css +99 -31
- package/server/views/app.ejs +263 -159
- package/server/views/autolaunch.ejs +363 -75
- package/server/views/index.ejs +550 -26
- package/server/views/logs.ejs +14 -12
- package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
- package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
- package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
- package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
- package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
- package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
- package/server/views/partials/home_action_modal.ejs +4 -1
- package/server/views/partials/launch_requirements_status_client.ejs +271 -0
- package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
- package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
- package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
- package/server/views/terminal.ejs +196 -2
- package/test/home-autolaunch-live-ui.test.js +455 -0
- package/test/launch-requirements-browser.test.js +579 -0
- package/test/launch-requirements-contract-coverage.test.js +627 -0
- package/test/launch-requirements-real-browser.js +625 -0
- package/test/launch-requirements-status-client.test.js +132 -0
- package/test/launch-requirements.test.js +1806 -0
- package/test/launch-settings-ui.test.js +370 -0
- package/test/ready-state.test.js +49 -0
- package/test/server-autolaunch.test.js +1052 -0
- package/test/startup-git-index-benchmark.js +409 -0
- package/test/startup-git-index-browser.js +320 -0
- package/test/startup-git-index-performance.test.js +380 -0
- package/test/startup-git-index-refactor.test.js +450 -0
- package/test/startup-git-index-route.test.js +588 -0
- package/test/universal-launcher.smoke.spec.js +10 -9
- package/test/workspace-gitignore-benchmark.js +815 -0
- package/test/workspace-gitignore-path-scoped.test.js +256 -0
- package/spec/INSTRUCTION_SYNC.md +0 -432
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const dependencyOptions = () => {
|
|
2
|
+
return candidateState && Array.isArray(candidateState.dependency_apps)
|
|
3
|
+
? candidateState.dependency_apps
|
|
4
|
+
: []
|
|
5
|
+
}
|
|
6
|
+
const dependencyById = (dependencyId) => {
|
|
7
|
+
return dependencyOptions().find((candidate) => candidate && candidate.id === dependencyId) || null
|
|
8
|
+
}
|
|
9
|
+
const dependencyFolderPath = (app) => {
|
|
10
|
+
const raw = String((app && (app.workspace_path || app.launcher_path || app.id)) || "").replace(/\\/g, "/")
|
|
11
|
+
if (!raw) return ""
|
|
12
|
+
const pinokioApiMatch = raw.match(/\/pinokio\/api\/(.+)$/i)
|
|
13
|
+
if (pinokioApiMatch && pinokioApiMatch[1]) {
|
|
14
|
+
return `~/pinokio/api/${pinokioApiMatch[1]}`
|
|
15
|
+
}
|
|
16
|
+
return raw.replace(/^\/Users\/[^/]+/, "~")
|
|
17
|
+
}
|
|
18
|
+
const dependencyFullPath = (app) => String((app && (app.workspace_path || app.launcher_path || app.id)) || "")
|
|
19
|
+
const renderDependencyOption = (candidate) => {
|
|
20
|
+
const title = candidate.title || candidate.name || candidate.id
|
|
21
|
+
const folderPath = dependencyFolderPath(candidate)
|
|
22
|
+
const fullFolderPath = dependencyFullPath(candidate)
|
|
23
|
+
const searchText = `${title} ${candidate.id || ""} ${folderPath} ${fullFolderPath}`.toLowerCase()
|
|
24
|
+
return `
|
|
25
|
+
<button type="button" class="app-autolaunch-dependency-option" data-app-autolaunch-add-dependency="${escapeHtml(candidate.id)}" data-search-text="${escapeHtml(searchText)}" title="${escapeHtml(fullFolderPath)}">
|
|
26
|
+
<img src="${escapeHtml(candidate.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
27
|
+
<span class="app-autolaunch-dependency-option-meta">
|
|
28
|
+
<span class="app-autolaunch-dependency-option-title">${escapeHtml(title)}</span>
|
|
29
|
+
<span class="app-autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
|
|
30
|
+
</span>
|
|
31
|
+
</button>
|
|
32
|
+
`
|
|
33
|
+
}
|
|
34
|
+
const renderSelectedDependency = (dependencyId) => {
|
|
35
|
+
const candidate = dependencyById(dependencyId)
|
|
36
|
+
const title = candidate ? (candidate.title || candidate.name || candidate.id) : dependencyId
|
|
37
|
+
const folderPath = candidate ? dependencyFolderPath(candidate) : dependencyId
|
|
38
|
+
const fullFolderPath = candidate ? dependencyFullPath(candidate) : dependencyId
|
|
39
|
+
const script = candidate && candidate.autolaunch ? candidate.autolaunch : ""
|
|
40
|
+
const scriptLine = script
|
|
41
|
+
? `<span class="app-autolaunch-dependency-option-path">${escapeHtml(script)}</span>`
|
|
42
|
+
: `<button type="button" class="app-autolaunch-dependency-configure" data-app-autolaunch-configure-dependency="${escapeHtml(dependencyId)}">
|
|
43
|
+
<i class="fa-solid fa-gear" aria-hidden="true"></i>
|
|
44
|
+
Missing launch script
|
|
45
|
+
</button>`
|
|
46
|
+
return `
|
|
47
|
+
<div class="app-autolaunch-dependency-row" title="${escapeHtml(fullFolderPath)}">
|
|
48
|
+
<img src="${escapeHtml((candidate && candidate.icon) || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
49
|
+
<span class="app-autolaunch-dependency-option-meta">
|
|
50
|
+
<span class="app-autolaunch-dependency-option-title">${escapeHtml(title)}</span>
|
|
51
|
+
<span class="app-autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
|
|
52
|
+
${scriptLine}
|
|
53
|
+
</span>
|
|
54
|
+
<button type="button" class="app-autolaunch-dependency-remove" data-app-autolaunch-remove-dependency="${escapeHtml(dependencyId)}" aria-label="Remove ${escapeHtml(title)}">
|
|
55
|
+
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
`
|
|
59
|
+
}
|
|
60
|
+
const renderDependencyScriptModal = () => {
|
|
61
|
+
const dependencyId = dependencyScriptPickerOpen
|
|
62
|
+
if (!dependencyId) return ""
|
|
63
|
+
const pickerState = dependencyScriptCandidates[dependencyId]
|
|
64
|
+
const dependency = dependencyById(dependencyId)
|
|
65
|
+
const title = dependency ? (dependency.title || dependency.name || dependency.id) : dependencyId
|
|
66
|
+
const folderPath = dependency ? dependencyFolderPath(dependency) : dependencyId
|
|
67
|
+
const fullFolderPath = dependency ? dependencyFullPath(dependency) : dependencyId
|
|
68
|
+
const icon = (dependency && dependency.icon) || "/pinokio-black.png"
|
|
69
|
+
const selectedDependencyScript = dependencyScriptSelections[dependencyId] || (dependency && dependency.autolaunch ? dependency.autolaunch : "")
|
|
70
|
+
let body = ""
|
|
71
|
+
if (!pickerState || pickerState.loading) {
|
|
72
|
+
body = '<div class="app-autolaunch-empty">Loading script choices...</div>'
|
|
73
|
+
} else if (pickerState.error) {
|
|
74
|
+
body = `<div class="app-autolaunch-empty">${escapeHtml(pickerState.error)}</div>`
|
|
75
|
+
} else {
|
|
76
|
+
const candidates = [].concat(pickerState.menu || [], pickerState.other || [])
|
|
77
|
+
if (candidates.length === 0) {
|
|
78
|
+
body = '<div class="app-autolaunch-empty">No local scripts were found for this app.</div>'
|
|
79
|
+
} else {
|
|
80
|
+
body = `
|
|
81
|
+
<div class="app-autolaunch-script-list">
|
|
82
|
+
${candidates.map((candidate) => {
|
|
83
|
+
const script = candidate && candidate.script ? candidate.script : ""
|
|
84
|
+
if (!script) return ""
|
|
85
|
+
const checked = script === selectedDependencyScript ? "checked" : ""
|
|
86
|
+
const selected = script === selectedDependencyScript ? " selected" : ""
|
|
87
|
+
return `
|
|
88
|
+
<label class="app-autolaunch-script-option${selected}">
|
|
89
|
+
<input type="radio" name="app-autolaunch-dependency-script-${escapeHtml(dependencyId)}" data-app-autolaunch-dependency-script-choice data-app-autolaunch-dependency-script-app="${escapeHtml(dependencyId)}" value="${escapeHtml(script)}" ${checked}>
|
|
90
|
+
<div>
|
|
91
|
+
<div class="app-autolaunch-script-title"><span>${escapeHtml(candidate.label || script)}</span></div>
|
|
92
|
+
<div class="app-autolaunch-script-path">${escapeHtml(script)}</div>
|
|
93
|
+
</div>
|
|
94
|
+
</label>
|
|
95
|
+
`
|
|
96
|
+
}).join("")}
|
|
97
|
+
</div>
|
|
98
|
+
`
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return `
|
|
102
|
+
<div class="app-autolaunch-dependency-script-modal" role="dialog" aria-modal="true" aria-label="Select launch script for ${escapeHtml(title)}">
|
|
103
|
+
<div class="app-autolaunch-dependency-script-panel">
|
|
104
|
+
<div class="app-autolaunch-dependency-script-head">
|
|
105
|
+
<div>
|
|
106
|
+
<div class="app-autolaunch-title">Select launch script</div>
|
|
107
|
+
<div class="app-autolaunch-note">This app must have a launch script before it can be required.</div>
|
|
108
|
+
</div>
|
|
109
|
+
<button type="button" class="app-autolaunch-close" data-app-autolaunch-close-dependency-script-modal aria-label="Close script selection">
|
|
110
|
+
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="app-autolaunch-dependency-script-app" title="${escapeHtml(fullFolderPath)}">
|
|
114
|
+
<img src="${escapeHtml(icon)}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
115
|
+
<div class="app-autolaunch-dependency-option-meta">
|
|
116
|
+
<div class="app-autolaunch-dependency-option-title">${escapeHtml(title)}</div>
|
|
117
|
+
<div class="app-autolaunch-dependency-option-path">${escapeHtml(folderPath)}</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="app-autolaunch-dependency-script-body">
|
|
121
|
+
${body}
|
|
122
|
+
</div>
|
|
123
|
+
<div class="app-autolaunch-dependency-script-actions">
|
|
124
|
+
<button type="button" class="app-autolaunch-primary" data-app-autolaunch-confirm-dependency-script data-app-autolaunch-dependency-script-app="${escapeHtml(dependencyId)}" ${selectedDependencyScript ? "" : "disabled"}>
|
|
125
|
+
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
|
126
|
+
<span class="app-autolaunch-primary-text">Add requirement</span>
|
|
127
|
+
${selectedDependencyScript ? `<span class="app-autolaunch-primary-script">${escapeHtml(selectedDependencyScript)}</span>` : ""}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
`
|
|
133
|
+
}
|
|
134
|
+
const configuredScript = (data) => data && data.current ? data.current : ""
|
|
135
|
+
const setFeedback = (message, isError = false) => {
|
|
136
|
+
feedbackEl.textContent = message || ""
|
|
137
|
+
feedbackEl.classList.toggle("error", !!isError)
|
|
138
|
+
}
|
|
139
|
+
const setBusy = (busy) => {
|
|
140
|
+
saving = !!busy
|
|
141
|
+
const blocked = saving || savingDependencies || savingDependencyScript || loadingCandidates
|
|
142
|
+
const setControlBusy = (control, disabled) => {
|
|
143
|
+
if (!control) return
|
|
144
|
+
const attr = "data-app-autolaunch-disabled-before-busy"
|
|
145
|
+
if (disabled) {
|
|
146
|
+
if (!control.hasAttribute(attr)) {
|
|
147
|
+
control.setAttribute(attr, control.disabled ? "true" : "false")
|
|
148
|
+
}
|
|
149
|
+
control.disabled = true
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const previous = control.getAttribute(attr)
|
|
153
|
+
if (previous !== null) {
|
|
154
|
+
control.disabled = previous === "true"
|
|
155
|
+
control.removeAttribute(attr)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
setControlBusy(switchButton, blocked)
|
|
159
|
+
button.toggleAttribute("aria-busy", saving || savingDependencies || savingDependencyScript)
|
|
160
|
+
scriptsEl.querySelectorAll("input").forEach((input) => {
|
|
161
|
+
setControlBusy(input, saving || savingDependencyScript)
|
|
162
|
+
})
|
|
163
|
+
scriptsEl.querySelectorAll("button").forEach((control) => {
|
|
164
|
+
setControlBusy(control, saving || savingDependencyScript)
|
|
165
|
+
})
|
|
166
|
+
dependenciesEl.querySelectorAll("button, input").forEach((control) => {
|
|
167
|
+
setControlBusy(control, saving || savingDependencies || savingDependencyScript)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
const renderStatus = () => {
|
|
171
|
+
const enabled = !!(state && state.autolaunch_enabled)
|
|
172
|
+
status.textContent = enabled ? "ON" : "OFF"
|
|
173
|
+
button.dataset.enabled = enabled ? "true" : "false"
|
|
174
|
+
button.setAttribute("aria-label", `Autolaunch. Start with Pinokio ${enabled ? "On" : "Off"}`)
|
|
175
|
+
switchButton.setAttribute("aria-checked", enabled ? "true" : "false")
|
|
176
|
+
switchButton.title = `Start with Pinokio is ${enabled ? "on" : "off"}`
|
|
177
|
+
switchLabel.textContent = enabled ? "ON" : "OFF"
|
|
178
|
+
root.classList.toggle("enabled", enabled)
|
|
179
|
+
}
|
|
180
|
+
const setOpen = (open) => {
|
|
181
|
+
root.classList.toggle("open", !!open)
|
|
182
|
+
button.setAttribute("aria-expanded", open ? "true" : "false")
|
|
183
|
+
modal.classList.toggle("hidden", !open)
|
|
184
|
+
if (open) {
|
|
185
|
+
if (!candidateState && !loadingCandidates) {
|
|
186
|
+
loadCandidates()
|
|
187
|
+
}
|
|
188
|
+
window.setTimeout(() => {
|
|
189
|
+
if (!modal.classList.contains("hidden")) {
|
|
190
|
+
switchButton.focus()
|
|
191
|
+
}
|
|
192
|
+
}, 0)
|
|
193
|
+
} else {
|
|
194
|
+
dependencyPickerOpen = false
|
|
195
|
+
pendingDependencyId = ""
|
|
196
|
+
dependencyScriptPickerOpen = ""
|
|
197
|
+
renderDependencies()
|
|
198
|
+
button.focus()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const renderScripts = () => {
|
|
202
|
+
if (loadingCandidates) {
|
|
203
|
+
scriptsEl.innerHTML = '<div class="app-autolaunch-empty">Loading scripts...</div>'
|
|
204
|
+
setBusy(false)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (candidateError) {
|
|
208
|
+
scriptsEl.innerHTML = `<div class="app-autolaunch-empty">${escapeHtml(candidateError)}</div>`
|
|
209
|
+
setBusy(false)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
if (!candidateState) {
|
|
213
|
+
scriptsEl.innerHTML = '<div class="app-autolaunch-empty">Open to load scripts.</div>'
|
|
214
|
+
setBusy(false)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const candidates = allCandidates().slice()
|
|
218
|
+
const current = candidateState.current || state.autolaunch || ""
|
|
219
|
+
if (current && !candidates.some((candidate) => candidate && candidate.script === current)) {
|
|
220
|
+
candidates.unshift({
|
|
221
|
+
script: current,
|
|
222
|
+
label: "Current",
|
|
223
|
+
source: "current",
|
|
224
|
+
current: true
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
if (candidates.length === 0) {
|
|
228
|
+
scriptsEl.innerHTML = '<div class="app-autolaunch-empty">No local scripts were found for this app.</div>'
|
|
229
|
+
setBusy(false)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
const dependencies = Array.isArray(state.autolaunch_depends) ? state.autolaunch_depends : []
|
|
233
|
+
const hasRequirements = dependencies.length > 0
|
|
234
|
+
const requirementWarning = hasRequirements && !current
|
|
235
|
+
? '<div class="app-autolaunch-feedback error">Choose this app\'s launch script before these requirements can run.</div>'
|
|
236
|
+
: ""
|
|
237
|
+
const clear = current
|
|
238
|
+
? `<button type="button" class="app-autolaunch-clear-script" data-app-autolaunch-clear-script title="${hasRequirements ? "Remove requirements before clearing this app's launch script." : "Clear launch script"}" ${hasRequirements ? "disabled" : ""}>
|
|
239
|
+
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
240
|
+
<span>Clear launch script</span>
|
|
241
|
+
</button>`
|
|
242
|
+
: ""
|
|
243
|
+
scriptsEl.innerHTML = requirementWarning + clear + candidates.map((candidate) => {
|
|
244
|
+
const script = candidate && candidate.script ? candidate.script : ""
|
|
245
|
+
if (!script) return ""
|
|
246
|
+
const label = candidate.label || script
|
|
247
|
+
const checked = script === selectedScript ? "checked" : ""
|
|
248
|
+
const selected = script === selectedScript ? " selected" : ""
|
|
249
|
+
const tags = [
|
|
250
|
+
candidate.current ? '<span class="app-autolaunch-tag">Current</span>' : "",
|
|
251
|
+
candidate.menu_default ? '<span class="app-autolaunch-tag">Default</span>' : "",
|
|
252
|
+
candidate.has_params ? '<span class="app-autolaunch-tag">Params ignored</span>' : ""
|
|
253
|
+
].filter(Boolean).join("")
|
|
254
|
+
const pathLine = label === script
|
|
255
|
+
? ""
|
|
256
|
+
: `<div class="app-autolaunch-script-path" title="${escapeHtml(script)}">${escapeHtml(script)}</div>`
|
|
257
|
+
return `
|
|
258
|
+
<label class="app-autolaunch-script-option${selected}">
|
|
259
|
+
<input type="radio" name="app-autolaunch-script" value="${escapeHtml(script)}" ${checked}>
|
|
260
|
+
<div>
|
|
261
|
+
<div class="app-autolaunch-script-title"><span>${escapeHtml(label)}</span>${tags}</div>
|
|
262
|
+
${pathLine}
|
|
263
|
+
</div>
|
|
264
|
+
</label>
|
|
265
|
+
`
|
|
266
|
+
}).join("")
|
|
267
|
+
setBusy(false)
|
|
268
|
+
}
|
|
269
|
+
const syncState = (updated) => {
|
|
270
|
+
if (updated) {
|
|
271
|
+
state = Object.assign({}, state, updated)
|
|
272
|
+
if (candidateState) {
|
|
273
|
+
candidateState.app = Object.assign({}, candidateState.app || {}, updated)
|
|
274
|
+
candidateState.current = updated.autolaunch || ""
|
|
275
|
+
}
|
|
276
|
+
selectedScript = updated.autolaunch || ""
|
|
277
|
+
}
|
|
278
|
+
renderStatus()
|
|
279
|
+
}
|
|
280
|
+
const renderDependencies = () => {
|
|
281
|
+
if (loadingCandidates) {
|
|
282
|
+
dependenciesEl.innerHTML = '<div class="app-autolaunch-empty">Loading dependencies...</div>'
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
if (!candidateState) {
|
|
286
|
+
dependenciesEl.innerHTML = '<div class="app-autolaunch-empty">Open to load dependencies.</div>'
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const dependencies = Array.isArray(state.autolaunch_depends) ? state.autolaunch_depends : []
|
|
290
|
+
const dependencySet = new Set(dependencies)
|
|
291
|
+
const displayDependencies = dependencies.slice()
|
|
292
|
+
const selectedRows = displayDependencies.length > 0
|
|
293
|
+
? displayDependencies.map(renderSelectedDependency).join("")
|
|
294
|
+
: '<div class="app-autolaunch-dependency-row-empty">No dependencies.</div>'
|
|
295
|
+
const availableDependencies = dependencyOptions()
|
|
296
|
+
.filter((candidate) => candidate && candidate.id && !dependencySet.has(candidate.id))
|
|
297
|
+
const picker = availableDependencies.length > 0
|
|
298
|
+
? `
|
|
299
|
+
<div class="app-autolaunch-dependency-picker" data-app-autolaunch-dependency-picker>
|
|
300
|
+
<button type="button" class="app-autolaunch-dependency-trigger" data-app-autolaunch-toggle-dependency-picker aria-expanded="${dependencyPickerOpen ? "true" : "false"}">
|
|
301
|
+
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
|
302
|
+
<span>Add dependency...</span>
|
|
303
|
+
<i class="fa-solid fa-chevron-${dependencyPickerOpen ? "up" : "down"}" aria-hidden="true"></i>
|
|
304
|
+
</button>
|
|
305
|
+
${dependencyPickerOpen ? `
|
|
306
|
+
<div class="app-autolaunch-dependency-menu">
|
|
307
|
+
<label class="app-autolaunch-dependency-filter">
|
|
308
|
+
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
|
309
|
+
<input type="search" placeholder="Search apps" data-app-autolaunch-dependency-search autocomplete="off">
|
|
310
|
+
</label>
|
|
311
|
+
<div class="app-autolaunch-dependency-options">
|
|
312
|
+
${availableDependencies.map(renderDependencyOption).join("")}
|
|
313
|
+
<div class="app-autolaunch-dependency-picker-empty" data-app-autolaunch-dependency-empty hidden>No apps match that search.</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
` : ""}
|
|
317
|
+
</div>
|
|
318
|
+
`
|
|
319
|
+
: `
|
|
320
|
+
<button type="button" class="app-autolaunch-dependency-trigger" disabled>
|
|
321
|
+
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
|
322
|
+
<span>No more apps to add</span>
|
|
323
|
+
</button>
|
|
324
|
+
`
|
|
325
|
+
dependenciesEl.innerHTML = `
|
|
326
|
+
<div class="app-autolaunch-dependency-stack">
|
|
327
|
+
<div class="app-autolaunch-dependency-selected">${selectedRows}</div>
|
|
328
|
+
${picker}
|
|
329
|
+
</div>
|
|
330
|
+
${renderDependencyScriptModal()}
|
|
331
|
+
`
|
|
332
|
+
setBusy(false)
|
|
333
|
+
}
|
|
334
|
+
const filterDependencyPicker = (picker) => {
|
|
335
|
+
if (!picker) return
|
|
336
|
+
const input = picker.querySelector("[data-app-autolaunch-dependency-search]")
|
|
337
|
+
const query = input ? input.value.trim().toLowerCase() : ""
|
|
338
|
+
let visibleCount = 0
|
|
339
|
+
picker.querySelectorAll("[data-app-autolaunch-add-dependency]").forEach((option) => {
|
|
340
|
+
const haystack = option.getAttribute("data-search-text") || ""
|
|
341
|
+
const visible = !query || haystack.includes(query)
|
|
342
|
+
option.hidden = !visible
|
|
343
|
+
if (visible) visibleCount += 1
|
|
344
|
+
})
|
|
345
|
+
const empty = picker.querySelector("[data-app-autolaunch-dependency-empty]")
|
|
346
|
+
if (empty) empty.hidden = visibleCount !== 0
|
|
347
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const dependencyAppsFor = (app) => {
|
|
2
|
+
if (!app) return [];
|
|
3
|
+
return autolaunchApps
|
|
4
|
+
.filter((candidate) => candidate && candidate.id && candidate.id !== app.id)
|
|
5
|
+
.slice()
|
|
6
|
+
.sort(compareApps);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const dependencyById = (appId) => autolaunchApps.find((candidate) => candidate && candidate.id === appId) || null;
|
|
10
|
+
|
|
11
|
+
const dependencyFolderPath = (app) => formatAppFolderPath(app);
|
|
12
|
+
|
|
13
|
+
const dependencyFullPath = (app) => String((app && (app.workspace_path || app.launcher_path || app.id)) || "");
|
|
14
|
+
|
|
15
|
+
const renderDependencyOption = (candidate) => {
|
|
16
|
+
const title = candidate.title || candidate.name || candidate.id;
|
|
17
|
+
const folderPath = dependencyFolderPath(candidate);
|
|
18
|
+
const fullFolderPath = dependencyFullPath(candidate);
|
|
19
|
+
const searchText = `${title} ${candidate.id || ""} ${folderPath} ${fullFolderPath}`.toLowerCase();
|
|
20
|
+
return `
|
|
21
|
+
<button type="button" class="autolaunch-dependency-option" data-add-dependency="${escapeHtml(candidate.id)}" data-search-text="${escapeHtml(searchText)}" title="${escapeHtml(fullFolderPath)}" ${savingDependencies ? "disabled" : ""}>
|
|
22
|
+
<img src="${escapeHtml(candidate.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
23
|
+
<span class="autolaunch-dependency-option-meta">
|
|
24
|
+
<span class="autolaunch-dependency-option-title">${escapeHtml(title)}</span>
|
|
25
|
+
<span class="autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
|
|
26
|
+
</span>
|
|
27
|
+
</button>
|
|
28
|
+
`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderSelectedDependency = (appId) => {
|
|
32
|
+
const candidate = dependencyById(appId);
|
|
33
|
+
const title = candidate ? (candidate.title || candidate.name || candidate.id) : appId;
|
|
34
|
+
const folderPath = candidate ? dependencyFolderPath(candidate) : appId;
|
|
35
|
+
const fullFolderPath = candidate ? dependencyFullPath(candidate) : appId;
|
|
36
|
+
const script = candidate && candidate.autolaunch ? candidate.autolaunch : "";
|
|
37
|
+
const scriptLine = script
|
|
38
|
+
? `<span class="autolaunch-dependency-option-path">${escapeHtml(script)}</span>`
|
|
39
|
+
: `<button type="button" class="autolaunch-dependency-configure" data-configure-dependency="${escapeHtml(appId)}" ${savingDependencies ? "disabled" : ""}>
|
|
40
|
+
<i class="fa-solid fa-gear" aria-hidden="true"></i>
|
|
41
|
+
Missing launch script
|
|
42
|
+
</button>`;
|
|
43
|
+
return `
|
|
44
|
+
<div class="autolaunch-dependency-row" title="${escapeHtml(fullFolderPath)}">
|
|
45
|
+
<img src="${escapeHtml((candidate && candidate.icon) || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
46
|
+
<span class="autolaunch-dependency-option-meta">
|
|
47
|
+
<span class="autolaunch-dependency-option-title">${escapeHtml(title)}</span>
|
|
48
|
+
<span class="autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
|
|
49
|
+
${scriptLine}
|
|
50
|
+
</span>
|
|
51
|
+
<button type="button" class="autolaunch-dependency-remove" data-remove-dependency="${escapeHtml(appId)}" aria-label="Remove ${escapeHtml(title)}" ${savingDependencies ? "disabled" : ""}>
|
|
52
|
+
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const renderDependencyScriptModal = () => {
|
|
59
|
+
const appId = dependencyScriptPickerOpen;
|
|
60
|
+
if (!appId) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
const state = dependencyScriptCandidates[appId];
|
|
64
|
+
const dependency = dependencyById(appId);
|
|
65
|
+
const title = dependency ? (dependency.title || dependency.name || dependency.id) : appId;
|
|
66
|
+
const folderPath = dependency ? dependencyFolderPath(dependency) : appId;
|
|
67
|
+
const fullFolderPath = dependency ? dependencyFullPath(dependency) : appId;
|
|
68
|
+
const appIcon = (dependency && dependency.icon) || "/pinokio-black.png";
|
|
69
|
+
const selectedDependencyScript = dependencyScriptSelections[appId] || (dependency && dependency.autolaunch ? dependency.autolaunch : "");
|
|
70
|
+
let body = "";
|
|
71
|
+
if (!state || state.loading) {
|
|
72
|
+
body = '<div class="autolaunch-loading"><i class="fa-solid fa-circle-notch fa-spin"></i> Loading script choices...</div>';
|
|
73
|
+
} else if (state.error) {
|
|
74
|
+
body = `<div class="autolaunch-empty">${escapeHtml(state.error)}</div>`;
|
|
75
|
+
} else {
|
|
76
|
+
const candidates = [].concat(state.menu || [], state.other || []);
|
|
77
|
+
if (candidates.length === 0) {
|
|
78
|
+
body = '<div class="autolaunch-empty">No local scripts were found for this app.</div>';
|
|
79
|
+
} else {
|
|
80
|
+
body = `
|
|
81
|
+
<div class="autolaunch-script-list">
|
|
82
|
+
${candidates.map((candidate) => {
|
|
83
|
+
const checked = candidate.script === selectedDependencyScript ? "checked" : "";
|
|
84
|
+
const selected = candidate.script === selectedDependencyScript ? " selected" : "";
|
|
85
|
+
const icon = candidate.icon || "fa-regular fa-file-code";
|
|
86
|
+
return `
|
|
87
|
+
<label class="autolaunch-script-option${selected}">
|
|
88
|
+
<input type="radio" name="dependency-script-${escapeHtml(appId)}" data-dependency-script-choice data-dependency-script-app="${escapeHtml(appId)}" value="${escapeHtml(candidate.script)}" ${checked} ${savingDependencyScript ? "disabled" : ""}>
|
|
89
|
+
<i class="${escapeHtml(icon)}"></i>
|
|
90
|
+
<div>
|
|
91
|
+
<div class="autolaunch-script-title"><span>${escapeHtml(candidate.label || candidate.script)}</span></div>
|
|
92
|
+
<div class="autolaunch-script-path">${escapeHtml(candidate.script)}</div>
|
|
93
|
+
</div>
|
|
94
|
+
</label>
|
|
95
|
+
`;
|
|
96
|
+
}).join("")}
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return `
|
|
102
|
+
<div class="autolaunch-dependency-script-modal" role="dialog" aria-modal="true" aria-label="Select launch script for ${escapeHtml(title)}">
|
|
103
|
+
<div class="autolaunch-dependency-script-panel">
|
|
104
|
+
<div class="autolaunch-dependency-script-head">
|
|
105
|
+
<div>
|
|
106
|
+
<h3 class="autolaunch-section-title">Select launch script</h3>
|
|
107
|
+
<div class="autolaunch-section-note">This app must have a launch script before it can be required.</div>
|
|
108
|
+
</div>
|
|
109
|
+
<button type="button" class="autolaunch-icon-button" data-close-dependency-script-modal aria-label="Close script selection" ${savingDependencyScript ? "disabled" : ""}>
|
|
110
|
+
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="autolaunch-dependency-script-app" title="${escapeHtml(fullFolderPath)}">
|
|
114
|
+
<img src="${escapeHtml(appIcon)}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
|
|
115
|
+
<span class="autolaunch-dependency-option-meta">
|
|
116
|
+
<span class="autolaunch-dependency-option-title">${escapeHtml(title)}</span>
|
|
117
|
+
<span class="autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="autolaunch-dependency-script-body">
|
|
121
|
+
${body}
|
|
122
|
+
</div>
|
|
123
|
+
<div class="autolaunch-dependency-script-actions">
|
|
124
|
+
<button type="button" class="autolaunch-primary" data-confirm-dependency-script data-dependency-script-app="${escapeHtml(appId)}" ${selectedDependencyScript ? "" : "disabled"}>
|
|
125
|
+
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
|
126
|
+
<span class="autolaunch-primary-text">Add requirement</span>
|
|
127
|
+
${selectedDependencyScript ? `<span class="autolaunch-primary-script">${escapeHtml(selectedDependencyScript)}</span>` : ""}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const renderDependencySection = (app) => {
|
|
136
|
+
if (!app) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
const dependencies = Array.isArray(app.autolaunch_depends) ? app.autolaunch_depends : [];
|
|
140
|
+
const dependencySet = new Set(dependencies);
|
|
141
|
+
const displayDependencies = dependencies.slice();
|
|
142
|
+
const dependencyApps = dependencyAppsFor(app);
|
|
143
|
+
const selectedRows = displayDependencies.length > 0
|
|
144
|
+
? displayDependencies.map(renderSelectedDependency).join("")
|
|
145
|
+
: '<div class="autolaunch-dependency-row-empty">No dependencies.</div>';
|
|
146
|
+
const availableDependencies = dependencyApps.filter((candidate) => !dependencySet.has(candidate.id));
|
|
147
|
+
const picker = availableDependencies.length > 0
|
|
148
|
+
? `
|
|
149
|
+
<div class="autolaunch-dependency-picker" data-dependency-picker>
|
|
150
|
+
<button type="button" class="autolaunch-dependency-trigger" data-toggle-dependency-picker aria-expanded="${dependencyPickerOpen ? "true" : "false"}" ${savingDependencies ? "disabled" : ""}>
|
|
151
|
+
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
|
152
|
+
<span>Add dependency...</span>
|
|
153
|
+
<i class="fa-solid fa-chevron-${dependencyPickerOpen ? "up" : "down"}" aria-hidden="true"></i>
|
|
154
|
+
</button>
|
|
155
|
+
${dependencyPickerOpen ? `
|
|
156
|
+
<div class="autolaunch-dependency-menu">
|
|
157
|
+
<label class="autolaunch-dependency-filter">
|
|
158
|
+
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
|
159
|
+
<input type="search" placeholder="Search apps" data-dependency-search autocomplete="off" ${savingDependencies ? "disabled" : ""}>
|
|
160
|
+
</label>
|
|
161
|
+
<div class="autolaunch-dependency-options">
|
|
162
|
+
${availableDependencies.map(renderDependencyOption).join("")}
|
|
163
|
+
<div class="autolaunch-dependency-picker-empty" data-dependency-empty hidden>No apps match that search.</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
` : ""}
|
|
167
|
+
</div>
|
|
168
|
+
`
|
|
169
|
+
: `
|
|
170
|
+
<button type="button" class="autolaunch-dependency-trigger" disabled>
|
|
171
|
+
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
|
172
|
+
<span>No more apps to add</span>
|
|
173
|
+
</button>
|
|
174
|
+
`;
|
|
175
|
+
return `
|
|
176
|
+
<div class="autolaunch-section">
|
|
177
|
+
<div class="autolaunch-section-header">
|
|
178
|
+
<h3 class="autolaunch-section-title">Requires</h3>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="autolaunch-dependency-list">
|
|
181
|
+
<div class="autolaunch-dependency-stack">
|
|
182
|
+
<div class="autolaunch-dependency-selected">${selectedRows}</div>
|
|
183
|
+
${picker}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
${renderDependencyScriptModal()}
|
|
188
|
+
`;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const filterDependencyPicker = (picker) => {
|
|
192
|
+
if (!picker) return;
|
|
193
|
+
const input = picker.querySelector("[data-dependency-search]");
|
|
194
|
+
const query = input ? input.value.trim().toLowerCase() : "";
|
|
195
|
+
let visibleCount = 0;
|
|
196
|
+
picker.querySelectorAll("[data-add-dependency]").forEach((option) => {
|
|
197
|
+
const haystack = option.getAttribute("data-search-text") || "";
|
|
198
|
+
const visible = !query || haystack.includes(query);
|
|
199
|
+
option.hidden = !visible;
|
|
200
|
+
if (visible) visibleCount += 1;
|
|
201
|
+
});
|
|
202
|
+
const empty = picker.querySelector("[data-dependency-empty]");
|
|
203
|
+
if (empty) empty.hidden = visibleCount !== 0;
|
|
204
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%- include('launch_settings_dependency_save_factory') %>
|
|
2
|
+
const saveDependencies = createLaunchSettingsDependencySaver({
|
|
3
|
+
getAppId: () => {
|
|
4
|
+
const app = currentApp();
|
|
5
|
+
return app ? app.id : "";
|
|
6
|
+
},
|
|
7
|
+
isSaving: () => savingDependencies,
|
|
8
|
+
setSaving: (value) => { savingDependencies = value; },
|
|
9
|
+
setBusy: setDetailBusy,
|
|
10
|
+
setFeedback,
|
|
11
|
+
onSaved: syncAppState,
|
|
12
|
+
render: renderCandidates
|
|
13
|
+
});
|