@@ -13,15 +13,84 @@ const pluginTitle = '$:/plugins/oeyoews/neotw-image-upload';
1313const DEFAULT_API_BASE = 'http://localhost:8096' ;
1414const FORMAT_TIDDLER = `${ pluginTitle } /format` ;
1515const 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
1785const 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 `` ;
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 = `` ;
@@ -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