Skip to content

Commit 9fafffb

Browse files
committed
feat: neotw-image-upload support i18n
1 parent 68f8f4d commit 9fafffb

File tree

4 files changed

+294
-115
lines changed

4 files changed

+294
-115
lines changed

plugins/oeyoews/neotw-image-upload/files/app.js

Lines changed: 178 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,84 @@ const pluginTitle = '$:/plugins/oeyoews/neotw-image-upload';
1313
const DEFAULT_API_BASE = 'http://localhost:8096';
1414
const FORMAT_TIDDLER = `${pluginTitle}/format`;
1515
const API_BASE_TIDDLER = `${pluginTitle}/api-base`;
16+
const LANG_TIDDLER = `${pluginTitle}/lang`;
17+
18+
const i18n = {
19+
en: {
20+
uploadTitle: 'Image Upload',
21+
dropHint: 'Click, drag or Ctrl+V to paste image here',
22+
size: 'Size',
23+
fileName: 'File name',
24+
fileNamePlaceholder: 'Filename (optional extension)',
25+
pasteHint: 'This area supports Ctrl+V to paste images',
26+
imageList: 'Image list',
27+
hideList: 'Hide list',
28+
upload: 'Upload',
29+
uploading: 'Uploading...',
30+
link: 'Link',
31+
copy: 'Copy',
32+
copied: 'Copied',
33+
refresh: 'Refresh',
34+
loading: 'Loading...',
35+
storage: 'Storage',
36+
config: 'Config',
37+
copyStoragePath: 'Copy storage path',
38+
copyConfigPath: 'Copy config path',
39+
noImages: 'No images',
40+
noImagesHint: 'Drag image in the upload area above or use Ctrl+V to paste',
41+
open: 'Open',
42+
copyLink: 'Copy link',
43+
delete: 'Delete',
44+
showImageList: 'Show image list',
45+
hideImageList: 'Hide image list',
46+
preview: 'Preview',
47+
deleteConfirm: 'Delete this image?',
48+
deleteFail: 'Delete failed',
49+
loadFail: 'Load failed',
50+
},
51+
zh: {
52+
uploadTitle: '图片上传',
53+
dropHint: '点击、拖拽或 Ctrl+V 粘贴图片到此处',
54+
size: '大小',
55+
fileName: '文件名称',
56+
fileNamePlaceholder: '文件名(可含扩展名)',
57+
pasteHint: '本区域支持 Ctrl+V 粘贴图片',
58+
imageList: '图片列表',
59+
hideList: '隐藏列表',
60+
upload: '上传',
61+
uploading: '上传中...',
62+
link: '链接',
63+
copy: '复制',
64+
copied: '已复制',
65+
refresh: '刷新',
66+
loading: '加载中...',
67+
storage: '存储',
68+
config: '配置',
69+
copyStoragePath: '复制存储路径',
70+
copyConfigPath: '复制配置路径',
71+
noImages: '暂无图片',
72+
noImagesHint: '可在上方上传区拖拽图片,或使用 Ctrl+V 粘贴上传',
73+
open: '打开',
74+
copyLink: '复制链接',
75+
delete: '删除',
76+
showImageList: '显示图片列表',
77+
hideImageList: '隐藏图片列表',
78+
preview: '预览',
79+
deleteConfirm: '确定删除这张图片吗?',
80+
deleteFail: '删除失败',
81+
loadFail: '加载失败',
82+
},
83+
};
1684

