dsh-plugin-vrmpet3d 0.1.0 → 0.1.2
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/README.md +2 -0
- package/desktop/main.js +47 -4
- package/desktop/preload.js +7 -2
- package/lib/client.core.js +487 -53
- package/lib/client.js +391 -57
- package/lib/client.menu.js +14 -4
- package/lib/client.scene.js +134 -7
- package/lib/config-page.html +82 -45
- package/lib/config-page.js +1271 -252
- package/lib/core.js +487 -53
- package/lib/desktop-page.js +298 -44
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +172 -101
- package/lib/index.js +196 -58
- package/lib/menu.js +14 -4
- package/lib/model-picker.js +139 -49
- package/lib/picker-page.html +34 -8
- package/lib/picker-page.js +192 -11
- package/lib/scene.js +134 -7
- package/lib/setup-page.html +68 -49
- package/lib/setup-page.js +395 -42
- package/lib/vrm-info.js +16 -1
- package/lib/whale.js +191 -0
- package/package.json +1 -1
package/lib/model-picker.js
CHANGED
|
@@ -30,47 +30,73 @@
|
|
|
30
30
|
var MODEL_ACCEPT = '.vrm,.glb,.gltf';
|
|
31
31
|
var MODEL_PATTERN = /\.(vrm|glb|gltf)$/i;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* This surface's own wording, in the two languages it draws in: `TEXT.zh` is the Chinese half, and the English
|
|
35
|
+
* twin is the literal {@link resolveText} returns for `'en'`.
|
|
36
|
+
*
|
|
37
|
+
* TWO LITERALS, one per language, rather than two columns of one table — because each half has to be written as
|
|
38
|
+
* its own language rather than as a translation of the other, which is also why `lib/menu.js` spells its
|
|
39
|
+
* `LABELS` out twice. Both halves are written in FULL: a half that is merely a gloss on the other is what makes
|
|
40
|
+
* a half-translated interface.
|
|
41
|
+
*
|
|
42
|
+
* THE TWO KEY SETS MUST BE IDENTICAL. A key the Chinese half has and the English does not falls back to Chinese
|
|
43
|
+
* mid-sentence (`core.textFor`), which is the defect a suite asserts against — and that includes the
|
|
44
|
+
* FUNCTION-valued entries: they stay functions in both halves because that is what lets the English put its
|
|
45
|
+
* words in its own order (`creditAuthor(author)`) rather than in the Chinese sentence's order.
|
|
46
|
+
*/
|
|
33
47
|
var TEXT = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
zh: {
|
|
49
|
+
dropTitle: '点击选择模型文件',
|
|
50
|
+
dropSub: '或把 ' + MODEL_ACCEPT.split(',').join(' / ') + ' 拖到这里',
|
|
51
|
+
reading: '正在读取模型列表…',
|
|
52
|
+
listFailed: '模型列表读取失败。',
|
|
53
|
+
empty: '还没有可用的模型。',
|
|
54
|
+
gridLabel: '现有模型',
|
|
55
|
+
count: function (total, imported) {
|
|
56
|
+
return '共 ' + total + ' 个模型' + (imported ? '(其中 ' + imported + ' 个是导入的)' : '') + '。';
|
|
57
|
+
},
|
|
58
|
+
tagBundled: '自带',
|
|
59
|
+
tagUser: '已导入',
|
|
60
|
+
using: '使用中',
|
|
61
|
+
remove: '移除这个导入的模型',
|
|
62
|
+
importing: '正在导入模型…',
|
|
63
|
+
importingFile: function (name) { return '正在导入 ' + name + '…'; },
|
|
64
|
+
imported: function (name) { return '已导入并切换:' + name; },
|
|
65
|
+
importedNoSwitch: function (name) { return '已导入:' + name; },
|
|
66
|
+
importFailed: '导入失败:',
|
|
67
|
+
switched: function (name) { return '已切换模型:' + name; },
|
|
68
|
+
switching: function (name) { return '正在切换模型:' + name + '…'; },
|
|
69
|
+
switchingBusy: '正在切换中,等这一只加载完再点。',
|
|
70
|
+
switchingSlow: '仍在加载。',
|
|
71
|
+
switchingTimeout: function (name) { return '切换超时:' + name + ' 可能没能加载,看看桌宠上的提示。'; },
|
|
72
|
+
noModel: '这个文件不是模型(只接受 .vrm / .glb / .gltf)。',
|
|
73
|
+
dropBusy: '正在导入,请等它读完。',
|
|
74
|
+
noFetchUpload: '这个页面不能上传文件。',
|
|
75
|
+
removed: '已移除插件复制的那一份。',
|
|
76
|
+
removeFailed: '移除失败:',
|
|
77
|
+
/**
|
|
78
|
+
* 自带模型的来源与作者 — the operator's own requirement (2026-10-13: 「需要注明来源和作者」).
|
|
79
|
+
*
|
|
80
|
+
* The WORDS live here with every other string; the FACTS come from the host, which is the only writer of a
|
|
81
|
+
* catalogue entry's `credit` (`BUNDLED_MODEL_CREDITS` in `lib/index.js`). A credit typed into this file as
|
|
82
|
+
* well would be a second copy of who made what.
|
|
83
|
+
*/
|
|
84
|
+
creditTitle: '插件自带模型',
|
|
85
|
+
creditName: function (title, file) { return title ? title + '(' + file + ')' : file; },
|
|
86
|
+
creditAuthor: function (author) { return '作者:' + author; },
|
|
87
|
+
creditLicense: function (license) { return '授权:' + license; },
|
|
88
|
+
creditSource: function (source) { return '来源:' + source; },
|
|
41
89
|
},
|
|
42
|
-
tagBundled: '自带',
|
|
43
|
-
tagUser: '已导入',
|
|
44
|
-
using: '使用中',
|
|
45
|
-
remove: '移除这个导入的模型',
|
|
46
|
-
importing: '正在导入模型…',
|
|
47
|
-
importingFile: function (name) { return '正在导入 ' + name + '…'; },
|
|
48
|
-
imported: function (name) { return '已导入并切换:' + name; },
|
|
49
|
-
importedNoSwitch: function (name) { return '已导入:' + name; },
|
|
50
|
-
importFailed: '导入失败:',
|
|
51
|
-
switched: function (name) { return '已切换模型:' + name; },
|
|
52
|
-
switching: function (name) { return '正在切换模型:' + name + '…'; },
|
|
53
|
-
switchingBusy: '正在切换中,等这一只加载完再点。',
|
|
54
|
-
switchingSlow: '仍在加载。',
|
|
55
|
-
switchingTimeout: function (name) { return '切换超时:' + name + ' 可能没能加载,看看桌宠上的提示。'; },
|
|
56
|
-
noModel: '这个文件不是模型(只接受 .vrm / .glb / .gltf)。',
|
|
57
|
-
dropBusy: '正在导入,请等它读完。',
|
|
58
|
-
removed: '已移除插件复制的那一份。',
|
|
59
|
-
removeFailed: '移除失败:',
|
|
60
|
-
/**
|
|
61
|
-
* 自带模型的来源与作者 — the operator's own requirement (2026-10-13: 「需要注明来源和作者」).
|
|
62
|
-
*
|
|
63
|
-
* The WORDS live here with every other string; the FACTS come from the host, which is the only writer of a
|
|
64
|
-
* catalogue entry's `credit` (`BUNDLED_MODEL_CREDITS` in `lib/index.js`). A credit typed into this file as
|
|
65
|
-
* well would be a second copy of who made what.
|
|
66
|
-
*/
|
|
67
|
-
creditTitle: '插件自带模型',
|
|
68
|
-
creditName: function (title, file) { return title ? title + '(' + file + ')' : file; },
|
|
69
|
-
creditAuthor: function (author) { return '作者:' + author; },
|
|
70
|
-
creditLicense: function (license) { return '授权:' + license; },
|
|
71
|
-
creditSource: function (source) { return '来源:' + source; },
|
|
72
90
|
};
|
|
73
91
|
|
|
92
|
+
/**
|
|
93
|
+
* This surface's words in one of its two languages — the accessor every caller goes through, and what
|
|
94
|
+
* `__VRMPET3D_MODEL_PICKER__.strings` exposes to a suite.
|
|
95
|
+
*
|
|
96
|
+
* `'zh'` (and anything that is not `'en'`, which includes a caller that passed no language at all) gets the
|
|
97
|
+
* Chinese half, the default this component has always had. `'auto'` is NOT resolved here: the caller knows what
|
|
98
|
+
* the environment says and passes the answer in, so a component that never sees a store cannot guess wrong.
|
|
99
|
+
*/
|
|
74
100
|
function resolveText(language) {
|
|
75
101
|
if (language === 'en') {
|
|
76
102
|
return {
|
|
@@ -79,8 +105,9 @@
|
|
|
79
105
|
reading: 'Reading the model list…',
|
|
80
106
|
listFailed: 'Could not read the model list.',
|
|
81
107
|
empty: 'No models available yet.',
|
|
108
|
+
gridLabel: 'Available models',
|
|
82
109
|
count: function (total, imported) {
|
|
83
|
-
return total + ' models' + (imported ? ' (' + imported + ' imported)' : '') + '.';
|
|
110
|
+
return total + (total === 1 ? ' model' : ' models') + (imported ? ' (' + imported + ' imported)' : '') + '.';
|
|
84
111
|
},
|
|
85
112
|
tagBundled: 'built-in',
|
|
86
113
|
tagUser: 'imported',
|
|
@@ -96,18 +123,19 @@
|
|
|
96
123
|
switchingBusy: 'A switch is already running — wait for this one to load.',
|
|
97
124
|
switchingSlow: 'Still loading.',
|
|
98
125
|
switchingTimeout: function (name) { return 'The switch timed out: ' + name + ' may not have loaded (check the pet for a message).'; },
|
|
99
|
-
noModel: 'That is not a model this plugin
|
|
100
|
-
dropBusy: 'An import is already running.',
|
|
126
|
+
noModel: 'That file is not a model this plugin can load (.vrm / .glb / .gltf only).',
|
|
127
|
+
dropBusy: 'An import is already running — wait for it to finish.',
|
|
128
|
+
noFetchUpload: 'This page cannot upload files.',
|
|
101
129
|
removed: 'Removed the plugin\'s own copy.',
|
|
102
130
|
removeFailed: 'Could not remove it: ',
|
|
103
|
-
creditTitle: '
|
|
131
|
+
creditTitle: 'Bundled models',
|
|
104
132
|
creditName: function (title, file) { return title ? title + ' (' + file + ')' : file; },
|
|
105
133
|
creditAuthor: function (author) { return 'Author: ' + author; },
|
|
106
|
-
creditLicense: function (license) { return '
|
|
134
|
+
creditLicense: function (license) { return 'License: ' + license; },
|
|
107
135
|
creditSource: function (source) { return 'Source: ' + source; },
|
|
108
136
|
};
|
|
109
137
|
}
|
|
110
|
-
return TEXT;
|
|
138
|
+
return TEXT.zh;
|
|
111
139
|
}
|
|
112
140
|
|
|
113
141
|
function element(doc, tag, className, value) {
|
|
@@ -133,7 +161,9 @@
|
|
|
133
161
|
* @param {object} options
|
|
134
162
|
* document the document to build in (defaults to the global one)
|
|
135
163
|
* mount the element to fill — the component owns everything inside it
|
|
136
|
-
* language 'zh' (default) or 'en'
|
|
164
|
+
* language 'zh' (default) or 'en' — the language it is BUILT in; `api.setLanguage` changes it later
|
|
165
|
+
* without a rebuild, which is how a caller whose language comes from the settings store
|
|
166
|
+
* (the 切换模型 page) follows a change while the grid is on screen
|
|
137
167
|
* bridge the launcher bridge, or null to force the browser path (defaults to
|
|
138
168
|
* `globalThis.vrmpet3dDesktop`); a page with no bridge still gets a working picker
|
|
139
169
|
* fetchImpl fetch override, for a probe
|
|
@@ -154,7 +184,16 @@
|
|
|
154
184
|
var mount = settings.mount;
|
|
155
185
|
if (!doc || !mount) throw new Error('createModelPicker needs a document and a mount element');
|
|
156
186
|
|
|
157
|
-
|
|
187
|
+
/**
|
|
188
|
+
* The language this picker draws in, and its table.
|
|
189
|
+
*
|
|
190
|
+
* Resolved ONCE here, from `options.language` — 'zh' unless the caller says 'en', which is the default this
|
|
191
|
+
* option has always had (the wizard mounts with 'zh'). A caller whose language can change while the picker is on
|
|
192
|
+
* screen calls {@link api.setLanguage} instead of rebuilding: the 切换模型 page is exactly that caller, because
|
|
193
|
+
* its language comes from the settings store it polls.
|
|
194
|
+
*/
|
|
195
|
+
var language = settings.language === 'en' ? 'en' : 'zh';
|
|
196
|
+
var text = resolveText(language);
|
|
158
197
|
var onSwitch = typeof settings.onSwitch === 'function' ? settings.onSwitch : function () {};
|
|
159
198
|
var onStatus = typeof settings.onStatus === 'function' ? settings.onStatus : function () {};
|
|
160
199
|
var doFetch = settings.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
|
|
@@ -212,7 +251,7 @@
|
|
|
212
251
|
input.hidden = true;
|
|
213
252
|
|
|
214
253
|
var gridHead = element(doc, 'div', 'model-grid-head');
|
|
215
|
-
var gridLabel = element(doc, 'span', 'k',
|
|
254
|
+
var gridLabel = element(doc, 'span', 'k', text.gridLabel);
|
|
216
255
|
var gridCount = element(doc, 'span', 'note model-grid-count');
|
|
217
256
|
gridHead.appendChild(gridLabel);
|
|
218
257
|
gridHead.appendChild(gridCount);
|
|
@@ -385,7 +424,7 @@
|
|
|
385
424
|
}
|
|
386
425
|
|
|
387
426
|
function requestJson(path, body) {
|
|
388
|
-
if (!doFetch) return Promise.reject(new Error(
|
|
427
|
+
if (!doFetch) return Promise.reject(new Error(text.noFetchUpload));
|
|
389
428
|
return doFetch(path, {
|
|
390
429
|
method: 'POST',
|
|
391
430
|
credentials: 'omit',
|
|
@@ -494,6 +533,32 @@
|
|
|
494
533
|
});
|
|
495
534
|
}
|
|
496
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Draw everything again in the CURRENT `text` — the whole of a language change.
|
|
538
|
+
*
|
|
539
|
+
* Three static labels are written at construction (the drop zone's two lines and the grid's heading) and one
|
|
540
|
+
* more is stateful (the progress bar, whose two lines are set when a switch STARTS). Everything else already
|
|
541
|
+
* comes out of `render()`, so repainting the grid covers the tiles, the tags, the 使用中 badge, the count, the
|
|
542
|
+
* empty note and the credit lines — and the bar is rewritten here so a switch that is in flight says the same
|
|
543
|
+
* thing in the new language instead of freezing in the old one until it settles.
|
|
544
|
+
*/
|
|
545
|
+
function relabel() {
|
|
546
|
+
dropTitle.textContent = text.dropTitle;
|
|
547
|
+
dropSub.textContent = text.dropSub;
|
|
548
|
+
gridLabel.textContent = text.gridLabel;
|
|
549
|
+
if (switching) {
|
|
550
|
+
progressText.textContent = text.switching(switching.name);
|
|
551
|
+
progressNote.textContent = switching.slow ? text.switchingSlow : '';
|
|
552
|
+
progress.setAttribute('aria-valuetext', text.switching(switching.name));
|
|
553
|
+
// And the CALLER's line, which is where the same sentence usually lives: without this the bar would say
|
|
554
|
+
// 「正在切换模型…」 while the status line above it stayed in the language the switch STARTED in. The
|
|
555
|
+
// component owns this text, so it is the component that says it again — through the same channel it was
|
|
556
|
+
// first said on.
|
|
557
|
+
onStatus(text.switching(switching.name));
|
|
558
|
+
}
|
|
559
|
+
render();
|
|
560
|
+
}
|
|
561
|
+
|
|
497
562
|
/**
|
|
498
563
|
* Write out 来源 and 作者 for every bundled model the host sent a credit for.
|
|
499
564
|
*
|
|
@@ -621,7 +686,7 @@
|
|
|
621
686
|
return Promise.resolve(null);
|
|
622
687
|
}
|
|
623
688
|
if (!doFetch) {
|
|
624
|
-
onStatus(text.
|
|
689
|
+
onStatus(text.noFetchUpload, 'warn');
|
|
625
690
|
return Promise.resolve(null);
|
|
626
691
|
}
|
|
627
692
|
setBusy(true, text.importingFile(name));
|
|
@@ -690,7 +755,7 @@
|
|
|
690
755
|
function pickNative() {
|
|
691
756
|
var chooser = bridge && typeof bridge.pickModelFile === 'function' ? bridge.pickModelFile : null;
|
|
692
757
|
if (!chooser) return false;
|
|
693
|
-
Promise.resolve(chooser()).then(function (chosen) { return chosen ? importPath(chosen) : null; });
|
|
758
|
+
Promise.resolve(chooser(language)).then(function (chosen) { return chosen ? importPath(chosen) : null; });
|
|
694
759
|
return true;
|
|
695
760
|
}
|
|
696
761
|
|
|
@@ -750,6 +815,30 @@
|
|
|
750
815
|
* handler calls it — see `settleSwitch`.
|
|
751
816
|
*/
|
|
752
817
|
setCurrent: function () { pendingId = null; render(); settleSwitch(); },
|
|
818
|
+
/**
|
|
819
|
+
* Draw in another language, WITHOUT being rebuilt.
|
|
820
|
+
*
|
|
821
|
+
* `options.language` is fixed at construction, and that is right for a wizard step whose language was settled
|
|
822
|
+
* before the step was reached. The 切换模型 page is the other case: its language comes from the settings store
|
|
823
|
+
* it polls, so it can change while the picker is on screen. Rebuilding would be the easy answer and the wrong
|
|
824
|
+
* one — it would throw away a switch's progress bar, its pending tile and the catalogue it already has, so a
|
|
825
|
+
* language change would cost a second round trip and lose the user's click. `relabel()` is what this changes:
|
|
826
|
+
* the same nodes, the same state, new words.
|
|
827
|
+
*
|
|
828
|
+
* A language this component does not know ('auto', undefined, a typo) is IGNORED rather than treated as
|
|
829
|
+
* Chinese: the picker keeps the table it has, so a caller that passes the store's raw preference through
|
|
830
|
+
* cannot flip a page to Chinese by accident.
|
|
831
|
+
*
|
|
832
|
+
* @returns {boolean} whether the picker redrew — false for "same language" and for "not a language I know"
|
|
833
|
+
*/
|
|
834
|
+
setLanguage: function (next) {
|
|
835
|
+
var wanted = next === 'en' ? 'en' : (next === 'zh' ? 'zh' : null);
|
|
836
|
+
if (!wanted || wanted === language) return false;
|
|
837
|
+
language = wanted;
|
|
838
|
+
text = resolveText(language);
|
|
839
|
+
relabel();
|
|
840
|
+
return true;
|
|
841
|
+
},
|
|
753
842
|
/** Adopt a catalog payload a caller already has (the guide's own GET, a probe). */
|
|
754
843
|
apply: apply,
|
|
755
844
|
/** Import a path (the launcher's dialog) or a File (a drop, a browser input). */
|
|
@@ -766,6 +855,7 @@
|
|
|
766
855
|
}),
|
|
767
856
|
total: models.length,
|
|
768
857
|
imported: imported.length,
|
|
858
|
+
language: language,
|
|
769
859
|
current: currentId(),
|
|
770
860
|
busy: busy,
|
|
771
861
|
switching: switching ? switching.id : null,
|
package/lib/picker-page.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>vrmpet3d · 切换模型</title>
|
|
6
|
+
<title data-i18n="title">vrmpet3d · 切换模型</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light">
|
|
8
8
|
<!--
|
|
9
9
|
切换模型 — the model picker's own page (`/vrmpet3d/picker`), opened from either pet's right-click menu.
|
|
@@ -21,15 +21,21 @@
|
|
|
21
21
|
<link rel="stylesheet" href="/vrmpet3d/window/lib/pages.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body data-page="picker">
|
|
24
|
+
<!--
|
|
25
|
+
The page's own five pieces of text carry `data-i18n` and keep the Chinese inline: `lib/picker-page.js`
|
|
26
|
+
resolves the language and calls `core.applyI18n`, and a page whose script never ran still reads in one
|
|
27
|
+
language rather than showing empty nodes. `html lang` is that same Chinese default, and the script rewrites it
|
|
28
|
+
once it knows better — it is NOT what the language is resolved from (see `core.browserLanguageHints`).
|
|
29
|
+
-->
|
|
24
30
|
<header class="page-header">
|
|
25
|
-
<div class="eyebrow">VRMPET3D · MODELS</div>
|
|
26
|
-
<h1>切换模型</h1>
|
|
27
|
-
<div id="status" role="status" aria-live="polite">正在读取…</div>
|
|
31
|
+
<div class="eyebrow" data-i18n="eyebrow">VRMPET3D · MODELS</div>
|
|
32
|
+
<h1 data-i18n="heading">切换模型</h1>
|
|
33
|
+
<div id="status" role="status" aria-live="polite" data-i18n="statusLoading">正在读取…</div>
|
|
28
34
|
</header>
|
|
29
35
|
|
|
30
36
|
<main>
|
|
31
37
|
<section class="settings-section" id="settings-0">
|
|
32
|
-
<h2>模型</h2>
|
|
38
|
+
<h2 data-i18n="sectionModels">模型</h2>
|
|
33
39
|
<div class="card" id="picker-mount"></div>
|
|
34
40
|
</section>
|
|
35
41
|
</main>
|
|
@@ -40,20 +46,40 @@
|
|
|
40
46
|
-->
|
|
41
47
|
<script>
|
|
42
48
|
(function () {
|
|
49
|
+
/**
|
|
50
|
+
* The watchdog's OWN words, in its own two languages, because it runs BEFORE core.js and can use neither
|
|
51
|
+
* `core.textFor` nor the settings store. `navigator.language` is the whole of its rule: the document's own
|
|
52
|
+
* `lang` is the Chinese default every page here ships, so reading it would only ever say "Chinese".
|
|
53
|
+
*/
|
|
54
|
+
var WORDS = {
|
|
55
|
+
zh: {
|
|
56
|
+
pageError: '页面脚本出错:',
|
|
57
|
+
stuckMissing: '初始化卡住:model-picker.js 没有加载(检查 /vrmpet3d/window/lib/model-picker.js 是否 404)。',
|
|
58
|
+
stuckSlow: '初始化卡住:脚本已加载但没有完成初始化(看看窗口的控制台,或宿主是否提供 /vrmpet3d/models 与 /vrmpet3d/settings)。',
|
|
59
|
+
},
|
|
60
|
+
en: {
|
|
61
|
+
pageError: 'Script error: ',
|
|
62
|
+
stuckMissing: 'Stuck on startup: model-picker.js did not load — check whether /vrmpet3d/window/lib/model-picker.js is a 404.',
|
|
63
|
+
stuckSlow: 'Stuck on startup: the script loaded but never finished — check the window console, and whether the host serves /vrmpet3d/models and /vrmpet3d/settings.',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
var language = /^(zh|cn|chinese)/i.test(navigator.language || '') ? 'zh' : 'en';
|
|
67
|
+
function t(key) { return WORDS[language][key]; }
|
|
43
68
|
function show(message) {
|
|
44
69
|
var status = document.getElementById("status");
|
|
45
70
|
if (status) { status.textContent = message; status.className = "warn"; }
|
|
46
71
|
}
|
|
47
72
|
window.addEventListener("error", function (event) {
|
|
48
|
-
|
|
73
|
+
var where = event.filename ? (language === 'zh' ? ' (' + event.filename + ')' : ' (' + event.filename + ')') : '';
|
|
74
|
+
show(t('pageError') + (event.message || event.type) + where);
|
|
49
75
|
}, true);
|
|
50
76
|
window.setTimeout(function () {
|
|
51
77
|
if (window.__VRMPET3D_PICKER_STATE__) return;
|
|
52
78
|
if (!window.__VRMPET3D_MODEL_PICKER__) {
|
|
53
|
-
show(
|
|
79
|
+
show(t('stuckMissing'));
|
|
54
80
|
return;
|
|
55
81
|
}
|
|
56
|
-
show(
|
|
82
|
+
show(t('stuckSlow'));
|
|
57
83
|
}, 2000);
|
|
58
84
|
})();
|
|
59
85
|
</script>
|
package/lib/picker-page.js
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* (`hasSnapshot` in `lib/config-page.js`): nothing is written before the first read, or a page that booted into
|
|
12
12
|
* defaults would overwrite everything the user configured.
|
|
13
13
|
*
|
|
14
|
+
* AND THE LANGUAGE IS THE SECOND THING THIS PAGE OWNS. The two settings pages pick theirs when they boot, and so
|
|
15
|
+
* does the guide; this page can be sitting open while the user changes 界面语言 in 通用设置, because the setting
|
|
16
|
+
* lives in the same store the page already polls. So every snapshot re-resolves it (`resolveLanguage`), the
|
|
17
|
+
* page's own lines are written in it (`t`), and the mounted component is RE-LABELLED rather than rebuilt — see
|
|
18
|
+
* `applyLanguage` for why that is not just an optimisation.
|
|
19
|
+
*
|
|
14
20
|
* For the browser probe and the launcher's self-test: `window.__VRMPET3D_PICKER_STATE__`.
|
|
15
21
|
*/
|
|
16
22
|
(function () {
|
|
@@ -19,6 +25,46 @@
|
|
|
19
25
|
var core = globalThis.__VRMPET3D_CORE__;
|
|
20
26
|
var pickerApi = globalThis.__VRMPET3D_MODEL_PICKER__;
|
|
21
27
|
|
|
28
|
+
/**
|
|
29
|
+
* This page's own words, in both languages — `{ zh, en }`, the shape `core.textFor` and `core.applyI18n` take.
|
|
30
|
+
*
|
|
31
|
+
* The first five are the ones the MARKUP also carries: `lib/picker-page.html` marks its title, eyebrow, heading,
|
|
32
|
+
* status placeholder and section heading with `data-i18n`, and keeps the Chinese inline, so a page whose script
|
|
33
|
+
* never ran still says something in one language rather than showing nothing. The rest exist only here, because
|
|
34
|
+
* they are the lines THIS SCRIPT writes (the six status/error lines), and there is no markup to fall back to.
|
|
35
|
+
*
|
|
36
|
+
* THE TWO HALVES MUST HOLD THE SAME KEYS: a key the English half lacks falls back to Chinese mid-page
|
|
37
|
+
* (`core.textFor`), which is the half-translated interface this table exists to prevent.
|
|
38
|
+
*/
|
|
39
|
+
var TEXT = {
|
|
40
|
+
zh: {
|
|
41
|
+
title: 'vrmpet3d · 切换模型',
|
|
42
|
+
eyebrow: 'VRMPET3D · MODELS',
|
|
43
|
+
heading: '切换模型',
|
|
44
|
+
statusLoading: '正在读取…',
|
|
45
|
+
sectionModels: '模型',
|
|
46
|
+
reading: '正在读取模型列表…',
|
|
47
|
+
settingsRead: '已读取当前设置。',
|
|
48
|
+
noHostSync: '这个宿主没有提供设置接口,切换只在本页生效。',
|
|
49
|
+
noCoreSettings: '这个版本的 core 没有设置接口,页面只能展示默认值。',
|
|
50
|
+
missingPicker: '页面脚本缺失:model-picker.js 没有加载。',
|
|
51
|
+
missingMount: '页面结构缺失:找不到 #picker-mount。',
|
|
52
|
+
},
|
|
53
|
+
en: {
|
|
54
|
+
title: 'vrmpet3d · Switch model',
|
|
55
|
+
eyebrow: 'VRMPET3D · MODELS',
|
|
56
|
+
heading: 'Switch model',
|
|
57
|
+
statusLoading: 'Reading…',
|
|
58
|
+
sectionModels: 'Model',
|
|
59
|
+
reading: 'Reading the model list…',
|
|
60
|
+
settingsRead: 'Settings loaded.',
|
|
61
|
+
noHostSync: 'This host has no settings API, so a switch only affects this page.',
|
|
62
|
+
noCoreSettings: 'This build of core has no settings API, so the page can only show defaults.',
|
|
63
|
+
missingPicker: 'Page script missing: model-picker.js did not load.',
|
|
64
|
+
missingMount: 'Page markup missing: #picker-mount was not found.',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
22
68
|
/** The first host snapshot is the permission to write. */
|
|
23
69
|
var hasSnapshot = false;
|
|
24
70
|
var preferences = core ? core.cleanPreferences({}) : {};
|
|
@@ -26,14 +72,67 @@
|
|
|
26
72
|
var picker = null;
|
|
27
73
|
/** Which model a SURFACE reports it is showing (`snapshot.live`), which is what ends the progress bar. */
|
|
28
74
|
var liveModel = null;
|
|
75
|
+
/**
|
|
76
|
+
* The language this page draws in — `'zh'` or `'en'`, never `'auto'` (see `resolveLanguage`).
|
|
77
|
+
*
|
|
78
|
+
* `null` until the first resolution, which is what makes the boot pass the one that fills the markup in.
|
|
79
|
+
*/
|
|
80
|
+
var language = null;
|
|
29
81
|
|
|
30
82
|
function el(id) { return document.getElementById(id); }
|
|
31
83
|
|
|
32
|
-
|
|
84
|
+
/**
|
|
85
|
+
* The line on `#status` right now: `{key}` when it is one of THIS page's own, `{text}` when the picker or the
|
|
86
|
+
* bootstrap watchdog wrote it, and `null` before anything has been written.
|
|
87
|
+
*
|
|
88
|
+
* The distinction is what a language change needs. One of this page's lines can be said again in the new
|
|
89
|
+
* language because it is stored as a KEY; a finished sentence from the component cannot be re-derived, so it is
|
|
90
|
+
* left exactly as it is rather than replaced by a translation of the placeholder — which would read as "reading
|
|
91
|
+
* the model list" while a model is in fact still loading.
|
|
92
|
+
*/
|
|
93
|
+
var statusLine = null;
|
|
94
|
+
/** What this page last painted, so a line written by somebody ELSE (the watchdog) can be told apart from it. */
|
|
95
|
+
var paintedStatus = null;
|
|
96
|
+
|
|
97
|
+
function paintStatus() {
|
|
33
98
|
var node = el('status');
|
|
34
|
-
if (!node) return;
|
|
35
|
-
node.textContent =
|
|
36
|
-
node.className = kind === 'warn' ? 'warn' : '';
|
|
99
|
+
if (!node || !statusLine) return;
|
|
100
|
+
node.textContent = statusLine.key ? t(statusLine.key) : statusLine.text;
|
|
101
|
+
node.className = statusLine.kind === 'warn' ? 'warn' : '';
|
|
102
|
+
paintedStatus = node.textContent;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A line this page wrote, as the key it came from — so a later language change can write it again. */
|
|
106
|
+
function setStatusKey(key, kind) {
|
|
107
|
+
statusLine = { key: key, kind: kind };
|
|
108
|
+
paintStatus();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A finished line, from the picker's `onStatus`. */
|
|
112
|
+
function setStatus(message, kind) {
|
|
113
|
+
statusLine = { text: message || '', kind: kind };
|
|
114
|
+
paintStatus();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The markup's own placeholder — the one line in `#status` that a translation is allowed to replace. */
|
|
118
|
+
function isPlaceholder(text) {
|
|
119
|
+
return text === TEXT.zh.statusLoading || text === TEXT.en.statusLoading;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* One of THIS page's own lines, in the language on screen.
|
|
124
|
+
*
|
|
125
|
+
* `core.textFor` when core is there — so the lookup rule (and the fallback to the other language for a gap) is
|
|
126
|
+
* the shared one rather than a second implementation of it — and the table itself when core is old enough not to
|
|
127
|
+
* have it. A key with no entry shows as the KEY rather than as a blank line, which is how a missing entry gets
|
|
128
|
+
* reported instead of silently disappearing.
|
|
129
|
+
*/
|
|
130
|
+
function t(key) {
|
|
131
|
+
var words = core && typeof core.textFor === 'function'
|
|
132
|
+
? core.textFor(TEXT, language)
|
|
133
|
+
: (language === 'en' ? TEXT.en : TEXT.zh);
|
|
134
|
+
var value = words[key];
|
|
135
|
+
return typeof value === 'string' ? value : key;
|
|
37
136
|
}
|
|
38
137
|
|
|
39
138
|
/**
|
|
@@ -57,10 +156,80 @@
|
|
|
57
156
|
return typeof preferences.modelId === 'string' ? preferences.modelId : '';
|
|
58
157
|
}
|
|
59
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Which language this page draws in, from the USER's setting and this surface's own environment.
|
|
161
|
+
*
|
|
162
|
+
* `preferences.language` is the store's answer — `'auto' | 'zh' | 'en'` (`core.LANGUAGE_SETTINGS`), which
|
|
163
|
+
* `core.cleanPreferences` passes through — so a choice made in 通用设置, in the guide or by another window
|
|
164
|
+
* reaches this page on its next snapshot. `'auto'` falls to `core.browserLanguageHints()`, which asks the
|
|
165
|
+
* browser BEFORE the document's own `lang`: every page in this plugin ships `<html lang="zh">`, so a page that
|
|
166
|
+
* read its own markup first could never auto-detect English.
|
|
167
|
+
*
|
|
168
|
+
* A core too old to know about languages keeps this page's own shipped answer, `'zh'` — the value this page
|
|
169
|
+
* passed unconditionally before it could resolve one, so an old host degrades to the old behaviour rather than
|
|
170
|
+
* failing to boot.
|
|
171
|
+
*/
|
|
172
|
+
function resolveLanguage() {
|
|
173
|
+
if (core && typeof core.resolveLanguage === 'function') {
|
|
174
|
+
var hints = typeof core.browserLanguageHints === 'function' ? core.browserLanguageHints() : null;
|
|
175
|
+
return core.resolveLanguage(preferences.language, hints);
|
|
176
|
+
}
|
|
177
|
+
return 'zh';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Put the resolved language everywhere it is needed: the marked-up text, `html lang`, this page's own lines
|
|
182
|
+
* (through `t` at the point of use) and the mounted component.
|
|
183
|
+
*
|
|
184
|
+
* The component is RE-LABELLED, not rebuilt, and that is a real distinction rather than tidiness: a rebuild
|
|
185
|
+
* throws away a switch's progress bar, the tile it is pending on and the catalogue the grid already has — and a
|
|
186
|
+
* language change is exactly when a switch may be in flight, because the user has just come back from 通用设置
|
|
187
|
+
* to look at the window. `setLanguage` is the component's documented path for this; a component old enough not
|
|
188
|
+
* to have it keeps the language it was built with rather than being destroyed, so a language change can never
|
|
189
|
+
* cost the user the click they just made.
|
|
190
|
+
*
|
|
191
|
+
* The bootstrap watchdog writes into `#status` too, and it is the FIRST script on the page — it reports a
|
|
192
|
+
* core.js that threw before this script ran. `core.applyI18n` fills every marked node including that one, so a
|
|
193
|
+
* line the watchdog has already written is put back; the placeholder the markup ships is the only text the
|
|
194
|
+
* translation is allowed to replace.
|
|
195
|
+
*
|
|
196
|
+
* @returns {boolean} whether the language changed — false on the 700 ms polls that change nothing
|
|
197
|
+
*/
|
|
198
|
+
function applyLanguage() {
|
|
199
|
+
var next = resolveLanguage();
|
|
200
|
+
if (next === language) return false;
|
|
201
|
+
language = next;
|
|
202
|
+
/**
|
|
203
|
+
* What is on the status line right now, if this page did NOT put it there.
|
|
204
|
+
*
|
|
205
|
+
* The bootstrap watchdog is the first script on the page and it writes into `#status` itself — it reports a
|
|
206
|
+
* core.js that threw while this script was still loading. `core.applyI18n` fills every marked node, that one
|
|
207
|
+
* included, so a line written by somebody else is captured here and put back: the placeholder the markup ships
|
|
208
|
+
* is the only text a translation may replace.
|
|
209
|
+
*/
|
|
210
|
+
var status = el('status');
|
|
211
|
+
var foreign = status && !isPlaceholder(status.textContent) && status.textContent !== paintedStatus
|
|
212
|
+
? status.textContent
|
|
213
|
+
: null;
|
|
214
|
+
if (core && typeof core.applyI18n === 'function') core.applyI18n(document, TEXT, language);
|
|
215
|
+
if (foreign !== null) {
|
|
216
|
+
status.textContent = foreign;
|
|
217
|
+
status.className = 'warn';
|
|
218
|
+
} else {
|
|
219
|
+
// One of this page's own lines — or the placeholder, which `applyI18n` has just translated.
|
|
220
|
+
paintStatus();
|
|
221
|
+
}
|
|
222
|
+
// The document says which language it is in, which is what a screen reader, a spell checker and a hyphenator
|
|
223
|
+
// read it for — and it is NOT the hint this page resolves from (that one was read before this line).
|
|
224
|
+
if (document.documentElement) document.documentElement.lang = language;
|
|
225
|
+
if (picker && typeof picker.setLanguage === 'function') picker.setLanguage(language);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
60
229
|
function patchPreferences(patch) {
|
|
61
230
|
Object.keys(patch).forEach(function (key) { preferences[key] = patch[key]; });
|
|
62
231
|
if (!hasSnapshot || !hostSync || typeof hostSync.push !== 'function') {
|
|
63
|
-
|
|
232
|
+
setStatusKey('noHostSync', 'warn');
|
|
64
233
|
return;
|
|
65
234
|
}
|
|
66
235
|
hostSync.push({ preferences: patch });
|
|
@@ -79,7 +248,7 @@
|
|
|
79
248
|
|
|
80
249
|
function watchSettings() {
|
|
81
250
|
if (!core || typeof core.watchHostSettings !== 'function') {
|
|
82
|
-
|
|
251
|
+
setStatusKey('noCoreSettings', 'warn');
|
|
83
252
|
return;
|
|
84
253
|
}
|
|
85
254
|
hostSync = core.watchHostSettings({
|
|
@@ -96,6 +265,10 @@
|
|
|
96
265
|
onSnapshot: function (snapshot) {
|
|
97
266
|
preferences = core.cleanPreferences(snapshot.preferences || {});
|
|
98
267
|
hasSnapshot = true;
|
|
268
|
+
// The store's 界面语言 may have moved while this window was open (another window, or the guide), so the
|
|
269
|
+
// language is re-resolved BEFORE the lines below are written: a status line written first would be the one
|
|
270
|
+
// thing left in the old language.
|
|
271
|
+
applyLanguage();
|
|
99
272
|
// Which model a pet reports it is SHOWING: the progress bar waits for this, not merely for the write.
|
|
100
273
|
liveModel = liveModelIdOf(snapshot);
|
|
101
274
|
// The value that wins is the STORE's, so a switch made from another window repaints this grid — and
|
|
@@ -103,24 +276,31 @@
|
|
|
103
276
|
// what asks whether an in-flight switch has finished.
|
|
104
277
|
if (picker) picker.setCurrent();
|
|
105
278
|
// A snapshot that COMPLETES a switch must not have its line overwritten by this one.
|
|
106
|
-
if (!(picker && picker.state().switching))
|
|
279
|
+
if (!(picker && picker.state().switching)) setStatusKey('settingsRead');
|
|
107
280
|
},
|
|
108
281
|
}).start();
|
|
109
282
|
}
|
|
110
283
|
|
|
111
284
|
function boot() {
|
|
112
285
|
var mount = el('picker-mount');
|
|
286
|
+
// The language FIRST, so that even the failure lines below are in the language the user asked for. The store
|
|
287
|
+
// has not answered yet, so this resolves `'auto'` from the browser; the first snapshot resolves it again and
|
|
288
|
+
// re-labels if the store says otherwise.
|
|
289
|
+
applyLanguage();
|
|
113
290
|
if (!pickerApi || typeof pickerApi.createModelPicker !== 'function') {
|
|
114
|
-
|
|
291
|
+
setStatusKey('missingPicker', 'warn');
|
|
115
292
|
return;
|
|
116
293
|
}
|
|
117
294
|
if (!mount) {
|
|
118
|
-
|
|
295
|
+
setStatusKey('missingMount', 'warn');
|
|
119
296
|
return;
|
|
120
297
|
}
|
|
121
298
|
picker = pickerApi.createModelPicker({
|
|
122
299
|
mount: mount,
|
|
123
|
-
language:
|
|
300
|
+
// The RESOLVED language, not the store's preference: the component owns two tables and no opinion about
|
|
301
|
+
// 'auto' (see `resolveText` in lib/model-picker.js). A change after this point goes through `setLanguage`,
|
|
302
|
+
// which is what `applyLanguage` calls.
|
|
303
|
+
language: language,
|
|
124
304
|
currentModelId: modelId,
|
|
125
305
|
// The live report is what makes the bar about the LOAD rather than about the write. See `liveModelIdOf`.
|
|
126
306
|
liveModelId: function () { return liveModel; },
|
|
@@ -128,7 +308,7 @@
|
|
|
128
308
|
onStatus: setStatus,
|
|
129
309
|
});
|
|
130
310
|
watchSettings();
|
|
131
|
-
|
|
311
|
+
setStatusKey('reading');
|
|
132
312
|
picker.refresh();
|
|
133
313
|
}
|
|
134
314
|
|
|
@@ -139,6 +319,7 @@
|
|
|
139
319
|
return {
|
|
140
320
|
hasSnapshot: hasSnapshot,
|
|
141
321
|
current: modelId(),
|
|
322
|
+
language: language,
|
|
142
323
|
picker: picker ? picker.state() : null,
|
|
143
324
|
};
|
|
144
325
|
};
|