@starpak/os 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +170 -0
- package/lib/index.js.map +1 -0
- package/package.json +11 -3
- package/ui/index.html +40 -0
- package/ui/os.css +219 -0
- package/ui/os.js +311 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Spak Team
|
|
4
|
+
Copyright (c) 2019-present Shigma
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface OsOptions {
|
|
2
|
+
/** 工程根目录(默认当前目录)。 */
|
|
3
|
+
root?: string;
|
|
4
|
+
/** 静态 UI 目录(默认:包内 ui/)。 */
|
|
5
|
+
uiDir?: string;
|
|
6
|
+
/** 监听端口(默认 8008)。 */
|
|
7
|
+
port?: number;
|
|
8
|
+
/** 监听地址(默认 127.0.0.1)。 */
|
|
9
|
+
host?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface OsHandle {
|
|
12
|
+
port: number;
|
|
13
|
+
host: string;
|
|
14
|
+
url: string;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function startOs(options?: OsOptions): Promise<OsHandle>;
|
|
18
|
+
/** 插件入口:宿主提供 router 时挂载 API(UI 由 startOs 提供)。 */
|
|
19
|
+
export declare function apply(ctx: {
|
|
20
|
+
router?: any;
|
|
21
|
+
logger?: any;
|
|
22
|
+
}, config?: OsOptions): void;
|
|
23
|
+
/** CLI:阻塞运行直到 SIGINT/SIGTERM。 */
|
|
24
|
+
export declare function runOsCli(options?: {
|
|
25
|
+
root?: string;
|
|
26
|
+
port?: number;
|
|
27
|
+
host?: string;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,SAAS;IACxB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAmDD,wBAAsB,OAAO,CAAC,OAAO,GAAE,SAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkExE;AAED,iDAAiD;AACjD,wBAAgB,KAAK,CAAC,GAAG,EAAE;IAAE,MAAM,CAAC,EAAE,GAAG,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,CAevF;AAED,iCAAiC;AACjC,wBAAsB,QAAQ,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3G"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ===== spm os — Spak OS 桌面壳(默认 8008)=====
|
|
3
|
+
//
|
|
4
|
+
// 提供 apps/spak-os 的静态页面与最小 API:
|
|
5
|
+
// GET / 桌面
|
|
6
|
+
// GET /api/apps 已安装应用(扫描 apps/*/spak.app.json)
|
|
7
|
+
//
|
|
8
|
+
// 端口固定默认 8008;监听地址默认 127.0.0.1,可用 --host 0.0.0.0 对外。
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.startOs = startOs;
|
|
11
|
+
exports.apply = apply;
|
|
12
|
+
exports.runOsCli = runOsCli;
|
|
13
|
+
const node_http_1 = require("node:http");
|
|
14
|
+
const promises_1 = require("node:fs/promises");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const MIME = {
|
|
17
|
+
'.html': 'text/html; charset=utf-8',
|
|
18
|
+
'.css': 'text/css; charset=utf-8',
|
|
19
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
20
|
+
'.json': 'application/json; charset=utf-8',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.png': 'image/png',
|
|
23
|
+
'.ico': 'image/x-icon',
|
|
24
|
+
};
|
|
25
|
+
async function listApps(root) {
|
|
26
|
+
const appsDir = (0, node_path_1.join)(root, 'apps');
|
|
27
|
+
const apps = [];
|
|
28
|
+
let names = [];
|
|
29
|
+
try {
|
|
30
|
+
names = await (0, promises_1.readdir)(appsDir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return apps;
|
|
34
|
+
}
|
|
35
|
+
for (const name of names.sort()) {
|
|
36
|
+
if (name.startsWith('.'))
|
|
37
|
+
continue;
|
|
38
|
+
const manifestPath = (0, node_path_1.join)(appsDir, name, 'spak.app.json');
|
|
39
|
+
try {
|
|
40
|
+
const manifest = JSON.parse(await (0, promises_1.readFile)(manifestPath, 'utf-8'));
|
|
41
|
+
apps.push({ id: manifest.id ?? name, ...manifest });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// 不是应用目录就跳过
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return apps;
|
|
48
|
+
}
|
|
49
|
+
async function listMods(root) {
|
|
50
|
+
const modsDir = (0, node_path_1.join)(root, 'mods');
|
|
51
|
+
const mods = [];
|
|
52
|
+
let names = [];
|
|
53
|
+
try {
|
|
54
|
+
names = await (0, promises_1.readdir)(modsDir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return mods;
|
|
58
|
+
}
|
|
59
|
+
for (const name of names.sort()) {
|
|
60
|
+
if (name.startsWith('.'))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const manifest = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(modsDir, name, 'mod.json'), 'utf-8'));
|
|
64
|
+
mods.push({ id: manifest.id ?? name, ...manifest });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// 不是模组目录就跳过
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return mods;
|
|
71
|
+
}
|
|
72
|
+
function json(response, status, body) {
|
|
73
|
+
const text = JSON.stringify(body);
|
|
74
|
+
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(text) });
|
|
75
|
+
response.end(text);
|
|
76
|
+
}
|
|
77
|
+
async function startOs(options = {}) {
|
|
78
|
+
const root = (0, node_path_1.resolve)(options.root || process.cwd());
|
|
79
|
+
// UI 资源随包发布(packages/os/ui),宿主只提供端口与应用目录
|
|
80
|
+
const uiDir = options.uiDir ? (0, node_path_1.resolve)(options.uiDir) : (0, node_path_1.join)(__dirname, '..', 'ui');
|
|
81
|
+
const port = options.port ?? 8008;
|
|
82
|
+
const host = options.host ?? '127.0.0.1';
|
|
83
|
+
const server = (0, node_http_1.createServer)(async (request, response) => {
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
86
|
+
if (url.pathname === '/api/apps') {
|
|
87
|
+
return json(response, 200, { apps: await listApps(root) });
|
|
88
|
+
}
|
|
89
|
+
if (url.pathname === '/api/mods') {
|
|
90
|
+
return json(response, 200, { mods: await listMods(root) });
|
|
91
|
+
}
|
|
92
|
+
if (url.pathname === '/api/health') {
|
|
93
|
+
return json(response, 200, { status: 'ok', os: 'spak-os', uptime: Math.round(process.uptime()) });
|
|
94
|
+
}
|
|
95
|
+
// 静态文件(禁止路径穿越)
|
|
96
|
+
const relative = (0, node_path_1.normalize)(decodeURIComponent(url.pathname)).replace(/^([/\\])+/, '');
|
|
97
|
+
const target = (0, node_path_1.resolve)(uiDir, relative || 'index.html');
|
|
98
|
+
if (!target.startsWith(uiDir + node_path_1.sep) && target !== uiDir) {
|
|
99
|
+
return json(response, 403, { error: 'forbidden' });
|
|
100
|
+
}
|
|
101
|
+
let file = target;
|
|
102
|
+
try {
|
|
103
|
+
const info = await (0, promises_1.stat)(file);
|
|
104
|
+
if (info.isDirectory())
|
|
105
|
+
file = (0, node_path_1.join)(file, 'index.html');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
if ((0, node_path_1.extname)(relative))
|
|
109
|
+
return json(response, 404, { error: 'not_found' });
|
|
110
|
+
file = (0, node_path_1.join)(uiDir, 'index.html'); // SPA 回退
|
|
111
|
+
}
|
|
112
|
+
const data = await (0, promises_1.readFile)(file);
|
|
113
|
+
response.writeHead(200, {
|
|
114
|
+
'content-type': MIME[(0, node_path_1.extname)(file)] ?? 'application/octet-stream',
|
|
115
|
+
'content-length': data.length,
|
|
116
|
+
'cache-control': 'no-cache',
|
|
117
|
+
});
|
|
118
|
+
response.end(data);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
json(response, 500, { error: 'internal_error', message: error.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
125
|
+
server.once('error', rejectListen);
|
|
126
|
+
server.listen(port, host, () => {
|
|
127
|
+
server.off('error', rejectListen);
|
|
128
|
+
resolveListen();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
const address = server.address();
|
|
132
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
133
|
+
return {
|
|
134
|
+
port: actualPort,
|
|
135
|
+
host,
|
|
136
|
+
url: `http://${host}:${actualPort}`,
|
|
137
|
+
close: () => new Promise(resolveClose => server.close(() => resolveClose())),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** 插件入口:宿主提供 router 时挂载 API(UI 由 startOs 提供)。 */
|
|
141
|
+
function apply(ctx, config = {}) {
|
|
142
|
+
const root = (0, node_path_1.resolve)(config.root || process.cwd());
|
|
143
|
+
const router = ctx?.router;
|
|
144
|
+
if (!router) {
|
|
145
|
+
ctx?.logger?.info?.('os 插件未挂载 router:请使用 startOs() 直接提供服务');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
router.get('/api/apps', async () => ({
|
|
149
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
150
|
+
body: JSON.stringify({ apps: await listApps(root) }),
|
|
151
|
+
}));
|
|
152
|
+
router.get('/api/mods', async () => ({
|
|
153
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
154
|
+
body: JSON.stringify({ mods: await listMods(root) }),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
/** CLI:阻塞运行直到 SIGINT/SIGTERM。 */
|
|
158
|
+
async function runOsCli(options = {}) {
|
|
159
|
+
const handle = await startOs(options);
|
|
160
|
+
console.log(`\n ◉ Spak OS → ${handle.url}`);
|
|
161
|
+
console.log(` 桌面: apps/spak-os 应用: ${(0, node_path_1.join)(options.root || process.cwd(), 'apps')}`);
|
|
162
|
+
console.log(' Ctrl+C 退出\n');
|
|
163
|
+
await new Promise((resolveStop) => {
|
|
164
|
+
const stop = () => { void handle.close().then(() => resolveStop()); };
|
|
165
|
+
process.once('SIGINT', stop);
|
|
166
|
+
process.once('SIGTERM', stop);
|
|
167
|
+
});
|
|
168
|
+
console.log('Spak OS 已停止');
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starpak/os",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Spak OS - desktop shell plugin (window manager, taskbar, launcher) served on :8008",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"ui"
|
|
10
|
+
],
|
|
11
|
+
"author": "Spak Team",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {}
|
|
6
14
|
}
|
package/ui/index.html
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
|
+
<title>Spak OS</title>
|
|
7
|
+
<link rel="stylesheet" href="./os.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="boot"><div class="logo">Spak OS</div><div class="bar"><i></i></div></div>
|
|
11
|
+
|
|
12
|
+
<main id="desktop" aria-label="桌面">
|
|
13
|
+
<div id="icons"></div>
|
|
14
|
+
<div id="windows"></div>
|
|
15
|
+
</main>
|
|
16
|
+
|
|
17
|
+
<nav id="bar" aria-label="任务栏">
|
|
18
|
+
<button id="start" title="开始菜单" aria-label="开始菜单">
|
|
19
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
20
|
+
|
|
21
|
+
</svg>
|
|
22
|
+
</button>
|
|
23
|
+
<div id="tasks"></div>
|
|
24
|
+
<div id="clock"></div>
|
|
25
|
+
</nav>
|
|
26
|
+
|
|
27
|
+
<div id="launcher" hidden>
|
|
28
|
+
<div class="launcher-box" role="dialog" aria-label="开始菜单">
|
|
29
|
+
<div class="launcher-head">
|
|
30
|
+
<h1>Spak OS</h1>
|
|
31
|
+
<span class="ver">0.1</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div id="apps" class="grid"></div>
|
|
34
|
+
<div class="launcher-foot"><span>Alt+Tab 切换 · Esc 关闭 · Ctrl+K 唤起</span></div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<script src="./os.js"></script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
package/ui/os.css
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
--bg: #0b1020;
|
|
4
|
+
--fg: #e8ecf7;
|
|
5
|
+
--dim: #8b93a7;
|
|
6
|
+
--accent: #ff8a00;
|
|
7
|
+
--accent2: #ffb03a;
|
|
8
|
+
--glass: rgba(22, 27, 45, .72);
|
|
9
|
+
--line: rgba(255, 255, 255, .12);
|
|
10
|
+
--win: #131a2c;
|
|
11
|
+
--radius: 14px;
|
|
12
|
+
--bar-h: 56px;
|
|
13
|
+
}
|
|
14
|
+
[data-theme="light"] {
|
|
15
|
+
--bg: #eef1f8;
|
|
16
|
+
--fg: #131a2c;
|
|
17
|
+
--dim: #5b6478;
|
|
18
|
+
--glass: rgba(255, 255, 255, .78);
|
|
19
|
+
--line: rgba(0, 0, 0, .1);
|
|
20
|
+
--win: #ffffff;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
html { height: 100%; }
|
|
24
|
+
body {
|
|
25
|
+
position: fixed;
|
|
26
|
+
inset: 0;
|
|
27
|
+
height: 100dvh;
|
|
28
|
+
margin: 0;
|
|
29
|
+
}
|
|
30
|
+
body {
|
|
31
|
+
font: 14px/1.5 system-ui, -apple-system, "Noto Sans CJK SC", sans-serif;
|
|
32
|
+
color: var(--fg);
|
|
33
|
+
background:
|
|
34
|
+
radial-gradient(1100px 680px at 12% -12%, #1e3358 0%, transparent 58%),
|
|
35
|
+
radial-gradient(900px 640px at 108% 108%, #4a2158 0%, transparent 55%),
|
|
36
|
+
linear-gradient(160deg, #070b16 0%, #0d1428 45%, #121021 100%);
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
user-select: none;
|
|
39
|
+
-webkit-user-select: none;
|
|
40
|
+
touch-action: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* boot */
|
|
44
|
+
#boot {
|
|
45
|
+
position: fixed; inset: 0; z-index: 90;
|
|
46
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
|
|
47
|
+
background: var(--bg);
|
|
48
|
+
transition: opacity .5s ease, visibility .5s;
|
|
49
|
+
}
|
|
50
|
+
#boot.done { display: none; }
|
|
51
|
+
#boot .logo {
|
|
52
|
+
font-size: clamp(26px, 8vw, 34px); letter-spacing: .04em; font-weight: 700;
|
|
53
|
+
background: linear-gradient(100deg, var(--accent2), var(--accent));
|
|
54
|
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
55
|
+
}
|
|
56
|
+
#boot .bar { width: 180px; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
|
|
57
|
+
#boot .bar i { display: block; height: 100%; width: 40%; border-radius: 2px; background: var(--accent); animation: boot 1.1s ease-in-out infinite; }
|
|
58
|
+
@keyframes boot { 0% { transform: translateX(-120%); } 100% { transform: translateX(320%); } }
|
|
59
|
+
|
|
60
|
+
/* desktop */
|
|
61
|
+
#desktop { position: absolute; inset: 0 0 var(--bar-h) 0; overflow: hidden; }
|
|
62
|
+
#icons {
|
|
63
|
+
position: absolute; inset: 16px auto auto 16px;
|
|
64
|
+
display: grid; gap: 10px; width: 96px;
|
|
65
|
+
}
|
|
66
|
+
#desktop::before {
|
|
67
|
+
content: "";
|
|
68
|
+
position: absolute; inset: -20%;
|
|
69
|
+
background:
|
|
70
|
+
radial-gradient(42% 34% at 24% 28%, rgba(255, 138, 0, .12), transparent 70%),
|
|
71
|
+
radial-gradient(38% 30% at 76% 66%, rgba(122, 92, 255, .14), transparent 70%);
|
|
72
|
+
filter: blur(48px);
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
.icon {
|
|
76
|
+
display: grid; justify-items: center; gap: 7px;
|
|
77
|
+
width: 86px; padding: 8px 6px; border-radius: 14px; cursor: pointer;
|
|
78
|
+
color: var(--fg); background: transparent; border: 0; font: inherit;
|
|
79
|
+
}
|
|
80
|
+
.icon:hover, .icon:focus-visible { background: rgba(255, 255, 255, .08); outline: none; }
|
|
81
|
+
.icon .glyph {
|
|
82
|
+
width: 54px; height: 54px; display: grid; place-items: center;
|
|
83
|
+
border-radius: 16px; color: #ffcf87;
|
|
84
|
+
background: linear-gradient(160deg, rgba(255, 255, 255, .14), rgba(255, 255, 255, .05));
|
|
85
|
+
border: 1px solid rgba(255, 255, 255, .20);
|
|
86
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), 0 8px 22px rgba(0, 0, 0, .40);
|
|
87
|
+
}
|
|
88
|
+
.icon span { color: #c8d0e2; }
|
|
89
|
+
.icon .glyph svg, .tile .glyph svg { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; }
|
|
90
|
+
.icon span { font-size: 12px; color: var(--dim); }
|
|
91
|
+
|
|
92
|
+
/* windows */
|
|
93
|
+
.win {
|
|
94
|
+
position: absolute; min-width: 280px; min-height: 160px;
|
|
95
|
+
background: var(--win); border: 1px solid var(--line);
|
|
96
|
+
border-radius: var(--radius); box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
|
|
97
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
98
|
+
animation: pop .16s ease-out;
|
|
99
|
+
}
|
|
100
|
+
@keyframes pop { from { transform: scale(.97); opacity: 0; } }
|
|
101
|
+
.win:focus-within { border-color: rgba(255, 138, 0, .5); }
|
|
102
|
+
.win header {
|
|
103
|
+
display: flex; align-items: center; gap: 8px;
|
|
104
|
+
padding: 10px 12px; cursor: grab; touch-action: none;
|
|
105
|
+
background: rgba(255, 255, 255, .04); border-bottom: 1px solid var(--line);
|
|
106
|
+
}
|
|
107
|
+
.win header:active { cursor: grabbing; }
|
|
108
|
+
.win header b { font-weight: 600; font-size: 13px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
109
|
+
.win header .spacer { flex: 1; }
|
|
110
|
+
.win header button {
|
|
111
|
+
width: 34px; height: 34px; border-radius: 9px; border: 0;
|
|
112
|
+
background: transparent; color: var(--dim); font-size: 16px; cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
.win header button:hover { background: rgba(255, 255, 255, .1); color: var(--fg); }
|
|
115
|
+
.win .body { flex: 1; overflow: auto; padding: 14px; -webkit-overflow-scrolling: touch; }
|
|
116
|
+
.win .body p { margin: 0 0 8px; color: var(--dim); }
|
|
117
|
+
.win .body code { color: var(--accent2); }
|
|
118
|
+
|
|
119
|
+
/* terminal */
|
|
120
|
+
.term { font: 13px/1.6 ui-monospace, "SF Mono", Menlo, monospace; background: #080b14; color: #cfe3ff; }
|
|
121
|
+
.term .body { padding: 12px; }
|
|
122
|
+
.term .out { white-space: pre-wrap; min-height: 120px; }
|
|
123
|
+
.term .row { display: flex; gap: 8px; align-items: center; border-top: 1px solid var(--line); padding-top: 8px; margin-top: 8px; }
|
|
124
|
+
.term .row { border-top: 0; }
|
|
125
|
+
|
|
126
|
+
/* files */
|
|
127
|
+
.files .row { display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 10px; }
|
|
128
|
+
.files .row:hover { background: rgba(255, 255, 255, .06); }
|
|
129
|
+
.files .row .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
|
|
130
|
+
|
|
131
|
+
.term .term-line { flex: 1; min-height: 24px; color: #cfe3ff; white-space: pre-wrap; word-break: break-all; }
|
|
132
|
+
.term .term-capture {
|
|
133
|
+
position: absolute; opacity: 0; width: 1px; height: 1px;
|
|
134
|
+
border: 0; padding: 0; background: transparent; pointer-events: none;
|
|
135
|
+
}
|
|
136
|
+
.term { position: relative; }
|
|
137
|
+
|
|
138
|
+
/* taskbar */
|
|
139
|
+
#bar {
|
|
140
|
+
position: fixed; left: 0; right: 0; bottom: 0; height: var(--bar-h);
|
|
141
|
+
display: flex; align-items: center; gap: 8px; padding: 0 10px;
|
|
142
|
+
background: var(--glass); backdrop-filter: blur(18px);
|
|
143
|
+
border-top: 1px solid var(--line);
|
|
144
|
+
}
|
|
145
|
+
#bar button {
|
|
146
|
+
height: 40px; min-width: 44px; padding: 0 12px; border-radius: 10px;
|
|
147
|
+
border: 1px solid transparent; background: transparent; color: var(--fg);
|
|
148
|
+
font: inherit; cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
#start { display: grid; place-items: center; color: var(--accent2); }
|
|
151
|
+
#start svg { width: 24px; height: 24px; fill: currentColor; }
|
|
152
|
+
#bar button:hover, #bar button[aria-pressed="true"] { background: rgba(255, 255, 255, .1); }
|
|
153
|
+
#tasks { display: flex; gap: 6px; flex: 1; overflow: hidden; }
|
|
154
|
+
#tasks .task {
|
|
155
|
+
max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
156
|
+
border-radius: 9px; padding: 8px 12px; font-size: 12px; color: var(--dim);
|
|
157
|
+
background: rgba(255, 255, 255, .05); border: 1px solid transparent; cursor: pointer;
|
|
158
|
+
}
|
|
159
|
+
#tasks .task.active { color: var(--fg); border-color: var(--line); }
|
|
160
|
+
#clock { font-size: 12px; color: var(--dim); padding-right: 6px; text-align: right; }
|
|
161
|
+
|
|
162
|
+
/* launcher */
|
|
163
|
+
#launcher {
|
|
164
|
+
position: fixed; inset: 0; z-index: 60;
|
|
165
|
+
background: rgba(4, 6, 14, .35);
|
|
166
|
+
}
|
|
167
|
+
#launcher[hidden] { display: none; }
|
|
168
|
+
.launcher-box {
|
|
169
|
+
position: absolute; left: 8px; bottom: calc(var(--bar-h) + 8px);
|
|
170
|
+
width: min(380px, calc(100vw - 16px)); max-height: 70dvh; overflow: auto;
|
|
171
|
+
padding: 16px;
|
|
172
|
+
background: var(--glass); backdrop-filter: blur(26px) saturate(1.25);
|
|
173
|
+
border: 1px solid var(--line); border-radius: 18px;
|
|
174
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, .5), inset 0 1px 0 rgba(255, 255, 255, .08);
|
|
175
|
+
}
|
|
176
|
+
.launcher-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
|
|
177
|
+
.launcher-head h1 { margin: 0; font-size: 17px; letter-spacing: .02em; }
|
|
178
|
+
.launcher-head .ver { color: var(--dim); font-size: 11px; }
|
|
179
|
+
.launcher-box .search {
|
|
180
|
+
width: 100%; margin: 0 0 12px; padding: 9px 12px;
|
|
181
|
+
border-radius: 10px; border: 1px solid var(--line); outline: none;
|
|
182
|
+
background: rgba(255, 255, 255, .06); color: var(--fg); font: inherit;
|
|
183
|
+
}
|
|
184
|
+
.launcher-box .search::placeholder { color: var(--dim); }
|
|
185
|
+
.launcher-foot { margin-top: 12px; color: var(--dim); font-size: 11px; }
|
|
186
|
+
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
|
187
|
+
.tile {
|
|
188
|
+
display: grid; gap: 6px; justify-items: center; padding: 12px 6px;
|
|
189
|
+
border-radius: 12px; border: 1px solid transparent; background: transparent;
|
|
190
|
+
color: var(--fg); font: inherit; cursor: pointer; transition: background .15s, border-color .15s;
|
|
191
|
+
}
|
|
192
|
+
.tile:hover { border-color: rgba(255, 138, 0, .5); }
|
|
193
|
+
.tile .glyph { font-size: 22px; }
|
|
194
|
+
|
|
195
|
+
/* touch: 放大热区 */
|
|
196
|
+
@media (pointer: coarse) {
|
|
197
|
+
.win header { padding: 8px 8px; gap: 6px; }
|
|
198
|
+
:root { --bar-h: 64px; }
|
|
199
|
+
.icon { padding: 10px 8px; gap: 9px; }
|
|
200
|
+
.icon .glyph { width: 58px; height: 58px; }
|
|
201
|
+
#bar button { height: 48px; min-width: 52px; }
|
|
202
|
+
.win header button { width: 42px; height: 42px; }
|
|
203
|
+
#desktop { inset: 0 0 64px 0; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* 手机:窗口铺满可视区,保证标题栏按钮不被裁掉 */
|
|
207
|
+
#windows { position: absolute; inset: 0; pointer-events: none; }
|
|
208
|
+
.win { pointer-events: auto; }
|
|
209
|
+
|
|
210
|
+
@media (max-width: 600px) {
|
|
211
|
+
.win {
|
|
212
|
+
left: 8px !important;
|
|
213
|
+
top: 8px !important;
|
|
214
|
+
width: calc(100vw - 16px) !important;
|
|
215
|
+
height: calc(100dvh - var(--bar-h) - 16px) !important;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* 终端输入:彻底去掉 UA 边框/背景 */
|
package/ui/os.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/* Spak OS — 桌面壳(窗口 / 任务栏 / 启动器 / 三端输入) */
|
|
2
|
+
(() => {
|
|
3
|
+
const $ = (sel, root = document) => root.querySelector(sel)
|
|
4
|
+
const desktop = $('#desktop')
|
|
5
|
+
const layer = $('#windows')
|
|
6
|
+
const iconBox = $('#icons')
|
|
7
|
+
const tasks = $('#tasks')
|
|
8
|
+
const launcher = $('#launcher')
|
|
9
|
+
const clock = $('#clock')
|
|
10
|
+
|
|
11
|
+
const state = { z: 10, open: new Map(), seq: 0, focused: null, installed: [], mods: [] }
|
|
12
|
+
|
|
13
|
+
const svg = (body) => `<svg viewBox="0 0 24 24" aria-hidden="true">${body}</svg>`
|
|
14
|
+
const APPS = {
|
|
15
|
+
files: { title: '文件', glyph: svg('<path d="M3 6h6l2 2h10v10H3z"/>'), mount: mountFiles },
|
|
16
|
+
terminal: { title: '终端', glyph: svg('<rect x="3" y="4" width="18" height="16" rx="2"/><path d="m7 9 3 3-3 3M13 15h4"/>'), mount: mountTerminal },
|
|
17
|
+
settings: { title: '设置', glyph: svg('<circle cx="12" cy="12" r="3"/><path d="M12 3v2m0 14v2M3 12h2m14 0h2M5.6 5.6l1.4 1.4m10 10 1.4 1.4m0-12.8-1.4 1.4m-10 10-1.4 1.4"/>'), mount: mountSettings },
|
|
18
|
+
mods: { title: '模组', glyph: svg('<path d="M4 7h7v7H4zM13 4h7v7h-7zM13 13h7v7h-7zM4 15h7v5H4z"/>'), mount: mountMods },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ---------- 窗口 ---------- */
|
|
22
|
+
function openApp(id, payload) {
|
|
23
|
+
const app = APPS[id]
|
|
24
|
+
if (!app) return toast(`没有应用:${id}`)
|
|
25
|
+
const key = payload ? `${id}:${payload}` : id
|
|
26
|
+
if (state.open.has(key)) return focus(state.open.get(key).el)
|
|
27
|
+
|
|
28
|
+
const el = document.createElement('section')
|
|
29
|
+
el.className = `win ${id}${id === 'terminal' ? ' term' : ''}`
|
|
30
|
+
el.tabIndex = 0
|
|
31
|
+
el.innerHTML = `
|
|
32
|
+
<header><b>${app.title}</b><span class="spacer"></span>
|
|
33
|
+
<button data-act="min" title="最小化">–</button>
|
|
34
|
+
<button data-act="close" title="关闭">×</button>
|
|
35
|
+
</header>
|
|
36
|
+
<div class="body"></div>`
|
|
37
|
+
const body = $('.body', el)
|
|
38
|
+
|
|
39
|
+
const narrow = desktop.clientWidth < 600
|
|
40
|
+
const width = narrow ? desktop.clientWidth - 16 : Math.min(560, Math.max(300, desktop.clientWidth - 60))
|
|
41
|
+
const height = narrow ? desktop.clientHeight - 16 : Math.min(380, Math.max(180, desktop.clientHeight - 80))
|
|
42
|
+
const n = state.seq++
|
|
43
|
+
el.style.width = width + 'px'
|
|
44
|
+
el.style.height = height + 'px'
|
|
45
|
+
el.style.left = narrow ? '8px' : Math.min(150 + n * 26, Math.max(10, desktop.clientWidth - width - 10)) + 'px'
|
|
46
|
+
el.style.top = narrow ? '8px' : Math.min(40 + n * 22, Math.max(10, desktop.clientHeight - height - 10)) + 'px'
|
|
47
|
+
|
|
48
|
+
layer.appendChild(el)
|
|
49
|
+
const rec = { el, id, key, body, min: false }
|
|
50
|
+
state.open.set(key, rec)
|
|
51
|
+
bindDrag(el, $('header', el))
|
|
52
|
+
bindTask(rec)
|
|
53
|
+
app.mount(body, payload, el)
|
|
54
|
+
focus(el)
|
|
55
|
+
el.addEventListener('pointerdown', () => focus(el))
|
|
56
|
+
$('[data-act="close"]', el).onclick = () => close(rec)
|
|
57
|
+
$('[data-act="min"]', el).onclick = () => minimize(rec)
|
|
58
|
+
return rec
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function focus(el) {
|
|
62
|
+
state.focused = el
|
|
63
|
+
el.style.zIndex = ++state.z
|
|
64
|
+
for (const rec of state.open.values()) rec.el.classList.toggle('focused', rec.el === el)
|
|
65
|
+
renderTasks()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function close(rec) {
|
|
69
|
+
rec.el.remove()
|
|
70
|
+
state.open.delete(rec.key)
|
|
71
|
+
rec.task?.remove()
|
|
72
|
+
renderTasks()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function minimize(rec) {
|
|
76
|
+
rec.min = !rec.min
|
|
77
|
+
rec.el.hidden = rec.min
|
|
78
|
+
renderTasks()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function bindTask(rec) {
|
|
82
|
+
const task = document.createElement('button')
|
|
83
|
+
task.className = 'task'
|
|
84
|
+
task.textContent = APPS[rec.id].title
|
|
85
|
+
task.onclick = () => (rec.min || rec.el.hidden ? (rec.min = false, rec.el.hidden = false, focus(rec.el)) : minimize(rec))
|
|
86
|
+
tasks.appendChild(task)
|
|
87
|
+
rec.task = task
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderTasks() {
|
|
91
|
+
for (const rec of state.open.values()) {
|
|
92
|
+
rec.task?.classList.toggle('active', rec.el === state.focused && !rec.el.hidden)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function bindDrag(el, handle) {
|
|
97
|
+
let dx = 0, dy = 0, dragging = false
|
|
98
|
+
handle.addEventListener('pointerdown', (event) => {
|
|
99
|
+
if (event.target.closest('button')) return
|
|
100
|
+
dragging = true
|
|
101
|
+
const rect = el.getBoundingClientRect()
|
|
102
|
+
dx = event.clientX - rect.left
|
|
103
|
+
dy = event.clientY - rect.top
|
|
104
|
+
handle.setPointerCapture(event.pointerId)
|
|
105
|
+
el.style.transition = 'none'
|
|
106
|
+
})
|
|
107
|
+
handle.addEventListener('pointermove', (event) => {
|
|
108
|
+
if (!dragging) return
|
|
109
|
+
const maxX = desktop.clientWidth - el.offsetWidth
|
|
110
|
+
const maxY = desktop.clientHeight - el.offsetHeight
|
|
111
|
+
el.style.left = Math.max(0, Math.min(maxX, event.clientX - dx)) + 'px'
|
|
112
|
+
el.style.top = Math.max(0, Math.min(maxY, event.clientY - dy)) + 'px'
|
|
113
|
+
})
|
|
114
|
+
const stop = (event) => {
|
|
115
|
+
if (!dragging) return
|
|
116
|
+
dragging = false
|
|
117
|
+
try { handle.releasePointerCapture(event.pointerId) } catch {}
|
|
118
|
+
}
|
|
119
|
+
handle.addEventListener('pointerup', stop)
|
|
120
|
+
handle.addEventListener('pointercancel', stop)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ---------- 应用实现 ---------- */
|
|
124
|
+
function mountFiles(body) {
|
|
125
|
+
body.innerHTML = '<p>已安装应用(来自 apps/)</p><div class="list"></div>'
|
|
126
|
+
const list = $('.list', body)
|
|
127
|
+
const rows = [...state.installed.map(app => ({ name: app.name || app.id, note: app.description || '已安装应用', id: app.id }))]
|
|
128
|
+
if (!rows.length) list.innerHTML = '<p>还没有安装应用喵</p>'
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
const el = document.createElement('div')
|
|
131
|
+
el.className = 'row'
|
|
132
|
+
el.innerHTML = `<span class="dot"></span><div><b>${row.name}</b><br><small>${row.note}</small></div>`
|
|
133
|
+
list.appendChild(el)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function mountTerminal(body) {
|
|
138
|
+
body.innerHTML = `
|
|
139
|
+
<div class="out">Spak OS shell · 输入 help 查看命令</div>
|
|
140
|
+
<div class="row"><span>❯</span><span class="term-line" aria-hidden="true"></span></div>
|
|
141
|
+
<input class="term-capture" autocomplete="off" spellcheck="false" aria-label="终端输入">`
|
|
142
|
+
const out = $('.out', body)
|
|
143
|
+
const capture = $('.term-capture', body)
|
|
144
|
+
const display = $('.term-line', body)
|
|
145
|
+
const print = (text) => { out.textContent += `\n${text}`; out.scrollTop = out.scrollHeight }
|
|
146
|
+
|
|
147
|
+
body.addEventListener('pointerdown', () => capture.focus())
|
|
148
|
+
capture.addEventListener('input', () => { display.textContent = capture.value })
|
|
149
|
+
capture.addEventListener('keydown', (event) => {
|
|
150
|
+
if (event.key !== 'Enter') return
|
|
151
|
+
event.preventDefault()
|
|
152
|
+
const line = capture.value.trim()
|
|
153
|
+
capture.value = ''
|
|
154
|
+
display.textContent = ''
|
|
155
|
+
if (!line) return
|
|
156
|
+
print(`❯ ${line}`)
|
|
157
|
+
const [cmd, ...rest] = line.split(/\s+/)
|
|
158
|
+
if (cmd === 'help') print('help · date · ls · open <app> · theme · clear')
|
|
159
|
+
else if (cmd === 'date') print(new Date().toLocaleString())
|
|
160
|
+
else if (cmd === 'ls') print(Object.keys(APPS).join(' '))
|
|
161
|
+
else if (cmd === 'open') rest[0] ? openApp(rest[0]) : print('用法:open <files|terminal|settings>')
|
|
162
|
+
else if (cmd === 'theme') toggleTheme()
|
|
163
|
+
else if (cmd === 'clear') out.textContent = ''
|
|
164
|
+
else print(`未知命令:${cmd}`)
|
|
165
|
+
})
|
|
166
|
+
setTimeout(() => capture.focus(), 50)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mountMods(body) {
|
|
170
|
+
body.innerHTML = '<p>已安装模组(来自 mods/)</p><div class="list"></div>'
|
|
171
|
+
const list = $('.list', body)
|
|
172
|
+
if (!state.mods.length) { list.innerHTML = '<p>还没有装模组喵</p>'; return }
|
|
173
|
+
for (const mod of state.mods) {
|
|
174
|
+
const perms = mod.capabilities || mod.permissions || {}
|
|
175
|
+
const summary = Object.entries(perms).map(([key, value]) => `${key}=${value}`).join(' · ') || '无额外权限'
|
|
176
|
+
const row = document.createElement('div')
|
|
177
|
+
row.className = 'row'
|
|
178
|
+
row.innerHTML = `<span class="dot"></span><div><b>${mod.name || mod.id}</b> <small>v${mod.version || '0.0.0'}</small><br><small>${summary}</small></div>`
|
|
179
|
+
list.appendChild(row)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mountSettings(body) {
|
|
184
|
+
const theme = localStorage.getItem('spak-theme') || 'dark'
|
|
185
|
+
body.innerHTML = `
|
|
186
|
+
<p>外观</p>
|
|
187
|
+
<div class="row"><span class="dot"></span><div>主题:<b data-theme-label>${theme}</b></div>
|
|
188
|
+
<button class="tile" data-act="theme" style="margin-left:auto;padding:8px 14px">切换</button></div>
|
|
189
|
+
<p style="margin-top:16px">输入</p>
|
|
190
|
+
<p>键盘:Alt+Tab 切换 · Esc 关闭 · Ctrl+K 启动器<br>鼠标:拖动标题栏 · 点击任务栏<br>触屏:单指拖动 · 双指滚动</p>`
|
|
191
|
+
$('[data-act="theme"]', body).onclick = () => {
|
|
192
|
+
toggleTheme()
|
|
193
|
+
$('[data-theme-label]', body).textContent = localStorage.getItem('spak-theme')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* 叶序螺旋点阵(黄金角 137.507764°):r = c*sqrt(n), theta = n*phi */
|
|
198
|
+
function phyllotaxis(n = 90, radius = 9.2) {
|
|
199
|
+
const angle = Math.PI * (3 - Math.sqrt(5))
|
|
200
|
+
const c = radius / Math.sqrt(n)
|
|
201
|
+
let out = ''
|
|
202
|
+
for (let i = 1; i <= n; i++) {
|
|
203
|
+
const r = c * Math.sqrt(i)
|
|
204
|
+
const t = i * angle
|
|
205
|
+
const alpha = (0.30 + 0.70 * (1 - i / n)).toFixed(2)
|
|
206
|
+
out += `<circle cx="${(12 + r * Math.cos(t)).toFixed(3)}" cy="${(12 + r * Math.sin(t)).toFixed(3)}" r="0.52" opacity="${alpha}"/>`
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ---------- 系统 ---------- */
|
|
212
|
+
function toggleTheme() {
|
|
213
|
+
const next = (localStorage.getItem('spak-theme') || 'dark') === 'dark' ? 'light' : 'dark'
|
|
214
|
+
localStorage.setItem('spak-theme', next)
|
|
215
|
+
document.documentElement.dataset.theme = next
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toast(text) {
|
|
219
|
+
const el = document.createElement('div')
|
|
220
|
+
el.textContent = text
|
|
221
|
+
el.style.cssText = 'position:absolute;left:50%;bottom:80px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;background:var(--glass);border:1px solid var(--line);z-index:80'
|
|
222
|
+
document.body.appendChild(el)
|
|
223
|
+
setTimeout(() => el.remove(), 1800)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderIcons() {
|
|
227
|
+
iconBox.innerHTML = ''
|
|
228
|
+
for (const [id, app] of Object.entries(APPS)) {
|
|
229
|
+
const btn = document.createElement('button')
|
|
230
|
+
btn.className = 'icon'
|
|
231
|
+
btn.innerHTML = `<span class="glyph">${app.glyph}</span><span>${app.title}</span>`
|
|
232
|
+
btn.onclick = () => openApp(id)
|
|
233
|
+
iconBox.appendChild(btn)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderLauncher() {
|
|
238
|
+
const grid = $('#apps')
|
|
239
|
+
grid.innerHTML = ''
|
|
240
|
+
for (const [id, app] of Object.entries(APPS)) {
|
|
241
|
+
const tile = document.createElement('button')
|
|
242
|
+
tile.className = 'tile'
|
|
243
|
+
tile.dataset.name = `${app.title} ${id}`.toLowerCase()
|
|
244
|
+
tile.innerHTML = `<span class="glyph">${app.glyph}</span><span>${app.title}</span>`
|
|
245
|
+
tile.onclick = () => { launcher.hidden = true; openApp(id) }
|
|
246
|
+
grid.appendChild(tile)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function tick() {
|
|
251
|
+
clock.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ---------- 输入 ---------- */
|
|
255
|
+
document.addEventListener('keydown', (event) => {
|
|
256
|
+
const typing = event.target instanceof HTMLElement && event.target.matches('input, textarea, .term-line')
|
|
257
|
+
if (event.key === 'Escape') {
|
|
258
|
+
if (!launcher.hidden) { toggleLauncher(false); return }
|
|
259
|
+
if (typing) { event.target.blur(); return }
|
|
260
|
+
const rec = [...state.open.values()].find(entry => entry.el === state.focused)
|
|
261
|
+
if (rec) close(rec)
|
|
262
|
+
else if (state.open.size) close([...state.open.values()].pop())
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (event.ctrlKey && event.key.toLowerCase() === 'k') {
|
|
266
|
+
event.preventDefault()
|
|
267
|
+
toggleLauncher()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
if (event.altKey && event.key === 'Tab') {
|
|
271
|
+
event.preventDefault()
|
|
272
|
+
const list = [...state.open.values()].filter(rec => !rec.el.hidden)
|
|
273
|
+
if (list.length < 2) return
|
|
274
|
+
const index = list.findIndex(rec => rec.el === state.focused)
|
|
275
|
+
focus(list[(index + 1) % list.length].el)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
function toggleLauncher(force) {
|
|
280
|
+
const open = force ?? launcher.hidden
|
|
281
|
+
launcher.hidden = !open
|
|
282
|
+
if (open) for (const tile of $('#apps').children) tile.hidden = false
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
$('#start').onclick = () => toggleLauncher()
|
|
286
|
+
launcher.addEventListener('click', (event) => { if (event.target === launcher) toggleLauncher(false) })
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
/* ---------- 启动 ---------- */
|
|
290
|
+
async function boot() {
|
|
291
|
+
document.documentElement.dataset.theme = localStorage.getItem('spak-theme') || 'dark'
|
|
292
|
+
$('#start svg').innerHTML = phyllotaxis()
|
|
293
|
+
renderIcons()
|
|
294
|
+
renderLauncher()
|
|
295
|
+
tick()
|
|
296
|
+
setInterval(tick, 1000)
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch('/api/apps')
|
|
299
|
+
if (response.ok) state.installed = (await response.json()).apps || []
|
|
300
|
+
const mods = await fetch('/api/mods')
|
|
301
|
+
if (mods.ok) state.mods = (await mods.json()).mods || []
|
|
302
|
+
} catch { /* 离线也照常启动 */ }
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
$('#boot').classList.add('done')
|
|
305
|
+
if (location.hash === '#start') toggleLauncher(true)
|
|
306
|
+
}, 900)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
boot()
|
|
310
|
+
window.SpakOS = { openApp, toggleTheme }
|
|
311
|
+
})()
|
package/README.md
DELETED