kingkont 0.8.7 → 0.9.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/assets/templates.jpg +0 -0
- package/index.html +42 -3
- package/lib/providers.js +131 -2
- package/main.js +120 -0
- package/package.json +1 -1
- package/preload.js +38 -12
- package/renderer/board.js +345 -87
- package/renderer/cloudFs.js +271 -0
- package/renderer/cloudProjects.js +612 -0
- package/renderer/generate.js +7 -0
- package/renderer/settings.js +40 -1
- package/renderer/state.js +5 -0
- package/renderer/styles.css +97 -17
- package/renderer/templates.js +286 -62
- package/renderer/timeline.js +149 -7
- package/server.js +63 -1
- package/skill/SKILL.md +109 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// renderer/cloudFs.js — FileSystemDirectoryHandle shim для облачных проектов.
|
|
2
|
+
//
|
|
3
|
+
// Зачем: state.filmHandle во всём существующем коде типизирован как FSAH.
|
|
4
|
+
// Чтобы не переписывать весь board.js / state.js / generate.js под условия
|
|
5
|
+
// «if (cloudProject) ... else ...», мы оборачиваем IPC к main-процессу
|
|
6
|
+
// (см. preload.js → window.cloudFs) в объект, который имеет тот же интерфейс
|
|
7
|
+
// что FileSystemDirectoryHandle:
|
|
8
|
+
//
|
|
9
|
+
// - .name — последний сегмент path
|
|
10
|
+
// - .kind === 'directory'
|
|
11
|
+
// - .queryPermission() — всегда 'granted' (это наша же папка)
|
|
12
|
+
// - .requestPermission() — то же
|
|
13
|
+
// - .entries() / .keys() — async-iterator
|
|
14
|
+
// - .getDirectoryHandle(name, { create })
|
|
15
|
+
// - .getFileHandle(name, { create })
|
|
16
|
+
// - .removeEntry(name, { recursive })
|
|
17
|
+
//
|
|
18
|
+
// FileHandle также мимикрирует:
|
|
19
|
+
// - .name
|
|
20
|
+
// - .kind === 'file'
|
|
21
|
+
// - .getFile() → File-like объект (.size, .type, .name, .arrayBuffer())
|
|
22
|
+
// - .createWritable() → { write(data), close() }
|
|
23
|
+
//
|
|
24
|
+
// На web-версии (без preload) — window.cloudFs undefined. Тогда возвращаем
|
|
25
|
+
// in-memory shim (карта { path → Uint8Array }). При save-to-server он
|
|
26
|
+
// сериализуется и уходит в /api/projects~update.
|
|
27
|
+
//
|
|
28
|
+
// Идентификатор проекта: projectId (string). Для in-memory — любой стабильный
|
|
29
|
+
// ключ, мы используем 'web:<serverId>' если открыт серверный, или 'web:<uuid>'
|
|
30
|
+
// для нового unsaved.
|
|
31
|
+
|
|
32
|
+
(function () {
|
|
33
|
+
// === In-memory FS (web fallback). ============================================
|
|
34
|
+
// Структура: Map<projectId, Map<path, {kind:'file'|'dir', data?: Uint8Array, mtimeMs?: number}>>.
|
|
35
|
+
// Path хранится в normalised форме без leading slash (например 'characters/Anna/scene.json').
|
|
36
|
+
const _mem = new Map();
|
|
37
|
+
function _memProject(id) {
|
|
38
|
+
if (!_mem.has(id)) _mem.set(id, new Map());
|
|
39
|
+
return _mem.get(id);
|
|
40
|
+
}
|
|
41
|
+
// Нормализация и помощник для родительских папок: 'a/b/c' → ['a', 'a/b', 'a/b/c'].
|
|
42
|
+
function _normalize(p) { return String(p || '').replace(/^\/+|\/+$/g, ''); }
|
|
43
|
+
function _ensureDirs(map, p) {
|
|
44
|
+
const parts = _normalize(p).split('/');
|
|
45
|
+
let acc = '';
|
|
46
|
+
for (let i = 0; i < parts.length; i++) {
|
|
47
|
+
acc = acc ? acc + '/' + parts[i] : parts[i];
|
|
48
|
+
if (!map.has(acc)) map.set(acc, { kind: 'dir' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function _split(p) {
|
|
52
|
+
const n = _normalize(p);
|
|
53
|
+
const i = n.lastIndexOf('/');
|
|
54
|
+
return i < 0 ? { dir: '', name: n } : { dir: n.slice(0, i), name: n.slice(i + 1) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// === Backend factories. =====================================================
|
|
58
|
+
// Возвращает объект с минимальным API над хранилищем (диск или память).
|
|
59
|
+
function makeBackend(projectId) {
|
|
60
|
+
if (window.cloudFs) {
|
|
61
|
+
const fs = window.cloudFs;
|
|
62
|
+
return {
|
|
63
|
+
async ensure() { await fs.ensureProject(projectId); },
|
|
64
|
+
list(rel) { return fs.list(projectId, rel || ''); },
|
|
65
|
+
exists(rel) { return fs.exists(projectId, rel || ''); },
|
|
66
|
+
stat(rel) { return fs.stat(projectId, rel || ''); },
|
|
67
|
+
read(rel) { return fs.read(projectId, rel || ''); }, // Uint8Array
|
|
68
|
+
readText(rel) { return fs.readText(projectId, rel || ''); },
|
|
69
|
+
write(rel, data) { return fs.write(projectId, rel || '', data); },
|
|
70
|
+
mkdir(rel) { return fs.mkdir(projectId, rel || ''); },
|
|
71
|
+
remove(rel) { return fs.remove(projectId, rel || ''); },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ---- web in-memory fallback ----
|
|
75
|
+
const map = _memProject(projectId);
|
|
76
|
+
return {
|
|
77
|
+
async ensure() { _ensureDirs(map, ''); },
|
|
78
|
+
async list(rel) {
|
|
79
|
+
const prefix = _normalize(rel);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const [k, v] of map.entries()) {
|
|
82
|
+
if (k === prefix) continue;
|
|
83
|
+
if (prefix && !k.startsWith(prefix + '/')) continue;
|
|
84
|
+
if (!prefix && k.includes('/')) continue;
|
|
85
|
+
if (prefix && k.slice(prefix.length + 1).includes('/')) continue;
|
|
86
|
+
out.push({ name: k.split('/').pop(), kind: v.kind });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
},
|
|
90
|
+
async exists(rel) { return map.has(_normalize(rel)); },
|
|
91
|
+
async stat(rel) {
|
|
92
|
+
const v = map.get(_normalize(rel));
|
|
93
|
+
if (!v) return null;
|
|
94
|
+
return {
|
|
95
|
+
size: v.data?.byteLength ?? 0,
|
|
96
|
+
mtimeMs: v.mtimeMs ?? 0,
|
|
97
|
+
isFile: v.kind === 'file',
|
|
98
|
+
isDirectory: v.kind === 'dir',
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
async read(rel) {
|
|
102
|
+
const v = map.get(_normalize(rel));
|
|
103
|
+
if (!v || v.kind !== 'file') throw new Error('ENOENT: ' + rel);
|
|
104
|
+
return v.data || new Uint8Array(0);
|
|
105
|
+
},
|
|
106
|
+
async readText(rel) {
|
|
107
|
+
const v = map.get(_normalize(rel));
|
|
108
|
+
if (!v || v.kind !== 'file') return null;
|
|
109
|
+
return new TextDecoder('utf-8').decode(v.data || new Uint8Array(0));
|
|
110
|
+
},
|
|
111
|
+
async write(rel, data) {
|
|
112
|
+
const p = _normalize(rel);
|
|
113
|
+
const { dir } = _split(p);
|
|
114
|
+
if (dir) _ensureDirs(map, dir);
|
|
115
|
+
const buf = typeof data === 'string'
|
|
116
|
+
? new TextEncoder().encode(data)
|
|
117
|
+
: (data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
118
|
+
map.set(p, { kind: 'file', data: buf, mtimeMs: Date.now() });
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
async mkdir(rel) { _ensureDirs(map, rel); return true; },
|
|
122
|
+
async remove(rel) {
|
|
123
|
+
const p = _normalize(rel);
|
|
124
|
+
// Удаляем запись и все потомки.
|
|
125
|
+
for (const k of Array.from(map.keys())) {
|
|
126
|
+
if (k === p || k.startsWith(p + '/')) map.delete(k);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// === FSAH-shim — directory + file. ==========================================
|
|
134
|
+
function _join(a, b) { return a ? a + '/' + b : b; }
|
|
135
|
+
|
|
136
|
+
function makeDirHandle(backend, dirPath, projectId, displayName) {
|
|
137
|
+
const _name = displayName ?? (dirPath ? dirPath.split('/').pop() : projectId);
|
|
138
|
+
return {
|
|
139
|
+
kind: 'directory',
|
|
140
|
+
name: _name,
|
|
141
|
+
// FSAH-специфичные методы пермишенов — у нас всегда granted.
|
|
142
|
+
async queryPermission() { return 'granted'; },
|
|
143
|
+
async requestPermission() { return 'granted'; },
|
|
144
|
+
|
|
145
|
+
async getFileHandle(name, opts) {
|
|
146
|
+
const rel = _join(dirPath, name);
|
|
147
|
+
const exists = await backend.exists(rel);
|
|
148
|
+
if (!exists) {
|
|
149
|
+
if (opts?.create) {
|
|
150
|
+
await backend.write(rel, new Uint8Array(0));
|
|
151
|
+
} else {
|
|
152
|
+
const e = new Error(`A requested file or directory could not be found at the time an operation was processed. (file: ${rel})`);
|
|
153
|
+
e.name = 'NotFoundError';
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return makeFileHandle(backend, rel, name);
|
|
158
|
+
},
|
|
159
|
+
async getDirectoryHandle(name, opts) {
|
|
160
|
+
const rel = _join(dirPath, name);
|
|
161
|
+
const exists = await backend.exists(rel);
|
|
162
|
+
if (!exists) {
|
|
163
|
+
if (opts?.create) {
|
|
164
|
+
await backend.mkdir(rel);
|
|
165
|
+
} else {
|
|
166
|
+
const e = new Error(`A requested directory could not be found at the time an operation was processed. (dir: ${rel})`);
|
|
167
|
+
e.name = 'NotFoundError';
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return makeDirHandle(backend, rel, projectId, name);
|
|
172
|
+
},
|
|
173
|
+
async removeEntry(name, opts) {
|
|
174
|
+
await backend.remove(_join(dirPath, name));
|
|
175
|
+
},
|
|
176
|
+
// async-iterable — повторяет .entries() FSAH ([name, handle]).
|
|
177
|
+
async *entries() {
|
|
178
|
+
const items = await backend.list(dirPath);
|
|
179
|
+
for (const it of items) {
|
|
180
|
+
const sub = _join(dirPath, it.name);
|
|
181
|
+
if (it.kind === 'dir') yield [it.name, makeDirHandle(backend, sub, projectId, it.name)];
|
|
182
|
+
else yield [it.name, makeFileHandle(backend, sub, it.name)];
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
async *keys() {
|
|
186
|
+
const items = await backend.list(dirPath);
|
|
187
|
+
for (const it of items) yield it.name;
|
|
188
|
+
},
|
|
189
|
+
async *values() {
|
|
190
|
+
for (const [, h] of this.entries()) yield h;
|
|
191
|
+
},
|
|
192
|
+
[Symbol.asyncIterator]() { return this.entries(); },
|
|
193
|
+
|
|
194
|
+
// --- KingKont-специфичные расширения (не часть стандарта FSAH). -----
|
|
195
|
+
__cloudProjectId: projectId, // используется board.js для ветвления
|
|
196
|
+
__cloudPath: dirPath,
|
|
197
|
+
__backend: backend,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function makeFileHandle(backend, relPath, displayName) {
|
|
202
|
+
const _name = displayName ?? relPath.split('/').pop();
|
|
203
|
+
return {
|
|
204
|
+
kind: 'file',
|
|
205
|
+
name: _name,
|
|
206
|
+
async queryPermission() { return 'granted'; },
|
|
207
|
+
async requestPermission() { return 'granted'; },
|
|
208
|
+
async getFile() {
|
|
209
|
+
const buf = await backend.read(relPath);
|
|
210
|
+
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
211
|
+
const stat = await backend.stat(relPath);
|
|
212
|
+
// Имитация Web File: (.name, .size, .type, .arrayBuffer(), .text(), .stream()).
|
|
213
|
+
const blob = new Blob([u8]);
|
|
214
|
+
return Object.assign(blob, {
|
|
215
|
+
name: _name,
|
|
216
|
+
lastModified: stat?.mtimeMs ?? Date.now(),
|
|
217
|
+
arrayBuffer: () => Promise.resolve(u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength)),
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
async createWritable() {
|
|
221
|
+
let chunks = [];
|
|
222
|
+
return {
|
|
223
|
+
async write(data) {
|
|
224
|
+
if (typeof data === 'string') chunks.push(new TextEncoder().encode(data));
|
|
225
|
+
else if (data instanceof Blob) chunks.push(new Uint8Array(await data.arrayBuffer()));
|
|
226
|
+
else if (data instanceof Uint8Array) chunks.push(data);
|
|
227
|
+
else if (data instanceof ArrayBuffer) chunks.push(new Uint8Array(data));
|
|
228
|
+
else throw new Error('createWritable: unsupported data type');
|
|
229
|
+
},
|
|
230
|
+
async close() {
|
|
231
|
+
const total = chunks.reduce((s, c) => s + c.byteLength, 0);
|
|
232
|
+
const merged = new Uint8Array(total);
|
|
233
|
+
let off = 0;
|
|
234
|
+
for (const c of chunks) { merged.set(c, off); off += c.byteLength; }
|
|
235
|
+
await backend.write(relPath, merged);
|
|
236
|
+
chunks = null;
|
|
237
|
+
},
|
|
238
|
+
async abort() { chunks = null; },
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
__cloudProjectId: backend.__projectId,
|
|
242
|
+
__cloudPath: relPath,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// === Public API. ============================================================
|
|
247
|
+
// Создать FSAH-shim для облачного проекта. projectName показывается как
|
|
248
|
+
// .name root-handle'а (вместо projectId).
|
|
249
|
+
async function makeCloudHandle(projectId, projectName) {
|
|
250
|
+
const backend = makeBackend(projectId);
|
|
251
|
+
backend.__projectId = projectId;
|
|
252
|
+
await backend.ensure();
|
|
253
|
+
return makeDirHandle(backend, '', projectId, projectName || projectId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Проверить — это cloud-handle (наш shim) или native FSAH?
|
|
257
|
+
function isCloudHandle(handle) {
|
|
258
|
+
return !!(handle && handle.__cloudProjectId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Получить projectId / backend из любого вложенного cloud-handle.
|
|
262
|
+
function getCloudProjectId(handle) { return handle?.__cloudProjectId || null; }
|
|
263
|
+
|
|
264
|
+
window.cloudFsShim = {
|
|
265
|
+
makeCloudHandle,
|
|
266
|
+
isCloudHandle,
|
|
267
|
+
getCloudProjectId,
|
|
268
|
+
// exposed для save-to-server: пройтись по всем файлам in-memory map'а.
|
|
269
|
+
_memProject,
|
|
270
|
+
};
|
|
271
|
+
})();
|