From 59700dce59422ee5600e810ed6def0f92e5251bc Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 4 Mar 2026 21:18:03 +0800 Subject: [PATCH] =?UTF-8?q?-=20feat:=E6=96=B0=E5=A2=9E=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=AE=A1=E7=90=86=E5=92=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce an upload attachments panel with styles, localization and client-side management. Added new i18n keys (EN/ZH) and refactored js/i18n.js to helper setters for text/placeholder/title. popup.html and css/main.css add the upload-list UI and styles. oper.js implements rendering, drag-and-drop reorder, deletion (calls DELETE api with token), storage persistence (resourceIdList), escapeHtml sanitization, and sync change handling; it also fixes a config check (repo -> apiTokens), stores filename from upload responses, handles attachments/resources compatibility, and clears the list after sending a memo. Overall this enables reorderable, deletable uploaded attachments with proper localization and tooltips. --- _locales/en/messages.json | 63 +++++++++++-- _locales/zh_CN/messages.json | 48 ++++++++++ css/main.css | 65 +++++++++++++ js/i18n.js | 55 ++++++++--- js/oper.js | 177 +++++++++++++++++++++++++++++++++-- popup.html | 6 ++ 6 files changed, 384 insertions(+), 30 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 456fb77..413a9a2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -14,39 +14,54 @@ "sendLinkTo": { "message": "Send link to Memos" }, - "sendImageTo": { - "message": "Send image to Memos" - }, "saveBtn":{ "message": "Save" }, "placeApiUrl":{ - "message": "Memos Self Home Url" + "message": "Memos site URL" }, "placeApiTokens":{ "message": "Memos Access Tokens" }, "placeContent":{ - "message": "Any thoughts..." + "message": "What's on your mind..." }, "lockPrivate":{ - "message": "Only visible to you" + "message": "Private" }, "lockProtected":{ - "message": "Visible to members" + "message": "Protected" }, "lockPublic":{ - "message": "Everyone can see" + "message": "Public" }, "submitBtn":{ "message": "Save" }, "placeHideInput":{ - "message": "Default 'Only visible to you' Tag name" + "message": "Default 'Private' tag name" }, "placeShowInput":{ "message": "Default 'Everyone can see' Tag name" }, + "uploadedListTitle": { + "message": "Uploaded files, Drag to reorder" + }, + "uploadedListEmpty": { + "message": "No uploaded files" + }, + "tipReorder": { + "message": "Drag to reorder" + }, + "tipDeleteAttachment": { + "message": "Delete" + }, + "attachmentDeleteSuccess": { + "message": "Deleted" + }, + "attachmentDeleteFailed": { + "message": "Delete failed 😭" + }, "picDrag":{ "message": "Drag upload the image" }, @@ -94,5 +109,35 @@ }, "invalidToken":{ "message": "Invalid token or url 😭" + }, + "tipOpenSite": { + "message": "Open Memos" + }, + "tipSettings": { + "message": "Settings" + }, + "tipTags": { + "message": "Insert tag" + }, + "tipTodo": { + "message": "Insert todo" + }, + "tipUpload": { + "message": "Upload file" + }, + "tipLink": { + "message": "Insert current tab link" + }, + "tipRandom": { + "message": "Random memo" + }, + "tipSearch": { + "message": "Search" + }, + "tipVisibility": { + "message": "Visibility" + }, + "tipSend": { + "message": "Send (Ctrl/⌘+Enter)" } } \ No newline at end of file diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 360ee42..c7c8966 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -94,5 +94,53 @@ }, "invalidToken":{ "message": "无效的 token 或 url 😭" + }, + "uploadedListTitle": { + "message": "已上传文件,可拖动排序" + }, + "uploadedListEmpty": { + "message": "暂无已上传文件" + }, + "tipReorder": { + "message": "拖动排序" + }, + "tipDeleteAttachment": { + "message": "删除" + }, + "attachmentDeleteSuccess": { + "message": "删除成功" + }, + "attachmentDeleteFailed": { + "message": "删除失败 😭" + }, + "tipOpenSite": { + "message": "打开 Memos" + }, + "tipSettings": { + "message": "设置" + }, + "tipTags": { + "message": "插入标签" + }, + "tipTodo": { + "message": "插入待办" + }, + "tipUpload": { + "message": "上传文件" + }, + "tipLink": { + "message": "插入当前页面链接" + }, + "tipRandom": { + "message": "随机一条" + }, + "tipSearch": { + "message": "搜索" + }, + "tipVisibility": { + "message": "可见性" + }, + "tipSend": { + "message": "发送(Ctrl/⌘+Enter)" } } \ No newline at end of file diff --git a/css/main.css b/css/main.css index 88799c6..383d6ff 100644 --- a/css/main.css +++ b/css/main.css @@ -94,6 +94,71 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;} margin-top: 1rem; justify-content: space-between; } + +.upload-list-wrapper{ + margin-top: .5rem; +} +.upload-list-title{ + font-size: .875rem; + color: #999; + margin-top: .5rem; + margin-bottom: .25rem; +} +.upload-list{ + border: 1px solid rgb(229,231,235); + border-radius: .5rem; + background-color: rgb(255,255,255); + padding: .25rem; +} +.upload-empty{ + padding: .5rem .75rem; + font-size: .875rem; + color: #999; +} +.upload-item{ + display:flex; + align-items:center; + justify-content: space-between; + padding: .4rem .5rem; + border-radius: .25rem; + color:#666; +} +.upload-item + .upload-item{ + border-top: 1px solid rgb(243,244,246); +} +.upload-item.drag-over{ + background-color: rgb(243,244,246); +} +.upload-left{ + display:flex; + align-items:center; + min-width: 0; + gap: .5rem; +} +.upload-drag{ + cursor: grab; + opacity: .6; + user-select: none; +} +.upload-filename{ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .875rem; +} +.upload-del{ + cursor:pointer; + font-size: 1rem; + line-height: 1; + padding: .15rem .35rem; + border-radius: .25rem; + opacity: .6; + background-color: transparent; +} +.upload-del:hover{ + opacity: 1; + background-color: rgb(243,244,246); +} .common-tools-container { display: flex; flex-direction: row; diff --git a/js/i18n.js b/js/i18n.js index f90f2cb..00ff52b 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -1,15 +1,48 @@ -document.getElementById("saveKey").textContent = chrome.i18n.getMessage("saveBtn"); -document.getElementById("saveTag").textContent = chrome.i18n.getMessage("saveBtn"); +function getMessage(key) { + return chrome.i18n.getMessage(key) || '' +} -document.getElementById("apiUrl").placeholder = chrome.i18n.getMessage("placeApiUrl"); -document.getElementById("apiTokens").placeholder = chrome.i18n.getMessage("placeApiTokens"); -document.getElementById("content").placeholder = chrome.i18n.getMessage("placeContent"); +function setText(id, messageKey) { + const el = document.getElementById(id) + if (el) el.textContent = getMessage(messageKey) +} -document.getElementById("lockPrivate").textContent = chrome.i18n.getMessage("lockPrivate"); -document.getElementById("lockProtected").textContent = chrome.i18n.getMessage("lockProtected"); -document.getElementById("lockPublic").textContent = chrome.i18n.getMessage("lockPublic"); +function setPlaceholder(id, messageKey) { + const el = document.getElementById(id) + if (el) el.placeholder = getMessage(messageKey) +} -document.getElementById("content_submit_text").textContent = chrome.i18n.getMessage("submitBtn"); +function setTitle(id, messageKey) { + const el = document.getElementById(id) + if (el) el.title = getMessage(messageKey) +} -document.getElementById("hideInput").placeholder = chrome.i18n.getMessage("placeHideInput"); -document.getElementById("showInput").placeholder = chrome.i18n.getMessage("placeShowInput"); \ No newline at end of file +setText("saveKey", "saveBtn") +setText("saveTag", "saveBtn") + +setPlaceholder("apiUrl", "placeApiUrl") +setPlaceholder("apiTokens", "placeApiTokens") +setPlaceholder("content", "placeContent") + +setText("lockPrivate", "lockPrivate") +setText("lockProtected", "lockProtected") +setText("lockPublic", "lockPublic") + +setText("content_submit_text", "submitBtn") + +setPlaceholder("hideInput", "placeHideInput") +setPlaceholder("showInput", "placeShowInput") + +setText("uploadlist-title", "uploadedListTitle") + +// Native hover tooltips (title) +setTitle("opensite", "tipOpenSite") +setTitle("blog_info_edit", "tipSettings") +setTitle("tags", "tipTags") +setTitle("newtodo", "tipTodo") +setTitle("upres", "tipUpload") +setTitle("getlink", "tipLink") +setTitle("random", "tipRandom") +setTitle("search", "tipSearch") +setTitle("lock", "tipVisibility") +setTitle("content_submit_text", "tipSend") \ No newline at end of file diff --git a/js/oper.js b/js/oper.js index 79c2e44..fbeb0df 100644 --- a/js/oper.js +++ b/js/oper.js @@ -1,6 +1,8 @@ dayjs.extend(window.dayjs_plugin_relativeTime) dayjs.locale('zh-cn') +let relistNow = [] + function get_info(callback) { chrome.storage.sync.get( { @@ -17,7 +19,7 @@ function get_info(callback) { function (items) { var flag = false var returnObject = {} - if (items.apiUrl === '' || items.repo === '') { + if (items.apiUrl === '' || items.apiTokens === '') { flag = false } else { flag = true @@ -67,10 +69,22 @@ get_info(function (info) { } else { $("textarea[name=text]").val(info.open_content) } + + relistNow = Array.isArray(info.resourceIdList) ? info.resourceIdList : [] + renderUploadList(relistNow) //从localstorage 里面读取数据 setTimeout(get_info, 1) }) +chrome.storage.onChanged.addListener(function (changes, areaName) { + if (areaName !== 'sync') return + if (!changes.resourceIdList) return + relistNow = Array.isArray(changes.resourceIdList.newValue) + ? changes.resourceIdList.newValue + : [] + renderUploadList(relistNow) +}) + $("textarea[name=text]").focus() //监听输入结束,保存未发送内容到本地 @@ -140,7 +154,147 @@ function initDrag() { } } -let relistNow = [] +function escapeHtml(input) { + return String(input) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function renderUploadList(list) { + const $wrapper = $('.upload-list-wrapper') + const $list = $('#uploadlist') + if ($list.length === 0) return + + const items = Array.isArray(list) ? list : [] + if (items.length === 0) { + if ($wrapper.length) $wrapper.hide() + $list.html('') + return + } + + if ($wrapper.length) $wrapper.show() + + const tipReorder = escapeHtml(chrome.i18n.getMessage('tipReorder')) + const tipDelete = escapeHtml(chrome.i18n.getMessage('tipDeleteAttachment')) + + let html = '' + for (let i = 0; i < items.length; i++) { + const att = items[i] || {} + const name = att.name || '' + const filename = att.filename || name + html += + '
' + + '
' + + '' + + '' + + escapeHtml(filename) + + '' + + '
' + + '' + + '
' + } + + $list.html(html) +} + +function saveUploadList(nextList, callback) { + relistNow = Array.isArray(nextList) ? nextList : [] + chrome.storage.sync.set({ resourceIdList: relistNow }, callback) +} + +let uploadDragIndex = null + +$(document).on('dragstart', '.upload-item', function (e) { + uploadDragIndex = Number($(this).data('index')) + const dt = e.originalEvent && e.originalEvent.dataTransfer + if (dt) { + dt.effectAllowed = 'move' + dt.setData('text/plain', String(uploadDragIndex)) + } +}) + +$(document).on('dragover', '.upload-item', function (e) { + e.preventDefault() + $(this).addClass('drag-over') + const dt = e.originalEvent && e.originalEvent.dataTransfer + if (dt) dt.dropEffect = 'move' +}) + +$(document).on('dragleave', '.upload-item', function () { + $(this).removeClass('drag-over') +}) + +$(document).on('drop', '.upload-item', function (e) { + e.preventDefault() + $('.upload-item.drag-over').removeClass('drag-over') + + const fromIndex = + uploadDragIndex != null + ? uploadDragIndex + : Number( + (e.originalEvent && e.originalEvent.dataTransfer + ? e.originalEvent.dataTransfer.getData('text/plain') + : '') || -1 + ) + const toIndex = Number($(this).data('index')) + + uploadDragIndex = null + if (!Number.isFinite(fromIndex) || !Number.isFinite(toIndex)) return + if (fromIndex < 0 || toIndex < 0) return + if (fromIndex === toIndex) return + + const next = (Array.isArray(relistNow) ? relistNow : []).slice() + if (fromIndex >= next.length || toIndex >= next.length) return + const moved = next.splice(fromIndex, 1)[0] + next.splice(toIndex, 0, moved) + + saveUploadList(next, function () { + renderUploadList(relistNow) + }) +}) + +$(document).on('click', '.upload-del', function () { + const name = $(this).data('name') + if (!name) return + + get_info(function (info) { + if (!info.status) { + $.message({ message: chrome.i18n.getMessage('placeApiUrl') }) + return + } + + $.ajax({ + url: info.apiUrl + 'api/v1/' + name, + type: 'DELETE', + headers: { Authorization: 'Bearer ' + info.apiTokens }, + success: function () { + const next = (Array.isArray(relistNow) ? relistNow : []).filter(function (x) { + return x && x.name !== name + }) + saveUploadList(next, function () { + $.message({ message: chrome.i18n.getMessage('attachmentDeleteSuccess') }) + renderUploadList(relistNow) + }) + }, + error: function () { + $.message({ message: chrome.i18n.getMessage('attachmentDeleteFailed') }) + } + }) + }) +}) function uploadImage(file) { $.message({ message: chrome.i18n.getMessage("picUploading"), @@ -196,9 +350,10 @@ function uploadImageNow(base64String, file) { if (data.name) { // 更新上传的文件信息并暂存浏览器本地 relistNow.push({ - "name":data.name, - "createTime":data.createTime, - "type":data.type + "name": data.name, + "filename": data.filename || new_name, + "createTime": data.createTime, + "type": data.type }) chrome.storage.sync.set( { @@ -383,8 +538,8 @@ $('#search').click(function () { for(var i=0;i < searchData.length;i++){ var memosID = searchData[i].name.split('/').pop(); searchDom += '
'+dayjs(searchData.createTime).fromNow()+'
'+searchData[i].content.replace(/!\[.*?\]\((.*?)\)/g,' ').replace(/\[(.*?)\]\((.*?)\)/g,' $1 ')+'
' - if(searchData[i].resources && searchData[i].resources.length > 0){ - var resources = searchData[i].resources; + var resources = (searchData[i].attachments && searchData[i].attachments.length > 0) ? searchData[i].attachments : (searchData[i].resources || []); + if(resources && resources.length > 0){ for(var j=0;j < resources.length;j++){ var restype = resources[j].type.slice(0,5); var resexlink = resources[j].externalLink @@ -454,8 +609,8 @@ function randDom(randomData){ get_info(function (info) { var memosID = randomData.name.split('/').pop(); var randomDom = '
'+dayjs(randomData.createTime).fromNow()+'
'+randomData.content.replace(/!\[.*?\]\((.*?)\)/g,' ').replace(/\[(.*?)\]\((.*?)\)/g,' $1 ')+'
' - if(randomData.resources && randomData.resources.length > 0){ - var resources = randomData.resources; + var resources = (randomData.attachments && randomData.attachments.length > 0) ? randomData.attachments : (randomData.resources || []); + if(resources && resources.length > 0){ for(var j=0;j < resources.length;j++){ var restype = resources[j].type.slice(0,5); var resexlink = resources[j].externalLink @@ -641,7 +796,7 @@ function sendText() { url:info.apiUrl+'api/v1/'+data.name, type:"PATCH", data:JSON.stringify({ - 'resources': info.resourceIdList || [], + 'attachments': (info.resourceIdList || []).map(function (att) { return { name: att.name } }), }), contentType:"application/json", dataType:"json", @@ -661,6 +816,8 @@ function sendText() { }) //$("#content_submit_text").removeAttr('disabled'); $("textarea[name=text]").val('') + relistNow = [] + renderUploadList(relistNow) } ) },error:function(err){//清空open_action(打开时候进行的操作),同时清空open_content diff --git a/popup.html b/popup.html index e7fa63a..8822b05 100644 --- a/popup.html +++ b/popup.html @@ -99,6 +99,12 @@
+ +
+
+
+
+