1785
const app = () => {
1886
const component = {
1987
template: getTemplate(`${pluginTitle}/templates/app.vue`),
2088
data() {
2189
return {
90+
lang: 'en',
2291
file: null,
2392
previewUrl: '',
24-
hintText: '点击或拖拽图片到此处',
93+
hintText: 'Click, drag or paste image here',
2594
selectedFileName: '',
2695
uploadFileName: '',
2796
isDragover: false,
@@ -37,24 +106,31 @@ const app = () => {
37106
imagesError: '',
38107
dateFilter: '',
39108
showImageList: false,
109+
storagePath: '',
110+
configPath: '',
111+
pathCopied: '', // 'storage' | 'config' | ''
40112
};
41113
},
42114
mounted() {
43115
const today = new Date().toISOString().slice(0, 10);
44116
this.dateFilter = today;
45117
this.loadFormatPreference();
46118
this.loadApiBasePreference();
119+
this.loadLangPreference();
47120
this.loadImages();
121+
this.loadServerInfo();
48122
},
49123

50124
computed: {
125+
t() {
126+
return i18n[this.lang] || i18n.en;
127+
},
51128
markdownSnippet() {
52129
if (!this.resultUrl) return '';
53130
return `![](${this.resultUrl})`;
54131
},
55132
wikitextSnippet() {
56133
if (!this.resultUrl) return '';
57-
// TiddlyWiki 图片语法:[img[alt|url]]
58134
return `[img[image|${this.resultUrl}]]`;
59135
},
60136
vanillaSnippet() {
@@ -63,9 +139,16 @@ const app = () => {
63139
formatLabel() {
64140
if (this.selectedFormat === 'md') return 'Markdown';
65141
if (this.selectedFormat === 'tw') return 'Wikitext';
66-
if (this.selectedFormat === 'link') return '链接';
142+
if (this.selectedFormat === 'link') return this.t.link;
67143
return '';
68144
},
145+
/** 根据当前复制格式动态显示的链接内容 */
146+
displaySnippet() {
147+
if (!this.resultUrl) return '';
148+
if (this.selectedFormat === 'md') return this.markdownSnippet;
149+
if (this.selectedFormat === 'tw') return this.wikitextSnippet;
150+
return this.vanillaSnippet;
151+
},
69152
},
70153

71154
beforeUnmount() {
@@ -83,7 +166,7 @@ const app = () => {
83166
this.previewUrl = '';
84167
this.selectedFileName = '';
85168
this.uploadFileName = '';
86-
this.hintText = '点击或拖拽图片到此处';
169+
this.hintText = this.t.dropHint;
87170
},
88171

89172
/** 上传时使用的文件名:用户可改,扩展名与原文件一致 */
@@ -207,6 +290,19 @@ const app = () => {
207290
}
208291
},
209292

293+
async loadServerInfo() {
294+
try {
295+
const res = await fetch(`${this.apiBase}/info`);
296+
if (!res.ok) return;
297+
const data = await res.json();
298+
this.storagePath = data.upload_dir || '';
299+
this.configPath = data.config_file || '';
300+
} catch (e) {
301+
this.storagePath = '';
302+
this.configPath = '';
303+
}
304+
},
305+
210306
async loadImages() {
211307
this.imagesLoading = true;
212308
this.imagesError = '';
@@ -221,14 +317,14 @@ const app = () => {
221317
try {
222318
const res = await fetch(url);
223319
if (!res.ok) {
224-
this.imagesError = '加载失败:' + res.status;
320+
this.imagesError = this.t.loadFail + ': ' + res.status;
225321
this.imageGroups = [];
226322
return;
227323
}
228324
const data = await res.json();
229325
this.imageGroups = Array.isArray(data) ? data : [];
230326
} catch (e) {
231-
this.imagesError = '加载失败:' + e.message;
327+
this.imagesError = this.t.loadFail + ': ' + e.message;
232328
this.imageGroups = [];
233329
} finally {
234330
this.imagesLoading = false;
@@ -286,33 +382,88 @@ const app = () => {
286382
}
287383
},
288384

289-
async copySnippet(kind) {
290-
if (!this.resultUrl || !navigator.clipboard) return;
385+
loadLangPreference() {
386+
try {
387+
if (typeof $tw === 'undefined' || !$tw.wiki || !$tw.wiki.getTiddlerText) return;
388+
const value = $tw.wiki.getTiddlerText(LANG_TIDDLER);
389+
if (value === 'en' || value === 'zh') {
390+
this.lang = value;
391+
}
392+
} catch (e) {
393+
// 忽略
394+
}
395+
},
396+
397+
saveLangPreference() {
398+
try {
399+
if (typeof $tw === 'undefined' || !$tw.wiki || !$tw.wiki.setText) return;
400+
$tw.wiki.setText(LANG_TIDDLER, 'text', undefined, this.lang);
401+
} catch (e) {
402+
// 忽略
403+
}
404+
},
405+
406+
setLang(l) {
407+
this.lang = l;
408+
this.saveLangPreference();
409+
},
291410

411+
/** 复制到剪贴板:优先 Clipboard API,失败时回退到 execCommand(适配 file:// 等非安全上下文) */
412+
async copyToClipboard(text) {
413+
if (!text || typeof text !== 'string') return false;
414+
try {
415+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
416+
await navigator.clipboard.writeText(text);
417+
return true;
418+
}
419+
} catch (e) {
420+
// 非安全上下文或权限被拒时继续尝试 fallback
421+
}
422+
const textarea = document.createElement('textarea');
423+
textarea.value = text;
424+
textarea.style.position = 'fixed';
425+
textarea.style.left = '-9999px';
426+
textarea.style.top = '0';
427+
textarea.setAttribute('readonly', '');
428+
document.body.appendChild(textarea);
429+
textarea.select();
430+
textarea.setSelectionRange(0, text.length);
431+
let ok = false;
432+
try {
433+
ok = document.execCommand('copy');
434+
} catch (e) {
435+
console.error('复制失败', e);
436+
}
437+
document.body.removeChild(textarea);
438+
return ok;
439+
},
440+
441+
async copySnippet(kind) {
442+
if (!this.resultUrl) return;
292443
let text = '';
293444
if (kind === 'md') text = this.markdownSnippet;
294445
else if (kind === 'tw') text = this.wikitextSnippet;
295446
else if (kind === 'link') text = this.vanillaSnippet;
296-
297447
if (!text) return;
298-
299-
try {
300-
await navigator.clipboard.writeText(text);
448+
const ok = await this.copyToClipboard(text);
449+
if (ok) {
301450
this.resultCopied = kind;
302-
setTimeout(() => {
303-
this.resultCopied = '';
304-
}, 1500);
305-
} catch (e) {
306-
console.error('复制失败', e);
451+
setTimeout(() => { this.resultCopied = ''; }, 1500);
452+
}
453+
},
454+
455+
async copyPath(path, kind) {
456+
if (!path) return;
457+
const ok = await this.copyToClipboard(path);
458+
if (ok) {
459+
this.pathCopied = kind;
460+
setTimeout(() => { this.pathCopied = ''; }, 1500);
307461
}
308462
},
309463

310464
async copyLink(img) {
311465
const url = img && img.url;
312-
if (!url || !navigator.clipboard) {
313-
return;
314-
}
315-
// 根据当前下拉选择的格式生成文本
466+
if (!url) return;
316467
let text = '';
317468
if (this.selectedFormat === 'md') {
318469
text = `![](${url})`;
@@ -321,23 +472,16 @@ const app = () => {
321472
} else {
322473
text = url;
323474
}
324-
325-
try {
326-
await navigator.clipboard.writeText(text);
327-
// mark copied on this item
475+
const ok = await this.copyToClipboard(text);
476+
if (ok) {
328477
img._copied = true;
329-
setTimeout(() => {
330-
img._copied = false;
331-
}, 1500);
332-
} catch (e) {
333-
// 复制失败时,不抛出错误,只在控制台记录
334-
console.error('复制失败', e);
478+
setTimeout(() => { img._copied = false; }, 1500);
335479
}
336480
},
337481

338482
async deleteImage(img) {
339483
if (!img || !img.path) return;
340-
const ok = window.confirm('确定删除这张图片吗?\n' + img.path);
484+
const ok = window.confirm(this.t.deleteConfirm + '\n' + img.path);
341485
if (!ok) return;
342486

343487
const url = `${this.apiBase}/images?path=${encodeURIComponent(img.path)}`;
@@ -351,12 +495,12 @@ const app = () => {
351495
// ignore
352496
}
353497
const message = data.error || res.status;
354-
window.alert('删除失败:' + message);
498+
window.alert(this.t.deleteFail + ': ' + message);
355499
return;
356500
}
357501
await this.loadImages();
358502
} catch (e) {
359-
window.alert('删除失败:' + e.message);
503+
window.alert(this.t.deleteFail + ': ' + e.message);
360504
}
361505
},
362506
},

0 commit comments

Comments
 (0)