pinokiod 3.111.0 → 3.112.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/plugin.js +19 -7
- package/package.json +1 -1
- package/server/index.js +123 -15
- package/server/public/common.js +3 -0
- package/server/public/install.js +42 -5
- package/server/views/connect.ejs +1 -0
- package/server/views/index.ejs +1 -0
- package/server/views/init/index.ejs +2 -1
- package/server/views/net.ejs +1 -0
- package/server/views/network.ejs +1 -0
- package/server/views/screenshots.ejs +1 -0
- package/server/views/settings.ejs +1 -0
- package/server/views/terminals.ejs +807 -0
- package/server/views/tools.ejs +1 -0
package/kernel/plugin.js
CHANGED
|
@@ -14,13 +14,25 @@ class Plugin {
|
|
|
14
14
|
let plugins = []
|
|
15
15
|
for(let plugin_path of plugin_paths) {
|
|
16
16
|
let config = await this.kernel.require(path.resolve(plugin_dir, plugin_path))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
if (config && config.run && Array.isArray(config.run)) {
|
|
18
|
+
let invalid
|
|
19
|
+
for(let key in config) {
|
|
20
|
+
if (typeof config[key] === "function") {
|
|
21
|
+
invalid = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (invalid) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let chunks = plugin_path.split(path.sep)
|
|
29
|
+
let cwd = chunks.slice(0, -1).join("/")
|
|
30
|
+
config.image = "/asset/plugin/" + cwd + "/" + config.icon
|
|
31
|
+
plugins.push({
|
|
32
|
+
href: "/run/plugin/" + chunks.join("/"),
|
|
33
|
+
...config
|
|
34
|
+
})
|
|
35
|
+
}
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
this.config = {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -85,20 +85,21 @@ class Server {
|
|
|
85
85
|
this.cf = new Cloudflare()
|
|
86
86
|
this.virtualEnvCache = new Map()
|
|
87
87
|
this.gitStatusIgnorePatterns = [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
/(^|\/)node_modules\//,
|
|
89
|
+
// /(^|\/)vendor\//,
|
|
90
|
+
/(^|\/)__pycache__\//,
|
|
91
|
+
// /(^|\/)build\//,
|
|
92
|
+
// /(^|\/)dist\//,
|
|
93
|
+
// /(^|\/)tmp\//,
|
|
94
|
+
/(^|\/)\.cache\//,
|
|
95
|
+
/(^|\/)\.ruff_cache\//,
|
|
96
|
+
/(^|\/)\.tox\//,
|
|
97
|
+
/(^|\/)\.terraform\//,
|
|
98
|
+
/(^|\/)\.parcel-cache\//,
|
|
99
|
+
/(^|\/)\.webpack\//,
|
|
100
|
+
/(^|\/)\.mypy_cache\//,
|
|
101
|
+
/(^|\/)\.pytest_cache\//,
|
|
102
|
+
/(^|\/)\.git\//
|
|
102
103
|
]
|
|
103
104
|
|
|
104
105
|
// sometimes the C:\Windows\System32 is not in PATH, need to add
|
|
@@ -1374,6 +1375,25 @@ class Server {
|
|
|
1374
1375
|
let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
|
|
1375
1376
|
bin: this.kernel.bin.preset("ai"),
|
|
1376
1377
|
})
|
|
1378
|
+
let sanitizedPath = null
|
|
1379
|
+
if (typeof req.query.path === 'string') {
|
|
1380
|
+
let trimmed = req.query.path.trim()
|
|
1381
|
+
if (trimmed) {
|
|
1382
|
+
trimmed = trimmed.replace(/^~[\\/]?/, '').replace(/^[\\/]+/, '')
|
|
1383
|
+
if (trimmed) {
|
|
1384
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean)
|
|
1385
|
+
if (segments.length > 0 && !segments.some((segment) => segment === '.' || segment === '..')) {
|
|
1386
|
+
sanitizedPath = segments.join('/')
|
|
1387
|
+
try {
|
|
1388
|
+
await fs.promises.mkdir(path.resolve(this.kernel.homedir, sanitizedPath), { recursive: true })
|
|
1389
|
+
} catch (mkdirErr) {
|
|
1390
|
+
console.warn('Failed to ensure download path exists', mkdirErr)
|
|
1391
|
+
sanitizedPath = null
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1377
1397
|
res.render("download", {
|
|
1378
1398
|
portal: this.portal,
|
|
1379
1399
|
error,
|
|
@@ -1386,7 +1406,7 @@ class Server {
|
|
|
1386
1406
|
agent: req.agent,
|
|
1387
1407
|
userdir: this.kernel.api.userdir,
|
|
1388
1408
|
display: ["form"],
|
|
1389
|
-
query: req.query
|
|
1409
|
+
query: sanitizedPath ? { ...req.query, path: sanitizedPath } : req.query
|
|
1390
1410
|
})
|
|
1391
1411
|
} else if (pathComponents.length === 0 && req.query.mode === "settings") {
|
|
1392
1412
|
let system_env = {}
|
|
@@ -3766,6 +3786,94 @@ class Server {
|
|
|
3766
3786
|
list,
|
|
3767
3787
|
})
|
|
3768
3788
|
}))
|
|
3789
|
+
this.app.get("/terminals", ex(async (req, res) => {
|
|
3790
|
+
if (!this.kernel.plugin.config) {
|
|
3791
|
+
try {
|
|
3792
|
+
await this.kernel.plugin.init()
|
|
3793
|
+
} catch (err) {
|
|
3794
|
+
console.warn('Failed to initialize plugins', err)
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
const pluginMenu = this.kernel.plugin && this.kernel.plugin.config && Array.isArray(this.kernel.plugin.config.menu)
|
|
3798
|
+
? this.kernel.plugin.config.menu
|
|
3799
|
+
: []
|
|
3800
|
+
|
|
3801
|
+
const apps = []
|
|
3802
|
+
try {
|
|
3803
|
+
const apipath = this.kernel.path("api")
|
|
3804
|
+
const entries = await fs.promises.readdir(apipath, { withFileTypes: true })
|
|
3805
|
+
for (const entry of entries) {
|
|
3806
|
+
let type
|
|
3807
|
+
try {
|
|
3808
|
+
type = await Util.file_type(apipath, entry)
|
|
3809
|
+
} catch (typeErr) {
|
|
3810
|
+
console.warn('Failed to inspect api entry', entry.name, typeErr)
|
|
3811
|
+
continue
|
|
3812
|
+
}
|
|
3813
|
+
if (!type || !type.directory) {
|
|
3814
|
+
continue
|
|
3815
|
+
}
|
|
3816
|
+
try {
|
|
3817
|
+
const meta = await this.kernel.api.meta(entry.name)
|
|
3818
|
+
const absolutePath = meta && meta.path ? meta.path : this.kernel.path("api", entry.name)
|
|
3819
|
+
let displayPath = absolutePath
|
|
3820
|
+
if (this.kernel.homedir && absolutePath.startsWith(this.kernel.homedir)) {
|
|
3821
|
+
const relative = path.relative(this.kernel.homedir, absolutePath)
|
|
3822
|
+
if (!relative || relative === '.' || relative === '') {
|
|
3823
|
+
displayPath = '~'
|
|
3824
|
+
} else if (!relative.startsWith('..')) {
|
|
3825
|
+
const normalized = relative.split(path.sep).join('/')
|
|
3826
|
+
displayPath = `~/${normalized}`
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
apps.push({
|
|
3830
|
+
name: entry.name,
|
|
3831
|
+
title: meta && meta.title ? meta.title : entry.name,
|
|
3832
|
+
description: meta && meta.description ? meta.description : '',
|
|
3833
|
+
icon: meta && meta.icon ? meta.icon : "/pinokio-black.png",
|
|
3834
|
+
cwd: absolutePath,
|
|
3835
|
+
displayPath
|
|
3836
|
+
})
|
|
3837
|
+
} catch (metaError) {
|
|
3838
|
+
console.warn('Failed to load app metadata', entry.name, metaError)
|
|
3839
|
+
const fallbackPath = this.kernel.path("api", entry.name)
|
|
3840
|
+
apps.push({
|
|
3841
|
+
name: entry.name,
|
|
3842
|
+
title: entry.name,
|
|
3843
|
+
description: '',
|
|
3844
|
+
icon: "/pinokio-black.png",
|
|
3845
|
+
cwd: fallbackPath,
|
|
3846
|
+
displayPath: fallbackPath
|
|
3847
|
+
})
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
} catch (enumerationError) {
|
|
3851
|
+
console.warn('Failed to enumerate api apps for plugin modal', enumerationError)
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
apps.sort((a, b) => {
|
|
3855
|
+
const at = (a.title || a.name || '').toLowerCase()
|
|
3856
|
+
const bt = (b.title || b.name || '').toLowerCase()
|
|
3857
|
+
if (at < bt) return -1
|
|
3858
|
+
if (at > bt) return 1
|
|
3859
|
+
return (a.name || '').localeCompare(b.name || '')
|
|
3860
|
+
})
|
|
3861
|
+
|
|
3862
|
+
const list = this.getPeers()
|
|
3863
|
+
res.render("terminals", {
|
|
3864
|
+
current_host: this.kernel.peer.host,
|
|
3865
|
+
pluginMenu,
|
|
3866
|
+
apps,
|
|
3867
|
+
portal: this.portal,
|
|
3868
|
+
logo: this.logo,
|
|
3869
|
+
theme: this.theme,
|
|
3870
|
+
agent: req.agent,
|
|
3871
|
+
list,
|
|
3872
|
+
})
|
|
3873
|
+
}))
|
|
3874
|
+
this.app.get("/plugins", (req, res) => {
|
|
3875
|
+
res.redirect(301, "/terminals")
|
|
3876
|
+
})
|
|
3769
3877
|
this.app.get("/screenshots", ex(async (req, res) => {
|
|
3770
3878
|
let list = this.getPeers()
|
|
3771
3879
|
res.render("screenshots", {
|
package/server/public/common.js
CHANGED
|
@@ -1592,6 +1592,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1592
1592
|
}
|
|
1593
1593
|
if (document.querySelector("#refresh-page")) {
|
|
1594
1594
|
document.querySelector("#refresh-page").addEventListener("click", (e) => {
|
|
1595
|
+
location.reload()
|
|
1596
|
+
/*
|
|
1595
1597
|
let browserview = document.querySelector(".browserview")
|
|
1596
1598
|
if (browserview) {
|
|
1597
1599
|
let iframe = browserview.querySelector("iframe:not(.hidden)")
|
|
@@ -1604,6 +1606,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1604
1606
|
} else {
|
|
1605
1607
|
location.reload()
|
|
1606
1608
|
}
|
|
1609
|
+
*/
|
|
1607
1610
|
})
|
|
1608
1611
|
}
|
|
1609
1612
|
const handleSplitNavigation = async (anchor) => {
|
package/server/public/install.js
CHANGED
|
@@ -38,9 +38,38 @@ const installname = async (url, name) => {
|
|
|
38
38
|
return null
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
const DEFAULT_INSTALL_RELATIVE_PATH = 'api'
|
|
42
|
+
|
|
43
|
+
// Ensure the requested install path stays within the Pinokio home directory
|
|
44
|
+
const normalizeInstallPath = (rawPath) => {
|
|
45
|
+
if (typeof rawPath !== 'string') {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
let trimmed = rawPath.trim()
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
// drop leading ~/ or any absolute indicators
|
|
53
|
+
trimmed = trimmed.replace(/^~[\\/]?/, '').replace(/^[\\/]+/, '')
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean)
|
|
58
|
+
if (!segments.length) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
if (segments.some((segment) => segment === '.' || segment === '..')) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
return segments.join('/')
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
const install = async (name, url, term, socket, options) => {
|
|
42
68
|
console.log("options", options)
|
|
43
69
|
const n = new N()
|
|
70
|
+
const normalizedPath = options && options.path ? normalizeInstallPath(options.path) : null
|
|
71
|
+
const targetPath = normalizedPath ? `~/${normalizedPath}` : `~/${DEFAULT_INSTALL_RELATIVE_PATH}`
|
|
72
|
+
|
|
44
73
|
await new Promise((resolve, reject) => {
|
|
45
74
|
socket.close()
|
|
46
75
|
|
|
@@ -68,7 +97,7 @@ const install = async (name, url, term, socket, options) => {
|
|
|
68
97
|
},
|
|
69
98
|
params: {
|
|
70
99
|
message: cmd,
|
|
71
|
-
path:
|
|
100
|
+
path: targetPath
|
|
72
101
|
}
|
|
73
102
|
}, (packet) => {
|
|
74
103
|
if (packet.type === 'stream') {
|
|
@@ -107,10 +136,18 @@ const install = async (name, url, term, socket, options) => {
|
|
|
107
136
|
}
|
|
108
137
|
})
|
|
109
138
|
} else {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
const shouldInitialize = !normalizedPath || normalizedPath === DEFAULT_INSTALL_RELATIVE_PATH
|
|
140
|
+
if (shouldInitialize) {
|
|
141
|
+
// ask the backend to create install.json and start.json if gradio
|
|
142
|
+
//location.href = `/pinokio/browser/${name}`
|
|
143
|
+
location.href = `/initialize/${name}`
|
|
144
|
+
} else {
|
|
145
|
+
n.Noty({
|
|
146
|
+
text: `Downloaded to ~/${normalizedPath}/${name}`,
|
|
147
|
+
timeout: 4000
|
|
148
|
+
})
|
|
149
|
+
location.href = "/terminals"
|
|
150
|
+
}
|
|
114
151
|
}
|
|
115
152
|
}
|
|
116
153
|
const createTerm = async (_theme) => {
|
package/server/views/connect.ejs
CHANGED
|
@@ -919,6 +919,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
919
919
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
920
920
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
921
921
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
922
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
922
923
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
923
924
|
</aside>
|
|
924
925
|
</main>
|
package/server/views/index.ejs
CHANGED
|
@@ -726,6 +726,7 @@ body.dark aside .current.selected {
|
|
|
726
726
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
727
727
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
728
728
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
729
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
729
730
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
730
731
|
</aside>
|
|
731
732
|
</main>
|
|
@@ -1554,7 +1554,7 @@ body.dark .ace-editor {
|
|
|
1554
1554
|
</button>
|
|
1555
1555
|
<button type="button" class="tab-button" data-tab="cli">
|
|
1556
1556
|
<div class='row'>
|
|
1557
|
-
<i class="fa-solid fa-
|
|
1557
|
+
<i class="fa-solid fa-desktop"></i>
|
|
1558
1558
|
<div><h5>Terminal App Launcher</h5></div>
|
|
1559
1559
|
</div>
|
|
1560
1560
|
</button>
|
|
@@ -1948,6 +1948,7 @@ body.dark .ace-editor {
|
|
|
1948
1948
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
1949
1949
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
1950
1950
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
1951
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
1951
1952
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
1952
1953
|
</aside>
|
|
1953
1954
|
</main>
|
package/server/views/net.ejs
CHANGED
|
@@ -819,6 +819,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
819
819
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
820
820
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
821
821
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
822
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
822
823
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
823
824
|
</aside>
|
|
824
825
|
</main>
|
package/server/views/network.ejs
CHANGED
|
@@ -1179,6 +1179,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1179
1179
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
1180
1180
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
1181
1181
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
1182
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
1182
1183
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
1183
1184
|
</aside>
|
|
1184
1185
|
</main>
|
|
@@ -771,6 +771,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
771
771
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
772
772
|
<a class='tab selected' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
773
773
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
774
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
774
775
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
775
776
|
</aside>
|
|
776
777
|
</main>
|
|
@@ -566,6 +566,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
566
566
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
567
567
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
568
568
|
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
569
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
569
570
|
<a class='tab selected' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
570
571
|
</aside>
|
|
571
572
|
</main>
|
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>Pinokio Terminals</title>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
6
|
+
<link href="/xterm.min.css" rel="stylesheet" />
|
|
7
|
+
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
8
|
+
<link href="/css/solid.min.css" rel="stylesheet">
|
|
9
|
+
<link href="/css/regular.min.css" rel="stylesheet">
|
|
10
|
+
<link href="/css/brands.min.css" rel="stylesheet">
|
|
11
|
+
<link href="/markdown.css" rel="stylesheet"/>
|
|
12
|
+
<link href="/noty.css" rel="stylesheet"/>
|
|
13
|
+
<link href="/urldropdown.css" rel="stylesheet"/>
|
|
14
|
+
<link href="/style.css" rel="stylesheet"/>
|
|
15
|
+
<% if (agent === "electron") { %>
|
|
16
|
+
<link href="/electron.css" rel="stylesheet"/>
|
|
17
|
+
<% } %>
|
|
18
|
+
<style>
|
|
19
|
+
body.plugin-page main {
|
|
20
|
+
display: flex;
|
|
21
|
+
}
|
|
22
|
+
body.plugin-page aside {
|
|
23
|
+
width: 200px;
|
|
24
|
+
display: block;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
}
|
|
27
|
+
body.plugin-page aside .tab {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 5px;
|
|
31
|
+
color: inherit;
|
|
32
|
+
text-decoration: none;
|
|
33
|
+
padding: 10px;
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
opacity: 0.5;
|
|
36
|
+
border-left: 10px solid transparent;
|
|
37
|
+
transition: color 0.2s ease, opacity 0.2s ease;
|
|
38
|
+
}
|
|
39
|
+
body.plugin-page aside .tab.selected {
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
opacity: 1;
|
|
42
|
+
}
|
|
43
|
+
body.plugin-page aside .tab i {
|
|
44
|
+
width: 20px;
|
|
45
|
+
text-align: center;
|
|
46
|
+
}
|
|
47
|
+
body.plugin-page .plugin-container {
|
|
48
|
+
flex: 1;
|
|
49
|
+
padding: 30px 40px;
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
}
|
|
52
|
+
body.plugin-page .btn-tab {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 10px;
|
|
56
|
+
margin-bottom: 15px;
|
|
57
|
+
}
|
|
58
|
+
body.plugin-page .btn-tab .btn {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 6px;
|
|
62
|
+
}
|
|
63
|
+
.plugin-container h1 {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 10px;
|
|
67
|
+
margin: 0 0 20px;
|
|
68
|
+
}
|
|
69
|
+
/*
|
|
70
|
+
.plugin-container h1 i {
|
|
71
|
+
color: rgba(127, 91, 243, 0.9);
|
|
72
|
+
}
|
|
73
|
+
*/
|
|
74
|
+
.plugin-grid {
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
|
77
|
+
gap: 14px;
|
|
78
|
+
}
|
|
79
|
+
.plugin-card {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 12px;
|
|
83
|
+
padding: 12px;
|
|
84
|
+
border-radius: 10px;
|
|
85
|
+
background: rgba(0, 0, 0, 0.03);
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
transition: background 0.15s ease, transform 0.15s ease;
|
|
88
|
+
text-decoration: none;
|
|
89
|
+
color: inherit;
|
|
90
|
+
min-height: 68px;
|
|
91
|
+
}
|
|
92
|
+
.plugin-card:hover,
|
|
93
|
+
.plugin-card:focus-visible {
|
|
94
|
+
background: rgba(0, 0, 0, 0.08);
|
|
95
|
+
}
|
|
96
|
+
.plugin-card:focus-visible {
|
|
97
|
+
outline: 2px solid rgba(127, 91, 243, 0.6);
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
}
|
|
100
|
+
body.dark .plugin-card {
|
|
101
|
+
background: rgba(255, 255, 255, 0.05);
|
|
102
|
+
color: white;
|
|
103
|
+
}
|
|
104
|
+
body.dark .plugin-card:hover,
|
|
105
|
+
body.dark .plugin-card:focus-visible {
|
|
106
|
+
background: rgba(255, 255, 255, 0.12);
|
|
107
|
+
}
|
|
108
|
+
.plugin-card img,
|
|
109
|
+
.plugin-card .plugin-icon {
|
|
110
|
+
width: 44px;
|
|
111
|
+
height: 44px;
|
|
112
|
+
border-radius: 8px;
|
|
113
|
+
object-fit: cover;
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
background: white;
|
|
116
|
+
}
|
|
117
|
+
body.dark .plugin-card img,
|
|
118
|
+
body.dark .plugin-card .plugin-icon {
|
|
119
|
+
background: rgba(255, 255, 255, 0.08);
|
|
120
|
+
}
|
|
121
|
+
.plugin-card .plugin-icon {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
font-size: 20px;
|
|
126
|
+
color: rgba(0, 0, 0, 0.65);
|
|
127
|
+
}
|
|
128
|
+
body.dark .plugin-card .plugin-icon {
|
|
129
|
+
color: rgba(255, 255, 255, 0.85);
|
|
130
|
+
}
|
|
131
|
+
.plugin-card .plugin-details {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
gap: 4px;
|
|
135
|
+
}
|
|
136
|
+
.plugin-card h2 {
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-size: 15px;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
}
|
|
141
|
+
.plugin-card .subtitle {
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
opacity: 0.65;
|
|
144
|
+
}
|
|
145
|
+
body.dark .plugin-card .subtitle {
|
|
146
|
+
opacity: 0.75;
|
|
147
|
+
}
|
|
148
|
+
.plugin-card .plugin-info {
|
|
149
|
+
background: none;
|
|
150
|
+
border: none;
|
|
151
|
+
color: rgba(0, 0, 0, 0.55);
|
|
152
|
+
padding: 6px;
|
|
153
|
+
border-radius: 18px;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
156
|
+
}
|
|
157
|
+
body.dark .plugin-card .plugin-info {
|
|
158
|
+
color: rgba(255, 255, 255, 0.7);
|
|
159
|
+
}
|
|
160
|
+
.plugin-card .plugin-info:hover,
|
|
161
|
+
.plugin-card .plugin-info:focus-visible {
|
|
162
|
+
background: rgba(0, 0, 0, 0.08);
|
|
163
|
+
color: rgba(0, 0, 0, 0.9);
|
|
164
|
+
}
|
|
165
|
+
body.dark .plugin-card .plugin-info:hover,
|
|
166
|
+
body.dark .plugin-card .plugin-info:focus-visible {
|
|
167
|
+
background: rgba(255, 255, 255, 0.18);
|
|
168
|
+
color: rgba(255, 255, 255, 0.95);
|
|
169
|
+
}
|
|
170
|
+
.plugin-card .disclosure-indicator {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
width: 18px;
|
|
175
|
+
color: rgba(0, 0, 0, 0.35);
|
|
176
|
+
pointer-events: none;
|
|
177
|
+
transition: transform 0.15s ease, color 0.15s ease;
|
|
178
|
+
}
|
|
179
|
+
.plugin-card:hover .disclosure-indicator,
|
|
180
|
+
.plugin-card:focus-visible .disclosure-indicator {
|
|
181
|
+
transform: translateX(2px);
|
|
182
|
+
color: rgba(0, 0, 0, 0.65);
|
|
183
|
+
}
|
|
184
|
+
body.dark .plugin-card .disclosure-indicator {
|
|
185
|
+
color: rgba(255, 255, 255, 0.4);
|
|
186
|
+
}
|
|
187
|
+
body.dark .plugin-card:hover .disclosure-indicator,
|
|
188
|
+
body.dark .plugin-card:focus-visible .disclosure-indicator {
|
|
189
|
+
color: rgba(255, 255, 255, 0.85);
|
|
190
|
+
}
|
|
191
|
+
.plugin-empty {
|
|
192
|
+
padding: 35px;
|
|
193
|
+
border-radius: 12px;
|
|
194
|
+
border: 1px dashed rgba(0, 0, 0, 0.12);
|
|
195
|
+
background: rgba(0, 0, 0, 0.02);
|
|
196
|
+
text-align: center;
|
|
197
|
+
}
|
|
198
|
+
body.dark .plugin-empty {
|
|
199
|
+
border-color: rgba(255, 255, 255, 0.16);
|
|
200
|
+
background: rgba(255, 255, 255, 0.04);
|
|
201
|
+
}
|
|
202
|
+
.plugin-empty p {
|
|
203
|
+
margin: 0 0 12px;
|
|
204
|
+
}
|
|
205
|
+
.plugin-empty code {
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
}
|
|
208
|
+
.plugin-modal-overlay {
|
|
209
|
+
z-index: 900;
|
|
210
|
+
}
|
|
211
|
+
.plugin-modal-overlay .url-modal-content {
|
|
212
|
+
max-width: 520px;
|
|
213
|
+
}
|
|
214
|
+
.plugin-modal-overlay .plugin-modal-dropdown {
|
|
215
|
+
max-height: 360px;
|
|
216
|
+
overflow-y: auto;
|
|
217
|
+
margin-top: 14px;
|
|
218
|
+
position: static;
|
|
219
|
+
display: block;
|
|
220
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
221
|
+
border-radius: 10px;
|
|
222
|
+
background: inherit;
|
|
223
|
+
top: auto;
|
|
224
|
+
left: auto;
|
|
225
|
+
right: auto;
|
|
226
|
+
}
|
|
227
|
+
.plugin-modal-overlay .plugin-modal-dropdown .url-dropdown-item {
|
|
228
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
229
|
+
}
|
|
230
|
+
.plugin-modal-overlay .plugin-modal-dropdown .url-dropdown-item:last-child {
|
|
231
|
+
border-bottom: none;
|
|
232
|
+
}
|
|
233
|
+
body.dark .plugin-modal-overlay .url-modal-content {
|
|
234
|
+
background: rgba(25, 26, 27, 0.98);
|
|
235
|
+
}
|
|
236
|
+
body.dark .plugin-modal-overlay .plugin-modal-dropdown {
|
|
237
|
+
border-color: rgba(255, 255, 255, 0.15);
|
|
238
|
+
background: inherit;
|
|
239
|
+
}
|
|
240
|
+
body.dark .plugin-modal-overlay .plugin-modal-dropdown .url-dropdown-item {
|
|
241
|
+
border-bottom-color: rgba(255, 255, 255, 0.12);
|
|
242
|
+
}
|
|
243
|
+
.plugin-option {
|
|
244
|
+
display: flex;
|
|
245
|
+
flex-direction: row;
|
|
246
|
+
gap: 10px;
|
|
247
|
+
align-items: center;
|
|
248
|
+
}
|
|
249
|
+
.plugin-option .option-icon {
|
|
250
|
+
width: 26px;
|
|
251
|
+
height: 26px;
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
background: rgba(0, 0, 0, 0.08);
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
justify-content: center;
|
|
257
|
+
font-size: 14px;
|
|
258
|
+
flex-shrink: 0;
|
|
259
|
+
}
|
|
260
|
+
.plugin-option img.option-icon {
|
|
261
|
+
object-fit: cover;
|
|
262
|
+
background: white;
|
|
263
|
+
}
|
|
264
|
+
body.dark .plugin-option .option-icon {
|
|
265
|
+
background: rgba(255, 255, 255, 0.12);
|
|
266
|
+
}
|
|
267
|
+
.plugin-option .option-body {
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-direction: column;
|
|
270
|
+
}
|
|
271
|
+
.plugin-option .option-name {
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
}
|
|
274
|
+
.plugin-option .option-path {
|
|
275
|
+
font-size: 12px;
|
|
276
|
+
opacity: 0.7;
|
|
277
|
+
}
|
|
278
|
+
.plugin-option .option-description {
|
|
279
|
+
font-size: 12px;
|
|
280
|
+
opacity: 0.62;
|
|
281
|
+
margin-top: 2px;
|
|
282
|
+
}
|
|
283
|
+
.plugin-option.selected {
|
|
284
|
+
border: 1px solid rgba(127, 91, 243, 0.6);
|
|
285
|
+
background: rgba(127, 91, 243, 0.08);
|
|
286
|
+
}
|
|
287
|
+
body.dark .plugin-option.selected {
|
|
288
|
+
border-color: rgba(127, 91, 243, 0.8);
|
|
289
|
+
background: rgba(127, 91, 243, 0.18);
|
|
290
|
+
}
|
|
291
|
+
.plugin-option:hover {
|
|
292
|
+
background: rgba(0, 0, 0, 0.04);
|
|
293
|
+
}
|
|
294
|
+
body.dark .plugin-option:hover {
|
|
295
|
+
background: rgba(255, 255, 255, 0.08);
|
|
296
|
+
}
|
|
297
|
+
.plugin-modal-overlay .url-modal-input {
|
|
298
|
+
margin-top: 18px;
|
|
299
|
+
}
|
|
300
|
+
@media (max-width: 800px) {
|
|
301
|
+
body.plugin-page .plugin-container {
|
|
302
|
+
padding: 20px;
|
|
303
|
+
}
|
|
304
|
+
.plugin-grid {
|
|
305
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
@media (max-width: 600px) {
|
|
309
|
+
body.plugin-page main {
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
}
|
|
312
|
+
body.plugin-page aside {
|
|
313
|
+
width: auto;
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-wrap: wrap;
|
|
316
|
+
gap: 6px;
|
|
317
|
+
padding: 0 20px 20px;
|
|
318
|
+
}
|
|
319
|
+
body.plugin-page aside .tab,
|
|
320
|
+
body.plugin-page aside .btn-tab {
|
|
321
|
+
border-left: none;
|
|
322
|
+
opacity: 1;
|
|
323
|
+
}
|
|
324
|
+
body.plugin-page aside .caption {
|
|
325
|
+
display: none;
|
|
326
|
+
}
|
|
327
|
+
body.plugin-page .btn-tab {
|
|
328
|
+
width: 100%;
|
|
329
|
+
justify-content: center;
|
|
330
|
+
gap: 8px;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
334
|
+
<% const serializedPlugins = pluginMenu.map((plugin) => {
|
|
335
|
+
const hrefValue = plugin.href || ''
|
|
336
|
+
let pluginPath = plugin.src || ''
|
|
337
|
+
const extraParams = []
|
|
338
|
+
if (hrefValue) {
|
|
339
|
+
try {
|
|
340
|
+
const parsed = new URL(hrefValue.startsWith('http') ? hrefValue : `http://localhost${hrefValue}`)
|
|
341
|
+
if (!pluginPath) {
|
|
342
|
+
pluginPath = parsed.pathname.replace(/^\/run/, '') || ''
|
|
343
|
+
}
|
|
344
|
+
parsed.searchParams.forEach((value, key) => {
|
|
345
|
+
if (key === 'cwd') {
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
extraParams.push([key, value])
|
|
349
|
+
})
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.warn('Failed to parse plugin href for serialization', hrefValue, err)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (pluginPath && !pluginPath.startsWith('/')) {
|
|
355
|
+
pluginPath = `/${pluginPath}`
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
title: plugin.title || plugin.text || plugin.name || 'Plugin',
|
|
359
|
+
description: plugin.description || '',
|
|
360
|
+
href: hrefValue,
|
|
361
|
+
link: plugin.link || '',
|
|
362
|
+
image: plugin.image || null,
|
|
363
|
+
icon: plugin.icon || null,
|
|
364
|
+
pluginPath,
|
|
365
|
+
extraParams
|
|
366
|
+
}
|
|
367
|
+
}) %>
|
|
368
|
+
</head>
|
|
369
|
+
<body class='<%=theme%> plugin-page' data-agent="<%=agent%>">
|
|
370
|
+
<header class='grabbable'>
|
|
371
|
+
<h1>
|
|
372
|
+
<a class='home' href="/home"><img class='icon' src="/pinokio-black.png" alt="Pinokio"></a>
|
|
373
|
+
<button class='btn2' id='back' data-tippy-content="back"><div><i class="fa-solid fa-chevron-left"></i></div></button>
|
|
374
|
+
<button class='btn2' id='forward' data-tippy-content="forward"><div><i class="fa-solid fa-chevron-right"></i></div></button>
|
|
375
|
+
<button class='btn2' id='refresh-page' data-tippy-content="refresh"><div><i class="fa-solid fa-rotate-right"></i></div></button>
|
|
376
|
+
<button class='btn2' id='screenshot' data-tippy-content="take a screenshot"><i class="fa-solid fa-camera"></i></button>
|
|
377
|
+
<div class='flexible'></div>
|
|
378
|
+
<a class='btn2' href="/columns" data-tippy-content="split into 2 columns">
|
|
379
|
+
<div><i class="fa-solid fa-table-columns"></i></div>
|
|
380
|
+
</a>
|
|
381
|
+
<a class='btn2' href="/rows" data-tippy-content="split into 2 rows">
|
|
382
|
+
<div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
|
|
383
|
+
</a>
|
|
384
|
+
<button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
|
|
385
|
+
<div><i class="fa-solid fa-plus"></i></div>
|
|
386
|
+
</button>
|
|
387
|
+
<button class='btn2 hidden' id='close-window' data-tippy-content='close this section'>
|
|
388
|
+
<div><i class="fa-solid fa-xmark"></i></div>
|
|
389
|
+
</button>
|
|
390
|
+
</h1>
|
|
391
|
+
</header>
|
|
392
|
+
<main class='plugin-main'>
|
|
393
|
+
<div class='plugin-container'>
|
|
394
|
+
<h1><i class="fa-solid fa-desktop"></i>Terminals</h1>
|
|
395
|
+
<% if (pluginMenu.length === 0) { %>
|
|
396
|
+
<div class='plugin-empty'>
|
|
397
|
+
<p>No terminals found. Clone or add terminal definitions under <code>~/pinokio/plugin</code> to see them listed here.</p>
|
|
398
|
+
<a class='btn' href="/home?mode=download"><i class="fa-solid fa-download"></i> Download Terminals</a>
|
|
399
|
+
</div>
|
|
400
|
+
<% } else { %>
|
|
401
|
+
<div class='plugin-grid'>
|
|
402
|
+
<% pluginMenu.forEach((pluginItem, index) => { %>
|
|
403
|
+
<div class='plugin-card' role="button" tabindex="0" data-plugin-index="<%=index%>">
|
|
404
|
+
<% if (pluginItem.image) { %>
|
|
405
|
+
<img src="<%=pluginItem.image%>" alt="<%=pluginItem.title || pluginItem.text || 'Plugin'%> icon">
|
|
406
|
+
<% } else if (pluginItem.icon) { %>
|
|
407
|
+
<div class='plugin-icon'><i class="<%=pluginItem.icon%>"></i></div>
|
|
408
|
+
<% } else { %>
|
|
409
|
+
<div class='plugin-icon'><i class="fa-solid fa-desktop"></i></div>
|
|
410
|
+
<% } %>
|
|
411
|
+
<div class='plugin-details'>
|
|
412
|
+
<h2><%=pluginItem.title || pluginItem.text || pluginItem.name || 'Plugin'%></h2>
|
|
413
|
+
<% if (pluginItem.description) { %>
|
|
414
|
+
<div class='subtitle'><%=pluginItem.description%></div>
|
|
415
|
+
<% } %>
|
|
416
|
+
</div>
|
|
417
|
+
<div class='flexible'></div>
|
|
418
|
+
<% if (pluginItem.link) { %>
|
|
419
|
+
<button type='button' class='plugin-info' data-link="<%=pluginItem.link.replace(/"/g, '"')%>" aria-label="Open info"><i class="fa-solid fa-circle-info"></i></button>
|
|
420
|
+
<% } %>
|
|
421
|
+
<div class='disclosure-indicator' aria-hidden="true">
|
|
422
|
+
<i class="fa-solid fa-chevron-right"></i>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<% }) %>
|
|
426
|
+
</div>
|
|
427
|
+
<% } %>
|
|
428
|
+
</div>
|
|
429
|
+
<aside>
|
|
430
|
+
<div class='btn-tab'>
|
|
431
|
+
<button type='button' class='btn' id='create-launcher-button'><i class="fa-solid fa-plus"></i><div class='caption'>Create</div></button>
|
|
432
|
+
<a class='btn' id='explore' href="/home?mode=explore"><i class="fa-solid fa-globe"></i><div class='caption'>Discover</div></a>
|
|
433
|
+
</div>
|
|
434
|
+
<a href="/" class='tab'><i class='fas fa-laptop-code'></i><div class='caption'>This machine</div></a>
|
|
435
|
+
<a href="/network" class='tab'><i class="fa-solid fa-wifi"></i><div class='caption'>Local network</div></a>
|
|
436
|
+
<% if (list.length > 0) { %>
|
|
437
|
+
<% let brands = { win32: "windows", darwin: "apple", linux: "Linux" } %>
|
|
438
|
+
<% list.forEach(({ host, name, platform }) => { %>
|
|
439
|
+
<a href="/net/<%=name%>" class='submenu tab'><i class="fa-brands fa-<%=brands[platform]%>"></i><div class='caption'><%=name%> (<%=current_host === host ? 'this machine' : host%>)</div></a>
|
|
440
|
+
<% }) %>
|
|
441
|
+
<% } %>
|
|
442
|
+
<a href="/connect" class='tab'><i class="fa-solid fa-plug"></i><div class='caption'>Login</div></a>
|
|
443
|
+
<a class='tab' href="<%=portal%>" target="_blank"><i class="fa-solid fa-question"></i><div class='caption'>Help</div></a>
|
|
444
|
+
<a class='tab' id='genlog'><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
|
|
445
|
+
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
446
|
+
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
447
|
+
<a class='tab' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
448
|
+
<a class='tab selected' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
449
|
+
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
450
|
+
</aside>
|
|
451
|
+
</main>
|
|
452
|
+
<script type="application/json" id="plugin-data"><%- JSON.stringify(serializedPlugins) %></script>
|
|
453
|
+
<script type="application/json" id="plugin-app-data"><%- JSON.stringify(apps) %></script>
|
|
454
|
+
<script src="/popper.min.js"></script>
|
|
455
|
+
<script src="/tippy-bundle.umd.min.js"></script>
|
|
456
|
+
<script src="/hotkeys.min.js"></script>
|
|
457
|
+
<script src="/sweetalert2.js"></script>
|
|
458
|
+
<script src="/noty.js"></script>
|
|
459
|
+
<script src="/notyq.js"></script>
|
|
460
|
+
<script src="/Socket.js"></script>
|
|
461
|
+
<script src="/common.js"></script>
|
|
462
|
+
<script src="/urldropdown.js"></script>
|
|
463
|
+
<script>
|
|
464
|
+
(function() {
|
|
465
|
+
const pluginDataEl = document.getElementById('plugin-data')
|
|
466
|
+
const appDataEl = document.getElementById('plugin-app-data')
|
|
467
|
+
let plugins = []
|
|
468
|
+
let apps = []
|
|
469
|
+
try {
|
|
470
|
+
plugins = pluginDataEl ? JSON.parse(pluginDataEl.textContent) : []
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('Failed to parse plugin data', error)
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
apps = appDataEl ? JSON.parse(appDataEl.textContent) : []
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error('Failed to parse app data', error)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function escapeHtml(value) {
|
|
481
|
+
const div = document.createElement('div')
|
|
482
|
+
div.textContent = value || ''
|
|
483
|
+
return div.innerHTML
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function createPluginModal(appList) {
|
|
487
|
+
const overlay = document.createElement('div')
|
|
488
|
+
overlay.id = 'plugin-modal-overlay'
|
|
489
|
+
overlay.className = 'modal-overlay url-modal-overlay plugin-modal-overlay'
|
|
490
|
+
|
|
491
|
+
overlay.innerHTML = `
|
|
492
|
+
<div class="url-modal-content" role="dialog" aria-modal="true" aria-labelledby="plugin-modal-title" aria-describedby="plugin-modal-description">
|
|
493
|
+
<button type="button" class="url-modal-close" aria-label="Close">×</button>
|
|
494
|
+
<h3 id="plugin-modal-title">Run Plugin</h3>
|
|
495
|
+
<p class="url-modal-description" id="plugin-modal-description">Choose a project to open the terminal from.</p>
|
|
496
|
+
<input type="search" class="url-modal-input" placeholder="Filter projects" autocomplete="off" />
|
|
497
|
+
<div class="url-dropdown plugin-modal-dropdown"></div>
|
|
498
|
+
<div class="url-modal-actions single">
|
|
499
|
+
<button type="button" class="url-modal-button cancel">Close</button>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
`
|
|
503
|
+
|
|
504
|
+
document.body.appendChild(overlay)
|
|
505
|
+
|
|
506
|
+
const content = overlay.querySelector('.url-modal-content')
|
|
507
|
+
const closeButton = overlay.querySelector('.url-modal-close')
|
|
508
|
+
const cancelButton = overlay.querySelector('.url-modal-button.cancel')
|
|
509
|
+
const titleEl = overlay.querySelector('#plugin-modal-title')
|
|
510
|
+
const descriptionEl = overlay.querySelector('#plugin-modal-description')
|
|
511
|
+
const inputEl = overlay.querySelector('.url-modal-input')
|
|
512
|
+
const dropdownEl = overlay.querySelector('.plugin-modal-dropdown')
|
|
513
|
+
|
|
514
|
+
const state = {
|
|
515
|
+
apps: Array.isArray(appList) ? appList : [],
|
|
516
|
+
plugin: null,
|
|
517
|
+
selectedIndex: -1,
|
|
518
|
+
selectedElement: null,
|
|
519
|
+
visibleIndices: []
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function clearSelection() {
|
|
523
|
+
if (state.selectedElement) {
|
|
524
|
+
state.selectedElement.classList.remove('selected')
|
|
525
|
+
}
|
|
526
|
+
state.selectedElement = null
|
|
527
|
+
state.selectedIndex = -1
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function ensureVisible(element) {
|
|
531
|
+
if (!element) return
|
|
532
|
+
element.scrollIntoView({ block: 'nearest' })
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function select(index, element, { autoLaunch = false } = {}) {
|
|
536
|
+
if (state.selectedElement && state.selectedElement !== element) {
|
|
537
|
+
state.selectedElement.classList.remove('selected')
|
|
538
|
+
}
|
|
539
|
+
state.selectedElement = element || null
|
|
540
|
+
state.selectedIndex = typeof index === 'number' ? index : -1
|
|
541
|
+
if (state.selectedElement) {
|
|
542
|
+
state.selectedElement.classList.add('selected')
|
|
543
|
+
if (autoLaunch) {
|
|
544
|
+
confirm()
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function buildOption(app, index) {
|
|
550
|
+
const option = document.createElement('div')
|
|
551
|
+
option.className = 'url-dropdown-item plugin-option'
|
|
552
|
+
option.setAttribute('data-app-index', index)
|
|
553
|
+
option.tabIndex = 0
|
|
554
|
+
const iconHtml = app.icon
|
|
555
|
+
? `<img class="option-icon" src="${escapeHtml(app.icon)}" alt="${escapeHtml(app.title || app.name || 'App')} icon">`
|
|
556
|
+
: '<div class="option-icon"><i class="fa-solid fa-folder"></i></div>'
|
|
557
|
+
const description = app.description ? `<div class="option-description">${escapeHtml(app.description)}</div>` : ''
|
|
558
|
+
const pathLabel = app.displayPath || app.cwd || ''
|
|
559
|
+
option.innerHTML = `
|
|
560
|
+
${iconHtml}
|
|
561
|
+
<div class="option-body">
|
|
562
|
+
<div class="option-name">${escapeHtml(app.title || app.name || 'Project')}</div>
|
|
563
|
+
<div class="option-path">${escapeHtml(pathLabel)}</div>
|
|
564
|
+
${description}
|
|
565
|
+
</div>
|
|
566
|
+
`
|
|
567
|
+
return option
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function renderList(query) {
|
|
571
|
+
const term = (query || '').toLowerCase().trim()
|
|
572
|
+
dropdownEl.innerHTML = ''
|
|
573
|
+
state.visibleIndices = []
|
|
574
|
+
clearSelection()
|
|
575
|
+
|
|
576
|
+
const matches = []
|
|
577
|
+
for (let i = 0; i < state.apps.length; i++) {
|
|
578
|
+
const app = state.apps[i]
|
|
579
|
+
const searchable = [app.title, app.name, app.description, app.displayPath, app.cwd]
|
|
580
|
+
.filter(Boolean)
|
|
581
|
+
.join(' ')
|
|
582
|
+
.toLowerCase()
|
|
583
|
+
if (!term || searchable.includes(term)) {
|
|
584
|
+
matches.push({ app, index: i })
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (matches.length === 0) {
|
|
589
|
+
dropdownEl.innerHTML = '<div class="url-dropdown-empty">No projects found</div>'
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const fragment = document.createDocumentFragment()
|
|
594
|
+
matches.forEach(({ app, index }) => {
|
|
595
|
+
const option = buildOption(app, index)
|
|
596
|
+
state.visibleIndices.push(index)
|
|
597
|
+
option.addEventListener('click', () => {
|
|
598
|
+
select(index, option, { autoLaunch: true })
|
|
599
|
+
})
|
|
600
|
+
option.addEventListener('keydown', (event) => {
|
|
601
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
602
|
+
event.preventDefault()
|
|
603
|
+
select(index, option, { autoLaunch: true })
|
|
604
|
+
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
605
|
+
event.preventDefault()
|
|
606
|
+
navigate(event.key === 'ArrowDown' ? 1 : -1)
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
fragment.appendChild(option)
|
|
610
|
+
})
|
|
611
|
+
dropdownEl.appendChild(fragment)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function navigate(delta) {
|
|
615
|
+
if (state.visibleIndices.length === 0) return
|
|
616
|
+
const currentPos = state.visibleIndices.indexOf(state.selectedIndex)
|
|
617
|
+
let nextPos
|
|
618
|
+
if (currentPos === -1) {
|
|
619
|
+
nextPos = delta > 0 ? 0 : state.visibleIndices.length - 1
|
|
620
|
+
} else {
|
|
621
|
+
nextPos = currentPos + delta
|
|
622
|
+
if (nextPos < 0) nextPos = 0
|
|
623
|
+
if (nextPos >= state.visibleIndices.length) nextPos = state.visibleIndices.length - 1
|
|
624
|
+
}
|
|
625
|
+
const nextIndex = state.visibleIndices[nextPos]
|
|
626
|
+
const element = dropdownEl.querySelector(`[data-app-index="${nextIndex}"]`)
|
|
627
|
+
if (element) {
|
|
628
|
+
select(nextIndex, element, { autoLaunch: false })
|
|
629
|
+
element.focus()
|
|
630
|
+
ensureVisible(element)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function closeModal() {
|
|
635
|
+
overlay.classList.remove('is-visible')
|
|
636
|
+
document.removeEventListener('keydown', handleKeydown, true)
|
|
637
|
+
setTimeout(() => {
|
|
638
|
+
dropdownEl.innerHTML = ''
|
|
639
|
+
inputEl.value = ''
|
|
640
|
+
clearSelection()
|
|
641
|
+
dropdownEl.style.display = 'none'
|
|
642
|
+
}, 150)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function confirm() {
|
|
646
|
+
if (state.selectedIndex === -1) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
const plugin = state.plugin
|
|
650
|
+
const app = state.apps[state.selectedIndex]
|
|
651
|
+
if (!plugin || !plugin.pluginPath) {
|
|
652
|
+
closeModal()
|
|
653
|
+
alert('This plugin is missing a launch target.')
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
if (!app) {
|
|
657
|
+
closeModal()
|
|
658
|
+
alert('Select a project to continue.')
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
const queryPairs = []
|
|
662
|
+
const pushPair = (key, value, { rawValue = false } = {}) => {
|
|
663
|
+
if (value === undefined || value === null) {
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
const encodedKey = encodeURIComponent(key)
|
|
667
|
+
const encodedValue = rawValue ? String(value) : encodeURIComponent(String(value))
|
|
668
|
+
queryPairs.push(`${encodedKey}=${encodedValue}`)
|
|
669
|
+
}
|
|
670
|
+
pushPair('plugin', plugin.pluginPath, { rawValue: true })
|
|
671
|
+
if (Array.isArray(plugin.extraParams)) {
|
|
672
|
+
plugin.extraParams.forEach(([key, value]) => {
|
|
673
|
+
pushPair(key, value)
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
const queryString = queryPairs.join('&')
|
|
677
|
+
const target = queryPairs.length > 0 ? `/p/${app.name}/dev?${queryString}` : `/p/${app.name}/dev`
|
|
678
|
+
overlay.classList.remove('is-visible')
|
|
679
|
+
document.removeEventListener('keydown', handleKeydown, true)
|
|
680
|
+
location.href = target
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function handleKeydown(event) {
|
|
684
|
+
if (!overlay.classList.contains('is-visible')) return
|
|
685
|
+
if (event.key === 'Escape') {
|
|
686
|
+
event.preventDefault()
|
|
687
|
+
closeModal()
|
|
688
|
+
} else if (event.key === 'ArrowDown') {
|
|
689
|
+
event.preventDefault()
|
|
690
|
+
navigate(1)
|
|
691
|
+
} else if (event.key === 'ArrowUp') {
|
|
692
|
+
event.preventDefault()
|
|
693
|
+
navigate(-1)
|
|
694
|
+
} else if (event.key === 'Enter' && document.activeElement === inputEl) {
|
|
695
|
+
event.preventDefault()
|
|
696
|
+
if (state.visibleIndices.length === 1 && state.selectedIndex === -1) {
|
|
697
|
+
const nextIndex = state.visibleIndices[0]
|
|
698
|
+
const element = dropdownEl.querySelector(`[data-app-index="${nextIndex}"]`)
|
|
699
|
+
select(nextIndex, element, { autoLaunch: true })
|
|
700
|
+
} else if (state.selectedIndex !== -1) {
|
|
701
|
+
confirm()
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
closeButton.addEventListener('click', (event) => {
|
|
707
|
+
event.preventDefault()
|
|
708
|
+
closeModal()
|
|
709
|
+
})
|
|
710
|
+
cancelButton.addEventListener('click', (event) => {
|
|
711
|
+
event.preventDefault()
|
|
712
|
+
closeModal()
|
|
713
|
+
})
|
|
714
|
+
overlay.addEventListener('click', (event) => {
|
|
715
|
+
if (event.target === overlay) {
|
|
716
|
+
closeModal()
|
|
717
|
+
}
|
|
718
|
+
})
|
|
719
|
+
inputEl.addEventListener('input', (event) => {
|
|
720
|
+
renderList(event.target.value)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
function openModal(plugin) {
|
|
724
|
+
state.plugin = plugin
|
|
725
|
+
const title = plugin && plugin.title ? `Run ${plugin.title}` : 'Run Plugin'
|
|
726
|
+
titleEl.textContent = title
|
|
727
|
+
if (state.apps.length === 0) {
|
|
728
|
+
descriptionEl.textContent = 'No projects found under ~/pinokio/api. Create or download a project to continue.'
|
|
729
|
+
} else {
|
|
730
|
+
descriptionEl.textContent = 'Choose a project to open the terminal from.'
|
|
731
|
+
}
|
|
732
|
+
renderList('')
|
|
733
|
+
dropdownEl.style.display = 'block'
|
|
734
|
+
overlay.classList.add('is-visible')
|
|
735
|
+
document.addEventListener('keydown', handleKeydown, true)
|
|
736
|
+
requestAnimationFrame(() => {
|
|
737
|
+
inputEl.focus()
|
|
738
|
+
inputEl.select()
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
open: openModal,
|
|
744
|
+
close: closeModal,
|
|
745
|
+
setApps(newApps) {
|
|
746
|
+
state.apps = Array.isArray(newApps) ? newApps : []
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function initUrlFeatures() {
|
|
752
|
+
if (typeof initUrlDropdown === 'function') {
|
|
753
|
+
initUrlDropdown({ clearBehavior: 'empty' })
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const modal = createPluginModal(apps)
|
|
758
|
+
|
|
759
|
+
function attachHandlers() {
|
|
760
|
+
const cards = document.querySelectorAll('.plugin-card')
|
|
761
|
+
if (!cards.length) return
|
|
762
|
+
cards.forEach((card) => {
|
|
763
|
+
const indexAttr = card.getAttribute('data-plugin-index')
|
|
764
|
+
const index = parseInt(indexAttr, 10)
|
|
765
|
+
if (Number.isNaN(index) || !plugins[index]) {
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
const plugin = plugins[index]
|
|
769
|
+
const infoButton = card.querySelector('.plugin-info')
|
|
770
|
+
if (infoButton) {
|
|
771
|
+
infoButton.addEventListener('click', (event) => {
|
|
772
|
+
event.preventDefault()
|
|
773
|
+
event.stopPropagation()
|
|
774
|
+
const link = infoButton.getAttribute('data-link')
|
|
775
|
+
if (link) {
|
|
776
|
+
window.open(link, '_blank', 'noopener,noreferrer')
|
|
777
|
+
}
|
|
778
|
+
})
|
|
779
|
+
}
|
|
780
|
+
const open = (event) => {
|
|
781
|
+
if (event) {
|
|
782
|
+
event.preventDefault()
|
|
783
|
+
}
|
|
784
|
+
if (!apps.length) {
|
|
785
|
+
alert('No projects found under ~/pinokio/api.')
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
modal.open(plugin)
|
|
789
|
+
}
|
|
790
|
+
card.addEventListener('click', open)
|
|
791
|
+
card.addEventListener('keydown', (event) => {
|
|
792
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
793
|
+
open(event)
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
})
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
800
|
+
initUrlFeatures()
|
|
801
|
+
attachHandlers()
|
|
802
|
+
})
|
|
803
|
+
})()
|
|
804
|
+
</script>
|
|
805
|
+
<script src="/opener.js"></script>
|
|
806
|
+
</body>
|
|
807
|
+
</html>
|
package/server/views/tools.ejs
CHANGED
|
@@ -1269,6 +1269,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1269
1269
|
<a class='tab' id='genlog'><i class="fa-solid fa-laptop-code"></i><div class='caption'>Logs</div></a>
|
|
1270
1270
|
<a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><i class="fa-solid fa-download"></i><div class='caption'>Download logs</div></a>
|
|
1271
1271
|
<a class='tab' href="/screenshots"><i class="fa-solid fa-camera"></i><div class='caption'>Screenshots</div></a>
|
|
1272
|
+
<a class='tab' href="/terminals"><i class="fa-solid fa-desktop"></i><div class='caption'>Terminals</div></a>
|
|
1272
1273
|
<a class='tab selected' href="/tools"><i class="fa-solid fa-toolbox"></i><div class='caption'>Installed Tools</div></a>
|
|
1273
1274
|
<a class='tab' href="/home?mode=settings"><i class="fa-solid fa-gear"></i><div class='caption'>Settings</div></a>
|
|
1274
1275
|
</aside>
|