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,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)
|
||||
Reference in New Issue
Block a user