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 CHANGED
@@ -42,6 +42,7 @@ no runtime dependencies · works offline
42
42
  | 互动 | 点击与长按部位触发表情和碎碎念;拖动、缩放、旋转视角、视线跟随和弹簧骨物理 |
43
43
  | 碎碎念 | 内置台词、自定义台词,或使用 DSH 模型 / OpenAI 兼容接口生成 AI 台词 |
44
44
  | 导入 | 模型以文件导入、动作包以文件夹导入,复制到插件存储后不依赖原路径 |
45
+ | 界面语言 | 全部界面中英双语,默认跟随系统 / DSH 自动识别;「通用设置 → 显示与外观」和初次引导欢迎页各有一个语言选择(自动 / 中文 / English) |
45
46
 
46
47
  ## 运行要求
47
48
 
@@ -155,6 +156,7 @@ The marketplace screenshot order is declared in the root [screenshots.json](scre
155
156
  | Interaction | Click or hold body parts for expressions and chatter; drag, resize, rotate the view, follow the pointer and use spring-bone physics |
156
157
  | Chatter | Built-in or custom lines, or AI-generated lines using a DSH model or an OpenAI-compatible service |
157
158
  | Import | Import models as files and motion packs as folders. Imported files are copied into plugin storage and no longer depend on their original paths |
159
+ | Language | The whole interface is Chinese and English, auto-detected from the system or DSH by default; a chooser (Auto / 中文 / English) sits in General settings → Display, and on the first-run guide's welcome screen |
158
160
 
159
161
  ### Requirements
160
162
 
package/desktop/main.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // Everything here was measured before it was written (see HANDOFF §12): a transparent
9
9
  // always-on-top window renders WebGL with a real alpha channel, can be resized to the whole
10
10
  // screen, and `capturePage()` returns pixels that are mostly fully transparent.
11
- const { app, BrowserWindow, dialog, ipcMain, screen } = require('electron');
11
+ const { app, BrowserWindow, dialog, ipcMain, screen, shell } = require('electron');
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
14
 
@@ -37,6 +37,47 @@ const DEFAULT_PET_WINDOW = { width: 240, height: 320 };
37
37
  */
38
38
  const VRMPET3D_STATUS_PREFIX = 'vrmpet3d-desktop: ';
39
39
 
40
+ /**
41
+ * The words THIS process puts on screen itself: the two native dialog titles and the model filter's name.
42
+ *
43
+ * Nothing else here is a string a user reads — every other sentence in this file is a log line or a self-test
44
+ * report — and these three are the exception because Electron, not a page, draws them. The window title is
45
+ * deliberately NOT in this table: it names the launcher's own window (`vrmpet3d - model settings`) and is the same
46
+ * ASCII wherever the user is, so a translated one would add a second name for a window the user finds by that one.
47
+ *
48
+ * The launcher is a SEPARATE PROCESS with no access to `lib/core.js`, so it keeps its own copy of the repository's
49
+ * two-language shape (`{ zh: {...}, en: {...} }`, identical key sets) rather than importing the host's rules.
50
+ */
51
+ const DIALOG_TEXT = {
52
+ zh: { motionTitle: '导入动作目录', modelTitle: '导入模型文件', modelFilter: 'VRM / glTF 模型' },
53
+ en: { motionTitle: 'Import motion folder', modelTitle: 'Import model file', modelFilter: 'VRM / glTF model' },
54
+ };
55
+
56
+ /**
57
+ * Which of the two this process draws in, for one request.
58
+ *
59
+ * The PAGE wins whenever it says something this process understands. The settings page knows the user's
60
+ * `preferences.language`, which may be a deliberate 中文 on an English machine or the reverse, so it is the better
61
+ * authority when it speaks; it may pass that as `{ kind, language }`, and `'auto'` (or nothing at all) is not a
62
+ * language, so the fallback is `app.getLocale()` — the machine's own locale, which is exactly what `'auto'` means
63
+ * for a surface with no DSH locale to consult.
64
+ *
65
+ * The field is OPTIONAL and nothing here requires it: an older page that sends only `{ kind }` (without a language argument) gets the locale, and a page that sends a value neither of the two languages
66
+ * ignores it rather than having it guessed at.
67
+ */
68
+ function dialogLanguage(options) {
69
+ const wanted = options && typeof options === 'object' ? options.language : null;
70
+ if (wanted === 'zh' || wanted === 'en') return wanted;
71
+ let locale = '';
72
+ try {
73
+ // After `ready`, which every handler here is; a locale that cannot be read is not a reason to fail a dialog.
74
+ locale = app.getLocale() || '';
75
+ } catch (_error) {
76
+ locale = '';
77
+ }
78
+ return /^(zh|cn|chinese)/i.test(String(locale).trim()) ? 'zh' : 'en';
79
+ }
80
+
40
81
  /**
41
82
  * The payload the page expects for a cursor sample.
42
83
  *
@@ -563,6 +604,7 @@ app.whenReady().then(async () => {
563
604
  if (!probing) sendCursor(lastCursor);
564
605
  };
565
606
 
607
+ ipcMain.on('vrmpet3d:focus-host', () => { shell.openExternal('dsh://open').catch(() => {}); });
566
608
  ipcMain.handle('vrmpet3d:config', () => ({
567
609
  modelId: options.modelId,
568
610
  assetBase: '/vrmpet3d/asset',
@@ -590,15 +632,16 @@ app.whenReady().then(async () => {
590
632
  */
591
633
  ipcMain.handle('vrmpet3d:pick-path', async (_event, options) => {
592
634
  const kind = options && typeof options === 'object' && options.kind === 'motion' ? 'motion' : 'model';
635
+ const text = DIALOG_TEXT[dialogLanguage(options)] || DIALOG_TEXT.en;
593
636
  try {
594
637
  const result = await dialog.showOpenDialog(win, kind === 'motion' ? {
595
- title: '导入动作目录',
638
+ title: text.motionTitle,
596
639
  properties: ['openDirectory'],
597
640
  defaultPath: app.getPath('home'),
598
641
  } : {
599
- title: '导入模型文件',
642
+ title: text.modelTitle,
600
643
  properties: ['openFile'],
601
- filters: [{ name: 'VRM / glTF 模型', extensions: ['vrm', 'glb', 'gltf'] }],
644
+ filters: [{ name: text.modelFilter, extensions: ['vrm', 'glb', 'gltf'] }],
602
645
  defaultPath: app.getPath('home'),
603
646
  });
604
647
  if (result.canceled || !Array.isArray(result.filePaths) || !result.filePaths.length) return null;
@@ -13,6 +13,7 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
13
13
  * Sent as an invoke so the page can await it before mounting anything.
14
14
  */
15
15
  config: () => ipcRenderer.invoke('vrmpet3d:config'),
16
+ focusHost: () => ipcRenderer.send('vrmpet3d:focus-host'),
16
17
 
17
18
  /** The global cursor position, pushed by the main process at frame rate. */
18
19
  onCursor: (callback) => {
@@ -39,7 +40,9 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
39
40
  * plugin's own storage. The host stays the only writer of that list, and the `VRMPET3D_MODEL_FILES_FILE` /
40
41
  * `VRMPET3D_IMPORT_DIR` overrides the suites rely on are not bypassed.
41
42
  */
42
- pickModelFile: () => ipcRenderer.invoke('vrmpet3d:pick-path', { kind: 'model' }),
43
+ pickModelFile: (language) => ipcRenderer.invoke('vrmpet3d:pick-path', {
44
+ kind: 'model', language: language === 'zh' || language === 'en' ? language : undefined,
45
+ }),
43
46
 
44
47
  /**
45
48
  * The same dialog for an imported MOTION folder (`/vrmpet3d/motions`), which stays a DIRECTORY: a motion pack is
@@ -47,7 +50,9 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
47
50
  *
48
51
  * One channel for both, with `kind` deciding a FILE dialog from a folder one and the title each shows.
49
52
  */
50
- pickMotionDir: () => ipcRenderer.invoke('vrmpet3d:pick-path', { kind: 'motion' }),
53
+ pickMotionDir: (language) => ipcRenderer.invoke('vrmpet3d:pick-path', {
54
+ kind: 'motion', language: language === 'zh' || language === 'en' ? language : undefined,
55
+ }),
51
56
 
52
57
  /**
53
58
  * Tell the launcher that a VIEW ORBIT is starting or ending — and let IT take the pointer lock.