mirror of
https://github.com/Jonnyan404/memos-bber.git
synced 2026-04-25 03:58:37 +09:00
Add packaging workflow and browser dist
Add a GitHub Actions workflow (package-extensions.yml) to build and upload Chrome/Firefox packages (store and offline artifacts) on manual trigger or when pushing v* tags. Update README with packaging instructions. Reorganize extension sources into chrome/ and firefox/ directories, add Firefox-specific files (manifest, locales, assets, CSS, LICENSE), and bump Chrome manifest version to 2026.04.23. Also modify js/oper.js (moved to chrome/js) to improve proportional editor resizing: add drag-to-resize, scale clamping/persistence (localStorage + chrome.storage.sync), pointer event handlers, and max-scale computation.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
const UI_LANGUAGE_STORAGE_KEY = 'uiLanguage'
|
||||
|
||||
const SUPPORTED_UI_LANGUAGES = new Set(['auto', 'en', 'zh_CN', 'ja', 'ko'])
|
||||
|
||||
function normalizeUiLanguage(value) {
|
||||
const lang = String(value || 'auto')
|
||||
return SUPPORTED_UI_LANGUAGES.has(lang) ? lang : 'auto'
|
||||
}
|
||||
|
||||
function storageSyncGet(defaults) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.sync.get(defaults, (items) => resolve(items || {}))
|
||||
})
|
||||
}
|
||||
|
||||
function updateContextMenu(id, update) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
chrome.contextMenus.update(id, update, () => resolve())
|
||||
} catch (_) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function pageReadSelectionText() {
|
||||
try {
|
||||
const active = document.activeElement
|
||||
const isTextInput =
|
||||
active &&
|
||||
(active.tagName === 'TEXTAREA' ||
|
||||
(active.tagName === 'INPUT' &&
|
||||
/^(text|search|url|tel|email|password)$/i.test(active.type || 'text')))
|
||||
|
||||
if (isTextInput && typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
|
||||
return String(active.value || '').slice(active.selectionStart, active.selectionEnd).replace(/\r\n?/g, '\n')
|
||||
}
|
||||
|
||||
const sel = window.getSelection && window.getSelection()
|
||||
if (!sel) return ''
|
||||
return String(sel.toString() || '').replace(/\r\n?/g, '\n')
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function pageReadSelectionTextSource() {
|
||||
return `(${pageReadSelectionText.toString()})()`
|
||||
}
|
||||
|
||||
function getSelectionTextFromTab(tabId, fallbackText) {
|
||||
return new Promise((resolve) => {
|
||||
const fallback = typeof fallbackText === 'string' ? fallbackText : ''
|
||||
if (!tabId) {
|
||||
resolve(fallback)
|
||||
return
|
||||
}
|
||||
|
||||
if (chrome.scripting && typeof chrome.scripting.executeScript === 'function') {
|
||||
try {
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: { tabId },
|
||||
func: pageReadSelectionText
|
||||
},
|
||||
(results) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(fallback)
|
||||
return
|
||||
}
|
||||
const first = Array.isArray(results) ? results[0] : null
|
||||
const text = first && typeof first.result === 'string' ? first.result : ''
|
||||
resolve(text || fallback)
|
||||
}
|
||||
)
|
||||
return
|
||||
} catch (_) {
|
||||
// Fallback below for Firefox MV2 background pages.
|
||||
}
|
||||
}
|
||||
|
||||
if (chrome.tabs && typeof chrome.tabs.executeScript === 'function') {
|
||||
try {
|
||||
chrome.tabs.executeScript(tabId, { code: pageReadSelectionTextSource() }, (results) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(fallback)
|
||||
return
|
||||
}
|
||||
const text = Array.isArray(results) && typeof results[0] === 'string' ? results[0] : ''
|
||||
resolve(text || fallback)
|
||||
})
|
||||
return
|
||||
} catch (_) {
|
||||
// Ignore and fall back to the original selection text below.
|
||||
}
|
||||
}
|
||||
|
||||
resolve(fallback)
|
||||
})
|
||||
}
|
||||
|
||||
function getActionApi() {
|
||||
if (chrome.action && typeof chrome.action.openPopup === 'function') return chrome.action
|
||||
if (chrome.browserAction && typeof chrome.browserAction.openPopup === 'function') return chrome.browserAction
|
||||
return null
|
||||
}
|
||||
|
||||
function tryOpenActionPopup(tab) {
|
||||
try {
|
||||
const actionApi = getActionApi()
|
||||
if (!actionApi) return
|
||||
const windowId = tab && typeof tab.windowId === 'number' ? tab.windowId : undefined
|
||||
|
||||
const open = () => {
|
||||
try {
|
||||
if (typeof windowId === 'number') {
|
||||
actionApi.openPopup({ windowId }, () => void chrome.runtime.lastError)
|
||||
} else {
|
||||
actionApi.openPopup({}, () => void chrome.runtime.lastError)
|
||||
}
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid: "Cannot show popup for an inactive window".
|
||||
if (typeof windowId === 'number' && chrome.windows && typeof chrome.windows.update === 'function') {
|
||||
chrome.windows.update(windowId, { focused: true }, () => {
|
||||
void chrome.runtime.lastError
|
||||
open()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
open()
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
let cachedUiLanguage = null
|
||||
let cachedOverrideMessages = null
|
||||
|
||||
async function loadLocaleMessages(locale) {
|
||||
if (!locale || locale === 'auto') return null
|
||||
try {
|
||||
const url = chrome.runtime.getURL(`_locales/${locale}/messages.json`)
|
||||
const resp = await fetch(url)
|
||||
if (!resp.ok) return null
|
||||
return await resp.json()
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function getUiLanguage() {
|
||||
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
|
||||
return normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
|
||||
}
|
||||
|
||||
async function t(key) {
|
||||
const lang = await getUiLanguage()
|
||||
if (lang !== cachedUiLanguage) {
|
||||
cachedUiLanguage = lang
|
||||
cachedOverrideMessages = await loadLocaleMessages(lang)
|
||||
}
|
||||
|
||||
const msg = cachedOverrideMessages && cachedOverrideMessages[key] && cachedOverrideMessages[key].message
|
||||
if (typeof msg === 'string' && msg.length > 0) return msg
|
||||
return chrome.i18n.getMessage(key) || ''
|
||||
}
|
||||
|
||||
async function refreshContextMenus() {
|
||||
await updateContextMenu('Memos-send-selection', { title: await t('sendTo') })
|
||||
await updateContextMenu('Memos-send-link', { title: await t('sendLinkTo') })
|
||||
await updateContextMenu('Memos-send-image', { title: await t('sendImageTo') })
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
type: 'normal',
|
||||
title: chrome.i18n.getMessage('sendTo'),
|
||||
id: 'Memos-send-selection',
|
||||
contexts: ['selection']
|
||||
})
|
||||
chrome.contextMenus.create({
|
||||
type: 'normal',
|
||||
title: chrome.i18n.getMessage('sendLinkTo'),
|
||||
id: 'Memos-send-link',
|
||||
contexts: ['link', 'page']
|
||||
})
|
||||
chrome.contextMenus.create({
|
||||
type: 'normal',
|
||||
title: chrome.i18n.getMessage('sendImageTo'),
|
||||
id: 'Memos-send-image',
|
||||
contexts: ['image']
|
||||
})
|
||||
|
||||
// Apply override titles if user selected a fixed language.
|
||||
refreshContextMenus()
|
||||
})
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== 'sync') return
|
||||
if (!changes[UI_LANGUAGE_STORAGE_KEY]) return
|
||||
cachedUiLanguage = null
|
||||
cachedOverrideMessages = null
|
||||
refreshContextMenus()
|
||||
})
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const appendContent = (tempCont, { openPopup } = { openPopup: false }) => {
|
||||
chrome.storage.sync.get({ open_action: 'save_text', open_content: '' }, function (items) {
|
||||
if (items.open_action === 'upload_image') {
|
||||
t('picPending').then((m) => alert(m))
|
||||
return
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(
|
||||
{
|
||||
open_action: 'save_text',
|
||||
open_content: items.open_content + tempCont
|
||||
},
|
||||
function () {
|
||||
if (openPopup) tryOpenActionPopup(tab)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'Memos-send-selection') {
|
||||
const ref = info.linkUrl || info.pageUrl
|
||||
const tabId = tab && tab.id
|
||||
|
||||
getSelectionTextFromTab(tabId, info.selectionText).then((selectionText) => {
|
||||
const tempCont = selectionText + '\n' + `[Reference Link](${ref})` + '\n'
|
||||
appendContent(tempCont, { openPopup: true })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'Memos-send-link') {
|
||||
appendContent((info.linkUrl || info.pageUrl) + '\n')
|
||||
return
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'Memos-send-image') {
|
||||
appendContent(`` + '\n')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,521 @@
|
||||
(function (global) {
|
||||
'use strict'
|
||||
|
||||
const FLAVOR_V020_V021 = 'v020-v021'
|
||||
const KNOWN_FLAVORS = [FLAVOR_V020_V021, 'v023', 'modern']
|
||||
|
||||
function requestJson(options, success, fail) {
|
||||
global.$
|
||||
.ajax(options)
|
||||
.done(function (data) {
|
||||
if (success) success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
function extractMemos(data) {
|
||||
if (global.MemosApiModern && typeof global.MemosApiModern.extractMemosListFromResponse === 'function') {
|
||||
return global.MemosApiModern.extractMemosListFromResponse(data)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function getFlavor(info) {
|
||||
if (!info) return 'legacy'
|
||||
if (info.apiFlavor === 'modern' && global.MemosApiV023) return 'modern'
|
||||
if (info.apiFlavor === 'v023' && global.MemosApiV023) return 'v023'
|
||||
if ((info.apiFlavor === FLAVOR_V020_V021 || info.apiFlavor === 'v1') && global.MemosApiV020V021) {
|
||||
return FLAVOR_V020_V021
|
||||
}
|
||||
return 'legacy'
|
||||
}
|
||||
|
||||
function normalizeDetectedFlavor(flavor) {
|
||||
const value = typeof flavor === 'string' ? flavor : ''
|
||||
if (value === 'v020' || value === 'v021' || value === 'v1') return FLAVOR_V020_V021
|
||||
return value
|
||||
}
|
||||
|
||||
function looksLikeMemosListPayload(data) {
|
||||
if (!data) return false
|
||||
if (Array.isArray(data)) return true
|
||||
if (Array.isArray(data.memos)) return true
|
||||
if (data.data && Array.isArray(data.data.memos)) return true
|
||||
if (Array.isArray(data.list)) return true
|
||||
if (typeof data.error === 'string' || typeof data.message === 'string') return false
|
||||
return false
|
||||
}
|
||||
|
||||
function isNotFoundLikeProbeXhr(xhr) {
|
||||
const status = xhr && xhr.status
|
||||
return status === 404 || status === 405
|
||||
}
|
||||
|
||||
function probeFlavor(apiUrl, apiTokens, callback) {
|
||||
const headers = { Authorization: 'Bearer ' + apiTokens }
|
||||
const modernQ =
|
||||
'api/v1/memos?pageSize=1&filter=' +
|
||||
encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]')
|
||||
const v023Q =
|
||||
'api/v1/memos?pageSize=1&filter=' +
|
||||
encodeURIComponent('visibilities == ["PUBLIC","PROTECTED"]')
|
||||
const v020V021Q = 'api/v1/memo?limit=1&rowStatus=NORMAL'
|
||||
|
||||
function finish(flavor) {
|
||||
const normalized = normalizeDetectedFlavor(flavor)
|
||||
if (KNOWN_FLAVORS.indexOf(normalized) !== -1) {
|
||||
if (callback) callback({ flavor: normalized })
|
||||
return
|
||||
}
|
||||
if (callback) callback({ flavor: 'unknown' })
|
||||
}
|
||||
|
||||
function probeV023() {
|
||||
global.$
|
||||
.ajax({
|
||||
url: apiUrl + v023Q,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function (data) {
|
||||
if (looksLikeMemosListPayload(data)) finish('v023')
|
||||
else finish('unknown')
|
||||
})
|
||||
.fail(function () {
|
||||
finish('unknown')
|
||||
})
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: apiUrl + modernQ,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function (data) {
|
||||
if (looksLikeMemosListPayload(data)) {
|
||||
finish('modern')
|
||||
return
|
||||
}
|
||||
probeV023()
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (xhr && xhr.status === 400) {
|
||||
probeV023()
|
||||
return
|
||||
}
|
||||
|
||||
if (isNotFoundLikeProbeXhr(xhr)) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: apiUrl + v020V021Q,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function (data) {
|
||||
if (looksLikeMemosListPayload(data)) finish(FLAVOR_V020_V021)
|
||||
else finish('unknown')
|
||||
})
|
||||
.fail(function () {
|
||||
finish('unknown')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
finish('unknown')
|
||||
})
|
||||
}
|
||||
|
||||
function keepLegacyVisibleMemos(list) {
|
||||
const items = Array.isArray(list) ? list : []
|
||||
return items.filter(function (memo) {
|
||||
if (!memo) return false
|
||||
const visibility = typeof memo.visibility === 'string' ? memo.visibility.toUpperCase() : ''
|
||||
if (!visibility) return true
|
||||
return visibility === 'PUBLIC' || visibility === 'PROTECTED'
|
||||
})
|
||||
}
|
||||
|
||||
function extractTagsFromGenericMemo(memo) {
|
||||
if (!memo) return []
|
||||
if (Array.isArray(memo.tags) && memo.tags.length > 0) return memo.tags
|
||||
if (Array.isArray(memo.tagList) && memo.tagList.length > 0) return memo.tagList
|
||||
if (memo.property && Array.isArray(memo.property.tags) && memo.property.tags.length > 0) {
|
||||
return memo.property.tags
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function collectTags(info, memos) {
|
||||
const items = Array.isArray(memos) ? memos : []
|
||||
const out = items.flatMap(function (memo) {
|
||||
if (!memo) return []
|
||||
if (getFlavor(info) === 'v023' && global.MemosApiV023 && typeof global.MemosApiV023.extractTagsFromMemo === 'function') {
|
||||
return global.MemosApiV023.extractTagsFromMemo(memo)
|
||||
}
|
||||
return extractTagsFromGenericMemo(memo)
|
||||
})
|
||||
return [...new Set(out.filter(Boolean))]
|
||||
}
|
||||
|
||||
function buildUploadVisibility(editorContent, hideTag, showTag, memoLock) {
|
||||
const content = typeof editorContent === 'string' ? editorContent : ''
|
||||
const nowTag = content.match(/(#[^\s#]+)/)
|
||||
let visibility = memoLock || ''
|
||||
if (nowTag) {
|
||||
if (nowTag[1] === showTag) visibility = 'PUBLIC'
|
||||
else if (nowTag[1] === hideTag) visibility = 'PRIVATE'
|
||||
}
|
||||
return visibility
|
||||
}
|
||||
|
||||
function buildModernFilter(parts) {
|
||||
const p = parts || {}
|
||||
const exprs = []
|
||||
|
||||
if (typeof p.contentSearch === 'string' && p.contentSearch.length > 0) {
|
||||
exprs.push('content.contains(' + JSON.stringify(String(p.contentSearch)) + ')')
|
||||
}
|
||||
|
||||
return exprs.join(' && ')
|
||||
}
|
||||
|
||||
function normalizeUploadedItem(entity, fallbackFilename) {
|
||||
if (!entity) return null
|
||||
const inferredId = (function () {
|
||||
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
|
||||
if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {
|
||||
return Math.floor(Number(value))
|
||||
}
|
||||
return null
|
||||
})()
|
||||
|
||||
const name = entity.name || (inferredId != null ? 'resources/' + String(inferredId) : '')
|
||||
if (!name && inferredId == null) return null
|
||||
|
||||
return {
|
||||
id: inferredId != null ? inferredId : entity.id,
|
||||
name: name,
|
||||
filename: entity.filename || fallbackFilename || name,
|
||||
createTime: entity.createTime || entity.createdTs || entity.createdAt,
|
||||
type: entity.type
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapLegacyMemoEntity(data) {
|
||||
if (!data) return data
|
||||
if (data.memo) return data.memo
|
||||
if (data.data && data.data.memo) return data.data.memo
|
||||
return data
|
||||
}
|
||||
|
||||
function normalizeLegacyResourceIdList(list) {
|
||||
const items = Array.isArray(list) ? list : []
|
||||
return items
|
||||
.map(function (item) {
|
||||
if (!item) return null
|
||||
if (typeof item.id === 'number' && Number.isFinite(item.id)) return Math.floor(item.id)
|
||||
if (typeof item.id === 'string' && item.id.trim() !== '' && !Number.isNaN(Number(item.id))) {
|
||||
return Math.floor(Number(item.id))
|
||||
}
|
||||
const name = typeof item.name === 'string' ? item.name : ''
|
||||
const tail = name ? name.split('/').pop() : ''
|
||||
if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail))
|
||||
return null
|
||||
})
|
||||
.filter(function (value) {
|
||||
return value != null && Number.isFinite(value)
|
||||
})
|
||||
}
|
||||
|
||||
function resolve(info) {
|
||||
const flavor = getFlavor(info)
|
||||
|
||||
function listTags(success, fail) {
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
|
||||
global.MemosApiV020V021.getTagSuggestion(info, success, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (flavor === 'v023' && global.MemosApiV023) {
|
||||
const filterExpr = global.MemosApiV023.buildFilter({
|
||||
rowStatus: 'NORMAL',
|
||||
creator: 'users/' + info.userid
|
||||
})
|
||||
global.MemosApiV023.listMemos(
|
||||
info,
|
||||
{ pageSize: 1000, filterExpr: filterExpr },
|
||||
function (data) {
|
||||
if (success) success(collectTags(info, extractMemos(data)))
|
||||
},
|
||||
fail
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (global.MemosApiModern) {
|
||||
global.MemosApiModern.fetchMemosWithFallback(
|
||||
info,
|
||||
'?pageSize=1000',
|
||||
function (data) {
|
||||
if (success) success(collectTags(info, extractMemos(data)))
|
||||
},
|
||||
fail
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function searchMemos(pattern, success, fail) {
|
||||
const text = String(pattern || '')
|
||||
const patternLiteral = JSON.stringify(text)
|
||||
const legacyFilter = '?filter=' + encodeURIComponent('visibility in ["PUBLIC","PROTECTED"] && content.contains(' + patternLiteral + ')')
|
||||
|
||||
if (flavor === 'modern' && global.MemosApiV023) {
|
||||
const filterExpr = buildModernFilter({ contentSearch: text })
|
||||
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
|
||||
if (success) success(extractMemos(data))
|
||||
}, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (flavor === 'v023' && global.MemosApiV023) {
|
||||
const filterExpr = global.MemosApiV023.buildFilter({
|
||||
visibilities: ['PUBLIC', 'PROTECTED'],
|
||||
contentSearch: text
|
||||
})
|
||||
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
|
||||
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
|
||||
}, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
|
||||
global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL', contentSearch: text }, function (data) {
|
||||
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
|
||||
}, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (global.MemosApiModern) {
|
||||
global.MemosApiModern.fetchMemosWithFallback(info, legacyFilter, function (data) {
|
||||
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
|
||||
}, fail)
|
||||
}
|
||||
}
|
||||
|
||||
function listRandomMemos(success, fail) {
|
||||
if (flavor === 'modern' && global.MemosApiV023) {
|
||||
const filterExpr = global.MemosApiV023.buildFilter({})
|
||||
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
|
||||
if (success) success(extractMemos(data))
|
||||
}, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (flavor === 'v023' && global.MemosApiV023) {
|
||||
const filterExpr = global.MemosApiV023.buildFilter({ visibilities: ['PUBLIC', 'PROTECTED'] })
|
||||
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
|
||||
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
|
||||
}, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
|
||||
global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, function (data) {
|
||||
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
|
||||
}, fail)
|
||||
return
|
||||
}
|
||||
|
||||
if (global.MemosApiModern) {
|
||||
const legacyFilter = '?filter=' + encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]')
|
||||
global.MemosApiModern.fetchMemosWithFallback(info, legacyFilter, function (data) {
|
||||
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
|
||||
}, fail)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteResource(item, success, fail) {
|
||||
const name = item && item.name ? item.name : ''
|
||||
const rid = item && item.id != null ? item.id : ''
|
||||
const inferredId = (function () {
|
||||
if (rid != null && String(rid).trim() !== '' && !Number.isNaN(Number(rid))) return Math.floor(Number(rid))
|
||||
const tail = String(name || '').split('/').pop()
|
||||
if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail))
|
||||
return null
|
||||
})()
|
||||
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021 && typeof global.MemosApiV020V021.deleteResource === 'function' && inferredId != null) {
|
||||
global.MemosApiV020V021.deleteResource(info, inferredId, success, fail)
|
||||
return
|
||||
}
|
||||
|
||||
requestJson({
|
||||
url: info.apiUrl + 'api/v1/' + name,
|
||||
type: 'DELETE',
|
||||
headers: { Authorization: 'Bearer ' + info.apiTokens }
|
||||
}, success, fail)
|
||||
}
|
||||
|
||||
function uploadFile(file, options, success, fail) {
|
||||
const oldName = String(file && file.name ? file.name : 'upload').split('.')
|
||||
const fileExt = String(file && file.name ? file.name : '').split('.').pop()
|
||||
const now = global.dayjs().format('YYYYMMDDHHmmss')
|
||||
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '')
|
||||
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
|
||||
global.MemosApiV020V021.uploadResourceBlob(
|
||||
info,
|
||||
file,
|
||||
{ filename: nextName, type: file.type },
|
||||
function (entity) {
|
||||
if (success) success(normalizeUploadedItem(entity, nextName))
|
||||
},
|
||||
fail
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (e) {
|
||||
const base64String = e && e.target && e.target.result ? String(e.target.result).split(',')[1] : ''
|
||||
const payload = {
|
||||
content: base64String,
|
||||
visibility: buildUploadVisibility(options && options.editorContent, options && options.hideTag, options && options.showTag, options && options.memoLock),
|
||||
filename: nextName,
|
||||
type: file.type
|
||||
}
|
||||
|
||||
global.MemosApiModern.uploadAttachmentOrResource(
|
||||
info,
|
||||
payload,
|
||||
function (resp) {
|
||||
const entity = (resp && resp.resource) || resp
|
||||
if (success) success(normalizeUploadedItem(entity, nextName))
|
||||
},
|
||||
fail
|
||||
)
|
||||
}
|
||||
reader.onerror = fail
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function archiveMemo(memo, success, fail) {
|
||||
const memoId = memo && memo.id != null ? memo.id : ''
|
||||
const memoName = memo && memo.name ? memo.name : ''
|
||||
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021 && memoId !== '') {
|
||||
global.MemosApiV020V021.patchMemo(info, memoId, { rowStatus: 'ARCHIVED' }, success, fail)
|
||||
return
|
||||
}
|
||||
|
||||
requestJson({
|
||||
url: info.apiUrl + 'api/v1/' + memoName,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify({ state: 'ARCHIVED' }),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: { Authorization: 'Bearer ' + info.apiTokens }
|
||||
}, success, fail)
|
||||
}
|
||||
|
||||
function getMemo(memoRef, success, fail) {
|
||||
const url = flavor === FLAVOR_V020_V021
|
||||
? info.apiUrl + 'api/v1/memo/' + memoRef
|
||||
: info.apiUrl + 'api/v1/' + memoRef
|
||||
|
||||
requestJson({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: { Authorization: 'Bearer ' + info.apiTokens }
|
||||
}, function (data) {
|
||||
if (success) success(flavor === FLAVOR_V020_V021 ? unwrapLegacyMemoEntity(data) : data)
|
||||
}, fail)
|
||||
}
|
||||
|
||||
function createMemo(params, success, fail) {
|
||||
const payload = params || {}
|
||||
|
||||
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
|
||||
global.MemosApiV020V021.createMemo(
|
||||
info,
|
||||
{
|
||||
content: payload.content,
|
||||
visibility: payload.visibility,
|
||||
resourceIdList: normalizeLegacyResourceIdList(payload.resourceIdList)
|
||||
},
|
||||
success,
|
||||
fail
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
requestJson({
|
||||
url: info.apiUrl + 'api/v1/memos',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
content: payload.content,
|
||||
visibility: payload.visibility
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: { Authorization: 'Bearer ' + info.apiTokens }
|
||||
}, function (data) {
|
||||
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : ''
|
||||
const resources = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
|
||||
if (!createdName) {
|
||||
if (success) success(data)
|
||||
return
|
||||
}
|
||||
if (resources.length === 0) {
|
||||
getMemo(createdName, success, fail)
|
||||
return
|
||||
}
|
||||
|
||||
global.MemosApiModern.patchMemoWithAttachmentsOrResources(
|
||||
info,
|
||||
createdName,
|
||||
resources,
|
||||
function () {
|
||||
getMemo(createdName, success, fail)
|
||||
},
|
||||
function () {
|
||||
getMemo(createdName, success, fail)
|
||||
}
|
||||
)
|
||||
}, fail)
|
||||
}
|
||||
|
||||
return {
|
||||
flavor: flavor,
|
||||
needsAuthenticatedImagePreview: function () {
|
||||
return flavor === FLAVOR_V020_V021
|
||||
},
|
||||
listTags: listTags,
|
||||
searchMemos: searchMemos,
|
||||
listRandomMemos: listRandomMemos,
|
||||
deleteResource: deleteResource,
|
||||
uploadFile: uploadFile,
|
||||
archiveMemo: archiveMemo,
|
||||
getMemo: getMemo,
|
||||
createMemo: createMemo
|
||||
}
|
||||
}
|
||||
|
||||
global.MemosApiAdapter = {
|
||||
FLAVOR_V020_V021: FLAVOR_V020_V021,
|
||||
KNOWN_FLAVORS: KNOWN_FLAVORS.slice(),
|
||||
getFlavor: getFlavor,
|
||||
normalizeDetectedFlavor: normalizeDetectedFlavor,
|
||||
probeFlavor: probeFlavor,
|
||||
resolve: resolve
|
||||
}
|
||||
})(window)
|
||||
@@ -0,0 +1,512 @@
|
||||
(function (global) {
|
||||
'use strict'
|
||||
|
||||
function extractUserIdFromAuthResponse(response) {
|
||||
if (!response) return null
|
||||
|
||||
const user = response.user || response
|
||||
|
||||
if (typeof user.id === 'number' && Number.isFinite(user.id)) return user.id
|
||||
if (typeof user.id === 'string' && user.id.trim() !== '' && !Number.isNaN(Number(user.id))) {
|
||||
return Number(user.id)
|
||||
}
|
||||
|
||||
if (typeof user.username === 'string' && user.username.trim() !== '') {
|
||||
return user.username.trim()
|
||||
}
|
||||
|
||||
const name = user.name || (user.user && user.user.name)
|
||||
if (typeof name === 'string') {
|
||||
const m = name.match(/\busers\/(\d+)\b/)
|
||||
if (m) return Number(m[1])
|
||||
const last = name.split('/').pop()
|
||||
if (last) {
|
||||
if (!Number.isNaN(Number(last))) return Number(last)
|
||||
if (last.trim() !== '') return last.trim()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function extractMemosListFromResponse(data) {
|
||||
if (!data) return []
|
||||
if (Array.isArray(data)) return data
|
||||
if (Array.isArray(data.memos)) return data.memos
|
||||
if (data.data && Array.isArray(data.data.memos)) return data.data.memos
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
return []
|
||||
}
|
||||
|
||||
function isNotFoundLikeXhr(jqXhr) {
|
||||
const status = jqXhr && jqXhr.status
|
||||
return status === 404 || status === 405
|
||||
}
|
||||
|
||||
function authWithFallback(apiUrl, apiTokens, callback) {
|
||||
const headers = { Authorization: 'Bearer ' + apiTokens }
|
||||
|
||||
// v0.26+: GET auth/me
|
||||
// older: POST/GET auth/status
|
||||
const tries = [
|
||||
{ method: 'GET', path: 'api/v1/auth/me', uiPath: 'memos' },
|
||||
// v0.25: session-based auth service still accepts bearer tokens and returns { user: ... }.
|
||||
{ method: 'GET', path: 'api/v1/auth/sessions/current', uiPath: 'memos' },
|
||||
// v0.20: current user endpoint.
|
||||
{ method: 'GET', path: 'api/v1/user/me', uiPath: 'm' },
|
||||
{ method: 'POST', path: 'api/v1/auth/status', uiPath: 'm' },
|
||||
{ method: 'GET', path: 'api/v1/auth/status', uiPath: 'm' }
|
||||
]
|
||||
|
||||
function runAt(index) {
|
||||
if (index >= tries.length) {
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
|
||||
const t = tries[index]
|
||||
global.$
|
||||
.ajax({
|
||||
async: true,
|
||||
crossDomain: true,
|
||||
url: apiUrl + t.path,
|
||||
method: t.method,
|
||||
headers: headers
|
||||
})
|
||||
.done(function (response) {
|
||||
const userId = extractUserIdFromAuthResponse(response)
|
||||
if (userId != null) callback({ userId: userId, uiPath: t.uiPath, raw: response })
|
||||
else runAt(index + 1)
|
||||
})
|
||||
.fail(function () {
|
||||
runAt(index + 1)
|
||||
})
|
||||
}
|
||||
|
||||
runAt(0)
|
||||
}
|
||||
|
||||
function fetchMemosWithFallback(info, query, success, fail) {
|
||||
const qs = query || ''
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
|
||||
// v0.24: `GET /api/v1/memos` tends to behave like a public feed (private memos excluded).
|
||||
// For an authenticated user, `GET /api/v1/users/{id}/memos` is the safe way to retrieve
|
||||
// the full set (including private), which affects tag extraction.
|
||||
// Newer versions may not expose the user-scoped endpoint, so we fallback by 404/405.
|
||||
const urlUserScoped = info.userid
|
||||
? info.apiUrl + 'api/v1/users/' + encodeURIComponent(String(info.userid)) + '/memos' + qs
|
||||
: null
|
||||
const urlGlobal = info.apiUrl + 'api/v1/memos' + qs
|
||||
|
||||
const urlPrimary = urlUserScoped || urlGlobal
|
||||
const urlFallback = urlUserScoped ? urlGlobal : null
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlPrimary,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
const status = xhr && xhr.status
|
||||
const canFallback = Boolean(urlFallback) && (isNotFoundLikeXhr(xhr) || status === 400)
|
||||
if (!canFallback) {
|
||||
if (fail) fail(xhr)
|
||||
return
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlFallback,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
success(data)
|
||||
})
|
||||
.fail(function (xhr2) {
|
||||
if (fail) fail(xhr2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function uploadAttachmentOrResource(info, payload, onSuccess, onFail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
const urlAttachments = info.apiUrl + 'api/v1/attachments'
|
||||
const urlResources = info.apiUrl + 'api/v1/resources'
|
||||
|
||||
function stripVisibility(p) {
|
||||
if (!p || typeof p !== 'object') return p
|
||||
if (!Object.prototype.hasOwnProperty.call(p, 'visibility')) return p
|
||||
const copy = Object.assign({}, p)
|
||||
delete copy.visibility
|
||||
return copy
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlAttachments,
|
||||
data: JSON.stringify(payload),
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'attachments')
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (xhr && xhr.status === 400) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlAttachments,
|
||||
data: JSON.stringify(stripVisibility(payload)),
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'attachments')
|
||||
})
|
||||
.fail(function (xhrRetry) {
|
||||
if (!isNotFoundLikeXhr(xhrRetry)) {
|
||||
if (onFail) onFail(xhrRetry)
|
||||
return
|
||||
}
|
||||
// fall through to resources below
|
||||
xhr = xhrRetry
|
||||
if (!isNotFoundLikeXhr(xhr)) {
|
||||
if (onFail) onFail(xhr)
|
||||
return
|
||||
}
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlResources,
|
||||
data: JSON.stringify(payload),
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr2) {
|
||||
if (xhr2 && xhr2.status === 400) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlResources,
|
||||
data: JSON.stringify(stripVisibility(payload)),
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr3) {
|
||||
if (onFail) onFail(xhr3)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (onFail) onFail(xhr2)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNotFoundLikeXhr(xhr)) {
|
||||
if (onFail) onFail(xhr)
|
||||
return
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlResources,
|
||||
data: JSON.stringify(payload),
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr2) {
|
||||
if (xhr2 && xhr2.status === 400) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: urlResources,
|
||||
data: JSON.stringify(stripVisibility(payload)),
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr3) {
|
||||
if (onFail) onFail(xhr3)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (onFail) onFail(xhr2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function patchMemoWithAttachmentsOrResources(info, memoName, list, onSuccess, onFail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
const url = info.apiUrl + 'api/v1/' + memoName
|
||||
const items = Array.isArray(list) ? list : []
|
||||
|
||||
const hasResourceNames = items.some(function (x) {
|
||||
return x && typeof x.name === 'string' && x.name.indexOf('resources/') === 0
|
||||
})
|
||||
const hasAttachmentNames = items.some(function (x) {
|
||||
return x && typeof x.name === 'string' && x.name.indexOf('attachments/') === 0
|
||||
})
|
||||
|
||||
function doPatchAttachments() {
|
||||
const attachments = items
|
||||
.map(function (x) {
|
||||
if (!x) return null
|
||||
const n = x.name
|
||||
if (!n) return null
|
||||
if (hasAttachmentNames && typeof n === 'string' && n.indexOf('attachments/') !== 0) return null
|
||||
return { name: n }
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
// Prefer the dedicated subresource endpoint when available.
|
||||
global.$
|
||||
.ajax({
|
||||
url: url + '/attachments',
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify({ name: memoName, attachments: attachments }),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'attachments')
|
||||
})
|
||||
.fail(function (xhr0) {
|
||||
// If the endpoint doesn't exist, try UpdateMemo-style patching.
|
||||
if (isNotFoundLikeXhr(xhr0)) {
|
||||
// continue
|
||||
} else if (xhr0 && xhr0.status && xhr0.status !== 400) {
|
||||
// continue; some gateways may reject body shape here.
|
||||
}
|
||||
|
||||
// Some versions accept a loose patch, others require updateMask.
|
||||
const attachmentsPayloadLoose = {
|
||||
name: memoName,
|
||||
attachments: attachments
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: url,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(attachmentsPayloadLoose),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'attachments')
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
// v0.25 requires update mask when updating attachments.
|
||||
if (!isNotFoundLikeXhr(xhr) && xhr && xhr.status !== 400) {
|
||||
if (onFail) onFail(xhr)
|
||||
return
|
||||
}
|
||||
|
||||
// If the server doesn't support attachments at all, fallback to resources flow.
|
||||
if (isNotFoundLikeXhr(xhr)) {
|
||||
doPatchResources()
|
||||
return
|
||||
}
|
||||
|
||||
const attachmentsPayloadV025 = {
|
||||
name: memoName,
|
||||
attachments: attachments
|
||||
}
|
||||
|
||||
const updateUrl1 = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'updateMask=attachments'
|
||||
global.$
|
||||
.ajax({
|
||||
url: updateUrl1,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(attachmentsPayloadV025),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'attachments')
|
||||
})
|
||||
.fail(function (xhr2) {
|
||||
if (isNotFoundLikeXhr(xhr2)) {
|
||||
doPatchResources()
|
||||
return
|
||||
}
|
||||
// Some grpc-gateway setups prefer updateMask.paths.
|
||||
if (xhr2 && xhr2.status === 400) {
|
||||
const updateUrl2 =
|
||||
url + (url.indexOf('?') >= 0 ? '&' : '?') + 'updateMask.paths=attachments'
|
||||
global.$
|
||||
.ajax({
|
||||
url: updateUrl2,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(attachmentsPayloadV025),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'attachments')
|
||||
})
|
||||
.fail(function (xhr3) {
|
||||
if (isNotFoundLikeXhr(xhr3)) {
|
||||
doPatchResources()
|
||||
return
|
||||
}
|
||||
if (onFail) onFail(xhr3)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (onFail) onFail(xhr2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function doPatchResources() {
|
||||
const resources = items
|
||||
.map(function (x) {
|
||||
if (!x) return null
|
||||
const n = x.name
|
||||
if (!n) return null
|
||||
if (hasResourceNames && typeof n === 'string' && n.indexOf('resources/') !== 0) return null
|
||||
return { name: n }
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
// Prefer the dedicated subresource endpoint when available.
|
||||
global.$
|
||||
.ajax({
|
||||
url: url + '/resources',
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify({ name: memoName, resources: resources }),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr0) {
|
||||
if (!isNotFoundLikeXhr(xhr0) && xhr0 && xhr0.status && xhr0.status !== 400) {
|
||||
// continue; try UpdateMemo flow below.
|
||||
}
|
||||
|
||||
// Try a loose PATCH first (some versions accept this).
|
||||
const resourcesPayloadLoose = { resources: resources }
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: url,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(resourcesPayloadLoose),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr2) {
|
||||
// v0.24 expects UpdateMemo with an update mask when modifying resources.
|
||||
// The gateway commonly accepts `updateMask=resources` as a query param and a
|
||||
// Memo body containing `name` + `resources`.
|
||||
if (!isNotFoundLikeXhr(xhr2) && xhr2 && xhr2.status !== 400) {
|
||||
if (onFail) onFail(xhr2)
|
||||
return
|
||||
}
|
||||
|
||||
const updateUrl = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'updateMask=resources'
|
||||
const resourcesPayloadV024 = {
|
||||
name: memoName,
|
||||
resources: resources
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: updateUrl,
|
||||
type: 'PATCH',
|
||||
data: JSON.stringify(resourcesPayloadV024),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
onSuccess(data, 'resources')
|
||||
})
|
||||
.fail(function (xhr3) {
|
||||
if (onFail) onFail(xhr3)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// If the list clearly contains v0.24-style resource names, go directly to the
|
||||
// resource linking flow. If it contains attachment names, go attachment flow.
|
||||
if (hasResourceNames && !hasAttachmentNames) {
|
||||
doPatchResources()
|
||||
return
|
||||
}
|
||||
if (hasAttachmentNames && !hasResourceNames) {
|
||||
doPatchAttachments()
|
||||
return
|
||||
}
|
||||
|
||||
// Default to attachments first, then fallback to resources.
|
||||
doPatchAttachments()
|
||||
}
|
||||
|
||||
global.MemosApiModern = {
|
||||
extractUserIdFromAuthResponse: extractUserIdFromAuthResponse,
|
||||
extractMemosListFromResponse: extractMemosListFromResponse,
|
||||
isNotFoundLikeXhr: isNotFoundLikeXhr,
|
||||
authWithFallback: authWithFallback,
|
||||
fetchMemosWithFallback: fetchMemosWithFallback,
|
||||
uploadAttachmentOrResource: uploadAttachmentOrResource,
|
||||
patchMemoWithAttachmentsOrResources: patchMemoWithAttachmentsOrResources
|
||||
}
|
||||
})(window)
|
||||
@@ -0,0 +1,286 @@
|
||||
(function (global) {
|
||||
'use strict'
|
||||
|
||||
function isNotFoundLikeXhr(jqXhr) {
|
||||
const status = jqXhr && jqXhr.status
|
||||
return status === 404 || status === 405
|
||||
}
|
||||
|
||||
function extractMemoListFromResponse(data) {
|
||||
if (!data) return []
|
||||
if (Array.isArray(data)) return data
|
||||
if (Array.isArray(data.memos)) return data.memos
|
||||
if (data.data && Array.isArray(data.data.memos)) return data.data.memos
|
||||
if (Array.isArray(data.list)) return data.list
|
||||
return []
|
||||
}
|
||||
|
||||
function extractMemoEntityFromResponse(data) {
|
||||
if (!data) return data
|
||||
if (data.memo) return data.memo
|
||||
if (data.data && data.data.memo) return data.data.memo
|
||||
if (data.data && (data.data.id != null || data.data.name || data.data.content)) return data.data
|
||||
return data
|
||||
}
|
||||
|
||||
function extractResourceEntityFromResponse(data) {
|
||||
if (!data) return data
|
||||
if (data.resource) return data.resource
|
||||
if (data.data && data.data.resource) return data.data.resource
|
||||
if (data.data && (data.data.id != null || data.data.name || data.data.filename)) return data.data
|
||||
return data
|
||||
}
|
||||
|
||||
function requestGet(url, headers, success, fail) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
if (success) success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
function requestPostJson(url, headers, body, success, fail) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: body != null ? JSON.stringify(body) : null,
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
if (success) success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
function requestPatchJson(url, headers, body, success, fail) {
|
||||
global.$
|
||||
.ajax({
|
||||
url: url,
|
||||
type: 'PATCH',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: body != null ? JSON.stringify(body) : null,
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
if (success) success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
// v1 memo list: GET /api/v1/memo
|
||||
// Query params (v0.20/v0.21): limit/offset/rowStatus/content/tag (best-effort)
|
||||
function listMemos(info, options, success, fail) {
|
||||
const opt = options || {}
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
|
||||
const limit = opt.limit && Number.isFinite(opt.limit) ? Math.max(1, Math.floor(opt.limit)) : 1000
|
||||
const offset = opt.offset && Number.isFinite(opt.offset) ? Math.max(0, Math.floor(opt.offset)) : null
|
||||
const rowStatus = typeof opt.rowStatus === 'string' && opt.rowStatus ? opt.rowStatus : 'NORMAL'
|
||||
|
||||
const content = typeof opt.contentSearch === 'string' ? opt.contentSearch : ''
|
||||
const tag = typeof opt.tagSearch === 'string' ? opt.tagSearch : ''
|
||||
|
||||
let qs = '?limit=' + encodeURIComponent(String(limit))
|
||||
if (offset != null) qs += '&offset=' + encodeURIComponent(String(offset))
|
||||
if (rowStatus) qs += '&rowStatus=' + encodeURIComponent(String(rowStatus))
|
||||
if (content) qs += '&content=' + encodeURIComponent(String(content))
|
||||
if (tag) qs += '&tag=' + encodeURIComponent(String(tag).replace(/^#/, ''))
|
||||
|
||||
requestGet(
|
||||
info.apiUrl + 'api/v1/memo' + qs,
|
||||
headers,
|
||||
function (data) {
|
||||
if (success) success({ memos: extractMemoListFromResponse(data) })
|
||||
},
|
||||
function (xhr) {
|
||||
// Some builds might expose plural `/api/v1/memos`; try as a last resort (still v1).
|
||||
if (isNotFoundLikeXhr(xhr)) {
|
||||
requestGet(
|
||||
info.apiUrl + 'api/v1/memos' + qs,
|
||||
headers,
|
||||
function (data2) {
|
||||
if (success) success({ memos: extractMemoListFromResponse(data2) })
|
||||
},
|
||||
fail
|
||||
)
|
||||
return
|
||||
}
|
||||
if (fail) fail(xhr)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function createMemo(info, body, success, fail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
requestPostJson(
|
||||
info.apiUrl + 'api/v1/memo',
|
||||
headers,
|
||||
body,
|
||||
function (data) {
|
||||
if (success) success(extractMemoEntityFromResponse(data))
|
||||
},
|
||||
function (xhr) {
|
||||
// Last resort: plural route.
|
||||
if (isNotFoundLikeXhr(xhr)) {
|
||||
requestPostJson(
|
||||
info.apiUrl + 'api/v1/memos',
|
||||
headers,
|
||||
body,
|
||||
function (data2) {
|
||||
if (success) success(extractMemoEntityFromResponse(data2))
|
||||
},
|
||||
fail
|
||||
)
|
||||
return
|
||||
}
|
||||
if (fail) fail(xhr)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function patchMemo(info, memoId, patch, success, fail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
const id = memoId != null ? String(memoId) : ''
|
||||
if (!id) {
|
||||
if (fail) fail({ status: 400 })
|
||||
return
|
||||
}
|
||||
|
||||
requestPatchJson(
|
||||
info.apiUrl + 'api/v1/memo/' + encodeURIComponent(id),
|
||||
headers,
|
||||
patch,
|
||||
function (data) {
|
||||
if (success) success(extractMemoEntityFromResponse(data))
|
||||
},
|
||||
fail
|
||||
)
|
||||
}
|
||||
|
||||
function getTagList(info, success, fail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
requestGet(
|
||||
info.apiUrl + 'api/v1/tag',
|
||||
headers,
|
||||
function (data) {
|
||||
const list = Array.isArray(data) ? data : Array.isArray(data.tags) ? data.tags : []
|
||||
const out = list
|
||||
.map(function (t) {
|
||||
if (!t) return ''
|
||||
if (typeof t === 'string') return t
|
||||
if (typeof t.name === 'string') return t.name
|
||||
if (typeof t.tag === 'string') return t.tag
|
||||
return ''
|
||||
})
|
||||
.map(function (s) {
|
||||
return String(s).replace(/^#/, '').trim()
|
||||
})
|
||||
.filter(Boolean)
|
||||
if (success) success(out)
|
||||
},
|
||||
fail
|
||||
)
|
||||
}
|
||||
|
||||
function getTagSuggestion(info, success, fail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
requestGet(
|
||||
info.apiUrl + 'api/v1/tag/suggestion',
|
||||
headers,
|
||||
function (data) {
|
||||
const list = Array.isArray(data) ? data : []
|
||||
const out = list
|
||||
.map(function (s) {
|
||||
return String(s).replace(/^#/, '').trim()
|
||||
})
|
||||
.filter(Boolean)
|
||||
if (success) success(out)
|
||||
},
|
||||
function (xhr) {
|
||||
// Some forks might only expose list.
|
||||
if (isNotFoundLikeXhr(xhr)) {
|
||||
getTagList(info, success, fail)
|
||||
return
|
||||
}
|
||||
if (fail) fail(xhr)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function uploadResourceBlob(info, file, meta, success, fail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
const url = info.apiUrl + 'api/v1/resource/blob'
|
||||
|
||||
const m = meta || {}
|
||||
const filename = String(m.filename || (file && file.name) || 'upload')
|
||||
|
||||
const form = new FormData()
|
||||
if (file) form.append('file', file, filename)
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
data: form,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'json',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
if (success) success(extractResourceEntityFromResponse(data))
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
function deleteResource(info, resourceId, success, fail) {
|
||||
const headers = { Authorization: 'Bearer ' + info.apiTokens }
|
||||
const id = resourceId != null ? String(resourceId) : ''
|
||||
if (!id) {
|
||||
if (fail) fail({ status: 400 })
|
||||
return
|
||||
}
|
||||
|
||||
global.$
|
||||
.ajax({
|
||||
url: info.apiUrl + 'api/v1/resource/' + encodeURIComponent(id),
|
||||
type: 'DELETE',
|
||||
headers: headers
|
||||
})
|
||||
.done(function (data) {
|
||||
if (success) success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
global.MemosApiV020V021 = {
|
||||
listMemos: listMemos,
|
||||
createMemo: createMemo,
|
||||
patchMemo: patchMemo,
|
||||
getTagList: getTagList,
|
||||
getTagSuggestion: getTagSuggestion,
|
||||
uploadResourceBlob: uploadResourceBlob,
|
||||
deleteResource: deleteResource
|
||||
}
|
||||
})(window)
|
||||
@@ -0,0 +1,119 @@
|
||||
(function (global) {
|
||||
'use strict'
|
||||
|
||||
function buildFilter(parts) {
|
||||
const p = parts || {}
|
||||
const exprs = []
|
||||
|
||||
if (p.creator) {
|
||||
// v0.23 expects a CEL string variable `creator`.
|
||||
exprs.push('creator == ' + JSON.stringify(String(p.creator)))
|
||||
}
|
||||
|
||||
if (Array.isArray(p.visibilities) && p.visibilities.length > 0) {
|
||||
const list = p.visibilities.map(function (v) {
|
||||
return JSON.stringify(String(v))
|
||||
})
|
||||
exprs.push('visibilities == [' + list.join(',') + ']')
|
||||
}
|
||||
|
||||
if (typeof p.contentSearch === 'string' && p.contentSearch.length > 0) {
|
||||
exprs.push('content_search == [' + JSON.stringify(String(p.contentSearch)) + ']')
|
||||
}
|
||||
|
||||
if (typeof p.rowStatus === 'string' && p.rowStatus.length > 0) {
|
||||
exprs.push('row_status == ' + JSON.stringify(String(p.rowStatus)))
|
||||
}
|
||||
|
||||
if (Array.isArray(p.tagSearch) && p.tagSearch.length > 0) {
|
||||
const list = p.tagSearch.map(function (t) {
|
||||
return JSON.stringify(String(t).replace(/^#/, ''))
|
||||
})
|
||||
exprs.push('tag_search == [' + list.join(',') + ']')
|
||||
}
|
||||
|
||||
if (typeof p.random === 'boolean') {
|
||||
exprs.push('random == ' + (p.random ? 'true' : 'false'))
|
||||
}
|
||||
|
||||
if (typeof p.limit === 'number' && Number.isFinite(p.limit) && p.limit > 0) {
|
||||
exprs.push('limit == ' + String(Math.floor(p.limit)))
|
||||
}
|
||||
|
||||
return exprs.join(' && ')
|
||||
}
|
||||
|
||||
function extractTagsFromMemo(memo) {
|
||||
if (!memo) return []
|
||||
|
||||
// v0.23: tags live in memo.property.tags
|
||||
if (memo.property && Array.isArray(memo.property.tags)) return memo.property.tags
|
||||
|
||||
// Defensive: some versions/serializers may use `properties` instead of `property`.
|
||||
if (memo.properties && Array.isArray(memo.properties.tags)) return memo.properties.tags
|
||||
|
||||
// Defensive: some JSON serializers may wrap repeated fields.
|
||||
if (memo.property && memo.property.tags && Array.isArray(memo.property.tags.values)) {
|
||||
return memo.property.tags.values
|
||||
}
|
||||
|
||||
if (memo.properties && memo.properties.tags && Array.isArray(memo.properties.tags.values)) {
|
||||
return memo.properties.tags.values
|
||||
}
|
||||
|
||||
// Fallback: parse tags from content, e.g. "#tag".
|
||||
const content = typeof memo.content === 'string' ? memo.content : ''
|
||||
if (!content) return []
|
||||
|
||||
const found = []
|
||||
// Match any hashtag token; server-side parser is stricter, but we want a lenient UI fallback.
|
||||
const re = /#([^\s#]+)/g
|
||||
let m
|
||||
while ((m = re.exec(content))) {
|
||||
let tag = m[1] || ''
|
||||
// Trim trailing punctuation/brackets commonly attached in markdown.
|
||||
tag = tag.replace(/[\]\[\)\(\}\{"'.,;:!?]+$/g, '')
|
||||
tag = tag.replace(/^#+/, '')
|
||||
tag = tag.trim()
|
||||
if (!tag) continue
|
||||
if (tag.length > 64) tag = tag.slice(0, 64)
|
||||
found.push(tag)
|
||||
}
|
||||
|
||||
return Array.from(new Set(found))
|
||||
}
|
||||
|
||||
function listMemos(info, options, success, fail) {
|
||||
const opt = options || {}
|
||||
const pageSize = opt.pageSize && Number.isFinite(opt.pageSize) ? Math.max(1, Math.floor(opt.pageSize)) : 1000
|
||||
const filterExpr = typeof opt.filterExpr === 'string' ? opt.filterExpr : ''
|
||||
|
||||
const qs =
|
||||
'?pageSize=' +
|
||||
encodeURIComponent(String(pageSize)) +
|
||||
(filterExpr ? '&filter=' + encodeURIComponent(filterExpr) : '')
|
||||
|
||||
// v0.23 removed the user-scoped memos endpoint: `/api/v1/users/{id}/memos`.
|
||||
// Don't reuse fetchMemosWithFallback() because it will always emit an extra 404 first.
|
||||
global.$
|
||||
.ajax({
|
||||
url: info.apiUrl + 'api/v1/memos' + qs,
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: { Authorization: 'Bearer ' + info.apiTokens }
|
||||
})
|
||||
.done(function (data) {
|
||||
success(data)
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
if (fail) fail(xhr)
|
||||
})
|
||||
}
|
||||
|
||||
global.MemosApiV023 = {
|
||||
buildFilter: buildFilter,
|
||||
listMemos: listMemos,
|
||||
extractTagsFromMemo: extractTagsFromMemo
|
||||
}
|
||||
})(window)
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,210 @@
|
||||
const UI_LANGUAGE_STORAGE_KEY = 'uiLanguage'
|
||||
|
||||
const SUPPORTED_UI_LANGUAGES = new Set(['auto', 'en', 'zh_CN', 'ja', 'ko'])
|
||||
|
||||
function normalizeUiLanguage(value) {
|
||||
const lang = String(value || 'auto')
|
||||
return SUPPORTED_UI_LANGUAGES.has(lang) ? lang : 'auto'
|
||||
}
|
||||
|
||||
function storageSyncGet(defaults) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.sync.get(defaults, (items) => resolve(items || {}))
|
||||
})
|
||||
}
|
||||
|
||||
function storageSyncSet(items) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.sync.set(items, () => resolve())
|
||||
})
|
||||
}
|
||||
|
||||
async function loadLocaleMessages(locale) {
|
||||
if (!locale || locale === 'auto') return null
|
||||
try {
|
||||
const url = chrome.runtime.getURL(`_locales/${locale}/messages.json`)
|
||||
const resp = await fetch(url)
|
||||
if (!resp.ok) return null
|
||||
return await resp.json()
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatSubstitutions(message, substitutions) {
|
||||
if (!message) return ''
|
||||
if (substitutions == null) return message
|
||||
const subs = Array.isArray(substitutions) ? substitutions : [substitutions]
|
||||
let out = message
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
const v = String(subs[i])
|
||||
out = out.replaceAll(`$${i + 1}`, v)
|
||||
out = out.replace('%s', v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
let currentUiLanguage = 'auto'
|
||||
let overrideMessages = null
|
||||
|
||||
function getLanguageToggleLabel(lang) {
|
||||
if (lang === 'en') return 'EN'
|
||||
if (lang === 'zh_CN') return '中'
|
||||
if (lang === 'ja') return '日'
|
||||
if (lang === 'ko') return '한'
|
||||
return 'A'
|
||||
}
|
||||
|
||||
function syncLanguageToggleText(lang) {
|
||||
const text = document.getElementById('langToggleText')
|
||||
if (text) text.textContent = getLanguageToggleLabel(lang)
|
||||
}
|
||||
|
||||
function syncLanguageMenuState(lang) {
|
||||
const items = document.querySelectorAll('.lang-menu-item')
|
||||
items.forEach((item) => {
|
||||
const isActive = item.getAttribute('data-lang') === lang
|
||||
item.classList.toggle('active', isActive)
|
||||
item.setAttribute('aria-checked', isActive ? 'true' : 'false')
|
||||
})
|
||||
}
|
||||
|
||||
function setLanguageMenuOpen(isOpen) {
|
||||
const toggle = document.getElementById('langToggle')
|
||||
const menu = document.getElementById('langMenu')
|
||||
if (!toggle || !menu) return
|
||||
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false')
|
||||
menu.classList.toggle('hidden', !isOpen)
|
||||
}
|
||||
|
||||
function t(key, substitutions) {
|
||||
const msg = overrideMessages && overrideMessages[key] && overrideMessages[key].message
|
||||
if (typeof msg === 'string' && msg.length > 0) {
|
||||
return formatSubstitutions(msg, substitutions)
|
||||
}
|
||||
const chromeMsg = chrome.i18n.getMessage(key, substitutions) || ''
|
||||
return formatSubstitutions(chromeMsg, substitutions)
|
||||
}
|
||||
|
||||
function setText(id, messageKey) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) el.textContent = t(messageKey)
|
||||
}
|
||||
|
||||
function setPlaceholder(id, messageKey) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) el.placeholder = t(messageKey)
|
||||
}
|
||||
|
||||
function setTitle(id, messageKey) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) el.title = t(messageKey)
|
||||
}
|
||||
|
||||
function applyStaticI18n() {
|
||||
setText('saveSettings', 'saveBtn')
|
||||
setText('saveTag', 'saveBtn')
|
||||
|
||||
setText('supportedMemosVersion', 'supportedMemosVersion')
|
||||
setText('settingsConnectionTitle', 'settingsConnectionTitle')
|
||||
setText('settingsConnectionDesc', 'settingsConnectionDesc')
|
||||
setText('settingsPostingTitle', 'settingsPostingTitle')
|
||||
setText('settingsPostingDesc', 'settingsPostingDesc')
|
||||
|
||||
setPlaceholder('apiUrl', 'placeApiUrl')
|
||||
setPlaceholder('apiTokens', 'placeApiTokens')
|
||||
setPlaceholder('content', 'placeContent')
|
||||
|
||||
setText('lockPrivate', 'lockPrivate')
|
||||
setText('lockProtected', 'lockProtected')
|
||||
setText('lockPublic', 'lockPublic')
|
||||
|
||||
setText('content_submit_text', 'submitBtn')
|
||||
const fullscreen = document.getElementById('fullscreen')
|
||||
if (fullscreen) fullscreen.setAttribute('aria-label', t('tipFullscreen'))
|
||||
|
||||
setPlaceholder('hideInput', 'placeHideInput')
|
||||
setPlaceholder('showInput', 'placeShowInput')
|
||||
setPlaceholder('attachmentOnlyDefaultText', 'placeAttachmentOnlyDefaultText')
|
||||
|
||||
setText('uploadlist-title', 'uploadedListTitle')
|
||||
|
||||
// Language switcher
|
||||
setText('langOptionAuto', 'langAuto')
|
||||
setText('langOptionEn', 'langEnglish')
|
||||
setText('langOptionZhCN', 'langChineseSimplified')
|
||||
setText('langOptionJa', 'langJapanese')
|
||||
setText('langOptionKo', 'langKorean')
|
||||
setTitle('langToggle', 'tipLanguage')
|
||||
const langToggle = document.getElementById('langToggle')
|
||||
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
|
||||
|
||||
// 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')
|
||||
setTitle('fullscreen', 'tipFullscreen')
|
||||
setTitle('editor-resize-handle', 'tipResize')
|
||||
}
|
||||
|
||||
async function setUiLanguage(nextLang, { persist = true } = {}) {
|
||||
const lang = normalizeUiLanguage(nextLang)
|
||||
currentUiLanguage = lang
|
||||
overrideMessages = await loadLocaleMessages(lang)
|
||||
applyStaticI18n()
|
||||
syncLanguageToggleText(lang)
|
||||
syncLanguageMenuState(lang)
|
||||
|
||||
if (persist) await storageSyncSet({ [UI_LANGUAGE_STORAGE_KEY]: lang })
|
||||
window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang } }))
|
||||
}
|
||||
|
||||
async function initLanguageSwitcher() {
|
||||
const switcher = document.getElementById('lang_switcher')
|
||||
const toggle = document.getElementById('langToggle')
|
||||
const langItems = document.querySelectorAll('.lang-menu-item')
|
||||
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
const isOpen = toggle.getAttribute('aria-expanded') === 'true'
|
||||
setLanguageMenuOpen(!isOpen)
|
||||
})
|
||||
}
|
||||
|
||||
langItems.forEach((item) => {
|
||||
item.addEventListener('click', async (event) => {
|
||||
event.stopPropagation()
|
||||
setLanguageMenuOpen(false)
|
||||
await setUiLanguage(item.getAttribute('data-lang'))
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!switcher || switcher.contains(event.target)) return
|
||||
setLanguageMenuOpen(false)
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') setLanguageMenuOpen(false)
|
||||
})
|
||||
|
||||
const storedItems = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
|
||||
const stored = normalizeUiLanguage(storedItems[UI_LANGUAGE_STORAGE_KEY])
|
||||
await setUiLanguage(stored, { persist: false })
|
||||
setLanguageMenuOpen(false)
|
||||
}
|
||||
|
||||
window.t = t
|
||||
window.setUiLanguage = setUiLanguage
|
||||
window.getUiLanguage = () => currentUiLanguage
|
||||
|
||||
applyStaticI18n()
|
||||
window.i18nReady = initLanguageSwitcher()
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ja=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),a={name:"ja",relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1か月",MM:"%dか月",y:"1年",yy:"%d年"}};return t.default.locale(a,null,!0),a}));
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ko=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),a={name:"ko",relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",m:"1분",mm:"%d분",h:"1시간",hh:"%d시간",d:"하루",dd:"%d일",M:"1개월",MM:"%d개월",y:"1년",yy:"%d년"}};return t.default.locale(a,null,!0),a}));
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 消息提示组件
|
||||
*
|
||||
* 1.调用
|
||||
* 字符串类型参数: $.message('成功');
|
||||
* 对象型参数:$.message({});
|
||||
*
|
||||
* 2.参数详解
|
||||
* message:' 操作成功', //提示信息
|
||||
time:'2000', //显示时间(默认:2s)
|
||||
type:'success', //显示类型,包括4种:success.error,info,warning
|
||||
showClose:false, //显示关闭按钮(默认:否)
|
||||
autoClose:true, //是否自动关闭(默认:是)
|
||||
*
|
||||
* type:success,error,info,warning
|
||||
*/
|
||||
|
||||
$.extend({
|
||||
message: function(options) {
|
||||
var defaults={
|
||||
message:' 操作成功',
|
||||
time:'2000',
|
||||
autoClose: true,
|
||||
onClose:function(){}
|
||||
};
|
||||
|
||||
if(typeof options === 'string'){
|
||||
defaults.message=options;
|
||||
}
|
||||
if(typeof options === 'object'){
|
||||
defaults=$.extend({},defaults,options);
|
||||
}
|
||||
//message模版
|
||||
var template='<div class="tip animate bounceIn">\n' +
|
||||
' <p class="tip-info">'+defaults.message+'</p>\n' +
|
||||
'</div>';
|
||||
var _this=this;
|
||||
var $body=$('body');
|
||||
var $message=$(template);
|
||||
var timer;
|
||||
|
||||
//移除所有并插入该消息
|
||||
$('.tip').remove();
|
||||
$body.append($message);
|
||||
//居中
|
||||
$message.css({
|
||||
'margin-left':'-'+$message.width()/2+'px'
|
||||
});
|
||||
|
||||
|
||||
//自动关闭
|
||||
if (defaults.autoClose){
|
||||
timer=setTimeout(function(){
|
||||
closeFn();
|
||||
},defaults.time);
|
||||
}
|
||||
//关闭
|
||||
var closeFn = function(){
|
||||
$message.addClass('hide');
|
||||
$message.remove();
|
||||
defaults.onClose(defaults);
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
});
|
||||
+1259
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(r="undefined"!=typeof globalThis?globalThis:r||self).dayjs_plugin_relativeTime=e()}(this,(function(){"use strict";return function(r,e,t){r=r||{};var n=e.prototype,o={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function i(r,e,t,o){return n.fromToBase(r,e,t,o)}t.en.relativeTime=o,n.fromToBase=function(e,n,i,d,u){for(var f,a,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:"s",r:44,d:"second"},{l:"m",r:89},{l:"mm",r:44,d:"minute"},{l:"h",r:89},{l:"hh",r:21,d:"hour"},{l:"d",r:35},{l:"dd",r:25,d:"day"},{l:"M",r:45},{l:"MM",r:10,d:"month"},{l:"y",r:17},{l:"yy",d:"year"}],m=h.length,c=0;c<m;c+=1){var y=h[c];y.d&&(f=d?t(e).diff(i,y.d,!0):i.diff(e,y.d,!0));var p=(r.rounding||Math.round)(Math.abs(f));if(s=f>0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(""+p)),a="string"==typeof v?v.replace("%d",p):v(p,n,y.l,s);break}}if(n)return a;var M=s?l.future:l.past;return"function"==typeof M?M(a):M.replace("%s",a)},n.to=function(r,e){return i(r,e,this,!0)},n.from=function(r,e){return i(r,e,this)};var d=function(r){return r.$u?t.utc():t()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}}));
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* ViewImage.min.js 2.0.2
|
||||
* MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||
* https://tokinx.github.io/ViewImage/
|
||||
*/
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(b){return b.raw=b};$jscomp.createTemplateTagFirstArgWithRaw=function(b,a){b.raw=a;return b};$jscomp.arrayIteratorImpl=function(b){var a=0;return function(){return a<b.length?{done:!1,value:b[a++]}:{done:!0}}};$jscomp.arrayIterator=function(b){return{next:$jscomp.arrayIteratorImpl(b)}};$jscomp.makeIterator=function(b){var a="undefined"!=typeof Symbol&&Symbol.iterator&&b[Symbol.iterator];return a?a.call(b):$jscomp.arrayIterator(b)};
|
||||
$jscomp.arrayFromIterator=function(b){for(var a,d=[];!(a=b.next()).done;)d.push(a.value);return d};$jscomp.arrayFromIterable=function(b){return b instanceof Array?b:$jscomp.arrayFromIterator($jscomp.makeIterator(b))};
|
||||
(function(){window.ViewImage=new function(){var b=this;this.target="[view-image] img";this.listener=function(a){if(!(a.ctrlKey||a.metaKey||a.shiftKey||a.altKey)){var d=String(b.target.split(",").map(function(g){return g.trim()+":not([no-view])"})),c=a.target.closest(d);if(c){var e=c.closest("[view-image]")||document.body;d=[].concat($jscomp.arrayFromIterable(e.querySelectorAll(d))).map(function(g){return g.href||g.src});b.display(d,c.href||c.src);a.stopPropagation();a.preventDefault()}}};this.init=
|
||||
function(a){a&&(b.target=a);["removeEventListener","addEventListener"].forEach(function(d){document[d]("click",b.listener,!1)})};this.display=function(a,d){var c=a.indexOf(d),e=(new DOMParser).parseFromString('\n <div class="view-image">\n <style>.view-image{position:fixed;inset:0;z-index:500;padding:1rem;display:flex;flex-direction:column;animation:view-image-in 300ms;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.view-image__out{animation:view-image-out 300ms}@keyframes view-image-in{0%{opacity:0}}@keyframes view-image-out{100%{opacity:0}}.view-image-btn{width:32px;height:32px;display:flex;justify-content:center;align-items:center;cursor:pointer;border-radius:3px;background-color:rgba(255,255,255,0.2)}.view-image-btn:hover{background-color:rgba(255,255,255,0.5)}.view-image-close__full{position:absolute;inset:0;background-color:rgba(48,55,66,0.3);z-index:unset;cursor:zoom-out;margin:0}.view-image-container{height:0;flex:1;display:flex;align-items:center;justify-content:center;}.view-image-lead{display:contents}.view-image-lead img{position:relative;z-index:1;max-width:100%;max-height:100%;object-fit:contain;border-radius:3px}.view-image-lead__in img{animation:view-image-lead-in 300ms}.view-image-lead__out img{animation:view-image-lead-out 300ms forwards}@keyframes view-image-lead-in{0%{opacity:0;transform:translateY(-20px)}}@keyframes view-image-lead-out{100%{opacity:0;transform:translateY(20px)}}[class*=__out] ~ .view-image-loading{display:block}.view-image-loading{position:absolute;inset:50%;width:8rem;height:2rem;color:#aab2bd;overflow:hidden;text-align:center;margin:-1rem -4rem;z-index:1;display:none}.view-image-loading::after{content:"";position:absolute;inset:50% 0;width:100%;height:3px;background:rgba(255,255,255,0.5);transform:translateX(-100%) translateY(-50%);animation:view-image-loading 800ms -100ms ease-in-out infinite}@keyframes view-image-loading{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}.view-image-tools{position:relative;display:flex;justify-content:space-between;align-content:center;color:#fff;max-width:600px;position: absolute; bottom: 5%; left: 1rem; right: 1rem; backdrop-filter: blur(10px);margin:0 auto;padding:10px;border-radius:5px;background:rgba(0,0,0,0.1);margin-bottom:constant(safe-area-inset-bottom);margin-bottom:env(safe-area-inset-bottom);z-index:1}.view-image-tools__count{width:60px;display:flex;align-items:center;justify-content:center}.view-image-tools__flip{display:flex;gap:10px}.view-image-tools [class*=-close]{margin:0 10px}</style>\n <div class="view-image-container">\n <div class="view-image-lead"></div>\n <div class="view-image-loading"></div>\n <div class="view-image-close view-image-close__full"></div>\n </div>\n <div class="view-image-tools">\n <div class="view-image-tools__count">\n <span><b class="view-image-index">'+
|
||||
(c+1)+"</b>/"+a.length+'</span>\n </div>\n <div class="view-image-tools__flip">\n <div class="view-image-btn view-image-tools__flip-prev">\n <svg width="20" height="20" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M31 36L19 24L31 12" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n <div class="view-image-btn view-image-tools__flip-next">\n <svg width="20" height="20" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M19 12L31 24L19 36" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n </div>\n <div class="view-image-btn view-image-close">\n <svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M8 8L40 40" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 40L40 8" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n </div>\n </div>\n ',
|
||||
"text/html").body.firstChild,g=function(f){var h={Escape:"close",ArrowLeft:"tools__flip-prev",ArrowRight:"tools__flip-next"};h[f.key]&&e.querySelector(".view-image-"+h[f.key]).click()},l=function(f){var h=new Image,k=e.querySelector(".view-image-lead");k.className="view-image-lead view-image-lead__out";setTimeout(function(){k.innerHTML="";h.onload=function(){setTimeout(function(){k.innerHTML='<img src="'+h.src+'" alt="ViewImage" no-view/>';k.className="view-image-lead view-image-lead__in"},100)};
|
||||
h.src=f},300)};document.body.appendChild(e);l(d);window.addEventListener("keydown",g);e.onclick=function(f){f.target.closest(".view-image-close")?(window.removeEventListener("keydown",g),e.onclick=null,e.classList.add("view-image__out"),setTimeout(function(){return e.remove()},290)):f.target.closest(".view-image-tools__flip")&&(c=f.target.closest(".view-image-tools__flip-prev")?0===c?a.length-1:c-1:c===a.length-1?0:c+1,l(a[c]),e.querySelector(".view-image-index").innerHTML=c+1)}}}})();
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_zh_cn=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),d={name:"zh-cn",weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),ordinal:function(e,_){return"W"===_?e+"周":e+"日"},weekStart:1,yearStart:4,formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah点mm分",LLLL:"YYYY年M月D日ddddAh点mm分",l:"YYYY/M/D",ll:"YYYY年M月D日",lll:"YYYY年M月D日 HH:mm",llll:"YYYY年M月D日dddd HH:mm"},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},meridiem:function(e,_){var t=100*e+_;return t<600?"凌晨":t<900?"早上":t<1100?"上午":t<1300?"中午":t<1800?"下午":"晚上"}};return t.default.locale(d,null,!0),d}));
|
||||
Reference in New Issue
Block a user