mirror of
https://github.com/Jonnyan404/memos-bber.git
synced 2026-04-25 12:08:37 +09:00
d307741f1f
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.
521 lines
17 KiB
JavaScript
521 lines
17 KiB
JavaScript
(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) |