diff --git a/src/tw-html-nodejs-sync/Config/SyncInterval.tid b/src/tw-html-nodejs-sync/Config/SyncInterval.tid new file mode 100644 index 0000000..c5b486d --- /dev/null +++ b/src/tw-html-nodejs-sync/Config/SyncInterval.tid @@ -0,0 +1,4 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/Config/SyncInterval +description: minutes between sync + +3 \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/Config/TiddlersPrefixToNotSync.tid b/src/tw-html-nodejs-sync/Config/TiddlersPrefixToNotSync.tid new file mode 100644 index 0000000..0ff514b --- /dev/null +++ b/src/tw-html-nodejs-sync/Config/TiddlersPrefixToNotSync.tid @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/Config/TiddlersPrefixToNotSync + +$:/state $:/temp \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/Config/TiddlersToNotSync.tid b/src/tw-html-nodejs-sync/Config/TiddlersToNotSync.tid new file mode 100644 index 0000000..ba5e597 --- /dev/null +++ b/src/tw-html-nodejs-sync/Config/TiddlersToNotSync.tid @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/Config/TiddlersToNotSync + +$:/HistoryList $:/StoryList $:/Import $:/language $:/layout \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/browser-background-sync.ts b/src/tw-html-nodejs-sync/browser-background-sync.ts new file mode 100644 index 0000000..ead7455 --- /dev/null +++ b/src/tw-html-nodejs-sync/browser-background-sync.ts @@ -0,0 +1,344 @@ +/* eslint-disable unicorn/no-array-callback-reference */ +import cloneDeep from 'lodash/cloneDeep'; +import mapValues from 'lodash/mapValues'; +import type { IServerStatus, ITiddlerFieldsParam, Tiddler } from 'tiddlywiki'; +import { activeServerStateTiddlerTitle, clientStatusStateTiddlerTitle, getLoopInterval } from './data/constants'; +import { filterOutNotSyncedTiddlers } from './data/filterOutNotSyncedTiddlers'; +import { getServerChangeFilter, getServerListFilter } from './data/filters'; +import { getClientInfoPoint, getFullHtmlEndPoint, getStatusEndPoint, getSyncEndPoint } from './data/getEndPoint'; +import { getSyncedTiddlersText } from './getSyncedTiddlersText'; +import type { IClientInfo, ISyncEndPointRequest } from './types'; +import { ConnectionState } from './types'; + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +exports.name = 'browser-background-sync'; +exports.platforms = ['browser']; +// modules listed in https://tiddlywiki.com/dev/#StartupMechanism +// not blocking rendering +exports.after = ['render']; +exports.synchronous = true; +/* eslint-enable @typescript-eslint/no-unsafe-member-access */ + +interface IServerInfoTiddler extends Tiddler { + fields: Tiddler['fields'] & { + ipAddress: string; + /** + * Last synced time, be undefined if never synced + */ + lastSync: string | undefined; + name: string; + port: number; + text: ConnectionState; + }; +} + +class BackgroundSyncManager { + loop: ReturnType | undefined; + loopInterval: number; + /** lock the sync for `this.syncWithServer`, while last sync is still on progress */ + lock = false; + + constructor() { + // TODO: get this from setting + this.loopInterval = getLoopInterval(); + this.setupListener(); + this.startCheckServerStatusLoop(); + } + + setupListener() { + $tw.rootWidget.addEventListener('tw-html-nodejs-sync-get-server-status', async (event) => { + await this.getServerStatus(); + }); + $tw.rootWidget.addEventListener('tw-html-nodejs-sync-set-active-server-and-sync', async (event) => { + const titleToActive = event.paramObject?.title as string | undefined; + await this.setActiveServerAndSync(titleToActive); + }); + /** handle events from src/ui/ServerItemViewTemplate.tid 's $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplate */ + $tw.rootWidget.addEventListener('tw-html-nodejs-sync-sync-start', async (event) => { + await this.start(); + }); + $tw.rootWidget.addEventListener('tw-html-nodejs-sync-download-full-html', async (event) => { + await this.downloadFullHtmlAndApplyToWiki(); + }); + } + + startCheckServerStatusLoop() { + // eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/promise-function-async + setInterval(() => this.getServerStatus(), this.loopInterval); + } + + async start(skipStatusCheck?: boolean) { + if (this.loop !== undefined) { + clearInterval(this.loop); + this.lock = false; + } + await this.onSyncStart(skipStatusCheck); + // eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/promise-function-async + this.loop = setInterval(() => this.onSyncStart(skipStatusCheck), this.loopInterval); + } + + async onSyncStart(skipStatusCheck?: boolean) { + void this.getConnectedClientStatus(); + if (this.lock) { + return; + } + this.lock = true; + try { + if (skipStatusCheck !== true) { + await this.getServerStatus(); + } + await this.syncWithServer(); + // Maybe should add lock to avoid infinite loop, if also sync after autosave. But we don't have sync after autosave yet, so no lock on this is ok. + $tw.rootWidget.dispatchEvent({ type: 'tm-auto-save-wiki' }); + } finally { + this.lock = false; + } + } + + async setActiveServerAndSync(titleToActive: string | undefined) { + try { + if (typeof titleToActive === 'string' && $tw.wiki.getTiddler(titleToActive) !== undefined) { + // update status first + await this.getServerStatus(); + // get latest tiddler + const serverToActive = $tw.wiki.getTiddler(titleToActive); + if (serverToActive !== undefined) { + const newStatus = [ConnectionState.onlineActive, ConnectionState.online].includes(serverToActive.fields.text as ConnectionState) + ? ConnectionState.onlineActive + : ConnectionState.offlineActive; + $tw.wiki.addTiddler({ ...serverToActive.fields, text: newStatus }); + this.setActiveServerTiddlerTitle(titleToActive, serverToActive.fields.lastSync); + await this.start(true); + } + } + } catch (error) { + console.error(error); + } + } + + getActiveServerTiddlerTitle() { + return $tw.wiki.getTiddlerText(activeServerStateTiddlerTitle); + } + + setActiveServerTiddlerTitle(title: string, lastSync: string | undefined) { + // update active server record in `activeServerStateTiddlerTitle`, this is a pointer tiddler point to actual server tiddler + $tw.wiki.addTiddler({ title: activeServerStateTiddlerTitle, text: title, lastSync }); + // update server's last sync + const serverToActive = $tw.wiki.getTiddler(title); + if (serverToActive !== undefined) { + $tw.wiki.addTiddler({ ...serverToActive.fields, lastSync }); + } + } + + /** On TidGi desktop, get connected client info */ + async getConnectedClientStatus() { + try { + const response: Record = await fetch(getClientInfoPoint()).then( + async (response) => (await response.json()) as Record, + ); + Object.values(response).forEach((clientInfo) => { + $tw.wiki.addTiddler({ + title: `${clientStatusStateTiddlerTitle}/${clientInfo.Origin}`, + ...clientInfo, + }); + }); + } catch (error) { + console.warn(`tw-html-nodejs-sync can't connect to tw nodejs side. Error: ${(error as Error).message}`); + } + } + + /** On Tiddloid mobile, get TidGi server status */ + async getServerStatus() { + const timeout = 3000; + const activeTiddlerTitle = this.getActiveServerTiddlerTitle(); + const serverListWithUpdatedStatus = await Promise.all( + this.serverList.map(async (serverInfoTiddler) => { + const active = serverInfoTiddler.fields.title === activeTiddlerTitle; + try { + const controller = new AbortController(); + const id = setTimeout(() => { + controller.abort(); + }, timeout); + const response: IServerStatus = await fetch(getStatusEndPoint(serverInfoTiddler.fields.ipAddress, serverInfoTiddler.fields.port), { + signal: controller.signal, + }).then(async (response) => (await response.json()) as IServerStatus); + clearTimeout(id); + if (typeof response.tiddlywiki_version === 'string') { + return { + ...serverInfoTiddler, + fields: { + ...serverInfoTiddler.fields, + text: active ? ConnectionState.onlineActive : ConnectionState.online, + }, + }; + } + } catch (error) { + if ((error as Error).message.includes('The operation was aborted')) { + $tw.wiki.addTiddler({ + title: '$:/state/notification/tw-html-nodejs-sync/notification', + text: `GetServerStatus Timeout after ${timeout / 1000}s`, + }); + } else { + console.error(`getServerStatus() ${(error as Error).message} ${serverInfoTiddler.fields.name} ${(error as Error).stack ?? ''}`); + $tw.wiki.addTiddler({ + title: '$:/state/notification/tw-html-nodejs-sync/notification', + text: `GetServerStatus Failed ${(error as Error).message}`, + }); + } + } + $tw.notifier.display('$:/state/notification/tw-html-nodejs-sync/notification'); + return { + ...serverInfoTiddler, + fields: { + ...serverInfoTiddler.fields, + text: active ? ConnectionState.offlineActive : ConnectionState.offline, + }, + }; + }), + ); + serverListWithUpdatedStatus.forEach((tiddler) => { + $tw.wiki.addTiddler(tiddler.fields); + }); + } + + async syncWithServer() { + const onlineActiveServer = this.onlineActiveServer; + + if (onlineActiveServer !== undefined) { + // fix multiple online active server + this.serverList.forEach((serverInfoTiddler) => { + if (serverInfoTiddler?.fields?.text === ConnectionState.onlineActive && serverInfoTiddler?.fields?.title !== onlineActiveServer.fields.title) { + $tw.wiki.addTiddler({ ...serverInfoTiddler.fields, text: ConnectionState.online }); + } + }); + try { + const changedTiddlersFromClient = filterOutNotSyncedTiddlers(this.currentModifiedTiddlers); + + const requestBody: ISyncEndPointRequest = { tiddlers: changedTiddlersFromClient, lastSync: onlineActiveServer.fields.lastSync }; + // TODO: handle conflict, find intersection of changedTiddlersFromServer and changedTiddlersFromClient, and write changes to each other + // send modified tiddlers to server + const changedTiddlersFromServer: ITiddlerFieldsParam[] = await fetch( + getSyncEndPoint(onlineActiveServer.fields.ipAddress, onlineActiveServer.fields.port), + { + method: 'POST', + mode: 'cors', + body: JSON.stringify(requestBody), + headers: { + 'X-Requested-With': 'TiddlyWiki', + 'Content-Type': 'application/json', + }, + // TODO: add auth token in header, after we can scan QR code to get token easily + }, + ).then(async (response) => filterOutNotSyncedTiddlers((await response.json()) as ITiddlerFieldsParam[])); + changedTiddlersFromServer.forEach((tiddler) => { + // TODO: handle conflict + $tw.wiki.addTiddler(tiddler); + }); + + $tw.wiki.addTiddler({ + title: '$:/state/notification/tw-html-nodejs-sync/notification', + text: `Sync Complete ${getSyncedTiddlersText(changedTiddlersFromClient, changedTiddlersFromServer, { client: [], server: [] })}`, + }); + this.setActiveServerTiddlerTitle(onlineActiveServer.fields.title, this.getLastSyncString()); + } catch (error) { + console.error(error); + $tw.wiki.addTiddler({ + title: '$:/state/notification/tw-html-nodejs-sync/notification', + text: `Sync Failed ${(error as Error).message}`, + }); + } + $tw.notifier.display('$:/state/notification/tw-html-nodejs-sync/notification'); + } + } + + async downloadFullHtmlAndApplyToWiki() { + const onlineActiveServer = this.onlineActiveServer; + + if (onlineActiveServer !== undefined) { + try { + const fullHtml = await fetch(getFullHtmlEndPoint(onlineActiveServer.fields.ipAddress, onlineActiveServer.fields.port), { + mode: 'cors', + headers: { + 'X-Requested-With': 'TiddlyWiki', + 'Content-Type': 'text/html', + }, + }).then(async (response) => await response.text()); + this.setActiveServerTiddlerTitle(onlineActiveServer.fields.title, this.getLastSyncString()); + // get all state tiddlers we need, before document is overwritten + const serverList = cloneDeep(this.serverList); + // overwrite + document.write(fullHtml); + document.close(); + this.#showNotification(`Full html applied, set server list back.`); + + // write back after html stabled + addEventListener('DOMContentLoaded', (event) => { + setTimeout(() => { + $tw.wiki.addTiddlers(serverList.map((tiddler) => tiddler.fields)); + }, 1000); + }); + } catch (error) { + console.error(error); + this.#showNotification(`Full html apply failed ${(error as Error).message}`); + } + } + } + + get onlineActiveServer() { + return this.serverList.find((serverInfoTiddler) => { + return serverInfoTiddler?.fields?.text === ConnectionState.onlineActive; + }); + } + + /** + * update last sync using <> + */ + getLastSyncString() { + return $tw.utils.stringifyDate(new Date()); + } + + get currentModifiedTiddlers(): ITiddlerFieldsParam[] { + const onlineActiveServer = this.onlineActiveServer; + + if (onlineActiveServer === undefined) { + return []; + } + const lastSync = onlineActiveServer.fields.lastSync; + const diffTiddlersFilter: string = getServerChangeFilter(lastSync); + const diffTiddlers: string[] = $tw.wiki.compileFilter(diffTiddlersFilter)() ?? []; + return diffTiddlers + .map($tw.wiki.getTiddler) + .filter((tiddler): tiddler is Tiddler => tiddler !== undefined) + .map( + (tiddler): ITiddlerFieldsParam => + mapValues(tiddler.fields, (value) => { + if (value instanceof Date) { + return $tw.utils.stringifyDate(value); + } + return value as string; + }), + ); + } + + get serverList() { + // get server list using filter + const serverList: string[] = $tw.wiki.compileFilter(getServerListFilter())() ?? []; + return serverList.map((serverInfoTiddlerTitle) => { + return $tw.wiki.getTiddler(serverInfoTiddlerTitle) as IServerInfoTiddler; + }); + } + + #showNotification(text: string) { + $tw.wiki.addTiddler({ + title: '$:/state/notification/tw-html-nodejs-sync/notification', + text, + }); + $tw.notifier.display('$:/state/notification/tw-html-nodejs-sync/notification'); + } +} + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +exports.startup = () => { + const syncManager = new BackgroundSyncManager(); + void syncManager.start(); +}; diff --git a/src/tw-html-nodejs-sync/browser-background-sync.ts.meta b/src/tw-html-nodejs-sync/browser-background-sync.ts.meta new file mode 100644 index 0000000..67bb47d --- /dev/null +++ b/src/tw-html-nodejs-sync/browser-background-sync.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/browser-background-sync.js +type: application/javascript +module-type: startup diff --git a/src/tw-html-nodejs-sync/data/clientInfoStore.ts b/src/tw-html-nodejs-sync/data/clientInfoStore.ts new file mode 100644 index 0000000..8d63103 --- /dev/null +++ b/src/tw-html-nodejs-sync/data/clientInfoStore.ts @@ -0,0 +1,4 @@ +import { ClientInfoStore } from './clientInfoStoreClass'; + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +module.exports.store = new ClientInfoStore(); diff --git a/src/tw-html-nodejs-sync/data/clientInfoStore.ts.meta b/src/tw-html-nodejs-sync/data/clientInfoStore.ts.meta new file mode 100644 index 0000000..0ed8fd8 --- /dev/null +++ b/src/tw-html-nodejs-sync/data/clientInfoStore.ts.meta @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/clientInfoStore.js +type: application/javascript +module-type: library diff --git a/src/tw-html-nodejs-sync/data/clientInfoStoreClass.ts b/src/tw-html-nodejs-sync/data/clientInfoStoreClass.ts new file mode 100644 index 0000000..0588801 --- /dev/null +++ b/src/tw-html-nodejs-sync/data/clientInfoStoreClass.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import structuredClone from '@ungap/structured-clone'; +import * as types from '../types'; +import { getLoopInterval } from './constants'; +import UAParser from 'ua-parser-js'; + +export class ClientInfoStore { + #clients: Record = {}; + loopHandel: NodeJS.Timer; + + constructor() { + const loopInterval = getLoopInterval(); + const keyOfflineTimeout = loopInterval * 2; + const keyDeleteTimeout = loopInterval * 10; + this.loopHandel = setInterval(() => { + Object.keys(this.#clients).forEach((key) => { + const timestamp = this.#clients[key].timestamp; + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!timestamp || Date.now() - timestamp > keyDeleteTimeout) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.#clients[key]; + } else if (Date.now() - timestamp > keyOfflineTimeout) { + this.#clients[key].state = types.ConnectionState.offline; + } + }); + }, loopInterval); + } + + get allClient() { + return structuredClone(this.#clients); + } + + updateClient(key: string, value: Partial) { + this.#clients[key] = { ...this.#clients[key], ...value }; + const ua = this.#clients[key]['User-Agent']; + if (ua) { + const userAgentInfo = new UAParser(ua); + const model = userAgentInfo.getDevice().model; + const os = userAgentInfo.getOS().name; // 获取系统 + this.#clients[key].name = model ?? userAgentInfo.getBrowser().name ?? this.#clients[key].Origin; + this.#clients[key].model = model; + this.#clients[key].os = os; + } else { + this.#clients[key].name = this.#clients[key].Origin; + } + } +} diff --git a/src/tw-html-nodejs-sync/data/constants.ts b/src/tw-html-nodejs-sync/data/constants.ts new file mode 100644 index 0000000..b98041f --- /dev/null +++ b/src/tw-html-nodejs-sync/data/constants.ts @@ -0,0 +1,4 @@ +export const activeServerStateTiddlerTitle = `$:/state/tw-html-nodejs-sync/activeServer`; +export const clientStatusStateTiddlerTitle = '$:/state/tw-html-nodejs-sync/clientStatus'; +// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions +export const getLoopInterval = () => (Number($tw.wiki.getTiddlerText('$:/plugins/linonetwo/tw-html-nodejs-sync/Config/SyncInterval')) || 3) * 60 * 1000; diff --git a/src/tw-html-nodejs-sync/data/filterOutNotSyncedTiddlers.ts b/src/tw-html-nodejs-sync/data/filterOutNotSyncedTiddlers.ts new file mode 100644 index 0000000..0fb3a88 --- /dev/null +++ b/src/tw-html-nodejs-sync/data/filterOutNotSyncedTiddlers.ts @@ -0,0 +1,15 @@ +import { ITiddlerFields, ITiddlerFieldsParam } from 'tiddlywiki'; + +let tiddlersToNotSync: Set | undefined; +let prefixToNotSync: string[] | undefined; +export const filterOutNotSyncedTiddlers = (tiddlers: T[]): T[] => { + if (tiddlersToNotSync === undefined || prefixToNotSync === undefined) { + tiddlersToNotSync = new Set( + $tw.utils.parseStringArray($tw.wiki.getTiddlerText('$:/plugins/linonetwo/tw-html-nodejs-sync/Config/TiddlersToNotSync') ?? ''), + ); + prefixToNotSync = $tw.utils.parseStringArray($tw.wiki.getTiddlerText('$:/plugins/linonetwo/tw-html-nodejs-sync/Config/TiddlersPrefixToNotSync') ?? ''); + } + return tiddlers + .filter((tiddler: T) => !prefixToNotSync!.some((prefix) => (tiddler.title as string).startsWith(prefix))) + .filter((tiddler: T) => !tiddlersToNotSync!.has(tiddler.title as string)); +}; diff --git a/src/tw-html-nodejs-sync/data/filters.ts b/src/tw-html-nodejs-sync/data/filters.ts new file mode 100644 index 0000000..3e49b2c --- /dev/null +++ b/src/tw-html-nodejs-sync/data/filters.ts @@ -0,0 +1,8 @@ +export function getServerChangeFilter(lastSync: string | undefined) { + return `[all[]] :filter[get[modified]compare:date:gt[${lastSync ?? ''}]]`; +} + +/** + * also in src/ui/ServerList.tid 's list widget + */ +export const getServerListFilter = () => $tw.wiki.getTiddlerText('$:/plugins/linonetwo/tw-html-nodejs-sync/ServerListFilter'); diff --git a/src/tw-html-nodejs-sync/data/getClientInfo.ts b/src/tw-html-nodejs-sync/data/getClientInfo.ts new file mode 100644 index 0000000..081383d --- /dev/null +++ b/src/tw-html-nodejs-sync/data/getClientInfo.ts @@ -0,0 +1,11 @@ +import type Http from 'http'; +import { ConnectionState, IClientInfo } from '../types'; + +export function getClientInfo(request: Http.ClientRequest & Http.InformationEvent, state = ConnectionState.online): Partial { + return { + Origin: request.rawHeaders[request.rawHeaders.indexOf('Origin') + 1] ?? request.rawHeaders[request.rawHeaders.indexOf('Referer') + 1], + 'User-Agent': request.rawHeaders[request.rawHeaders.indexOf('User-Agent') + 1], + timestamp: Date.now(), + state, + }; +} diff --git a/src/tw-html-nodejs-sync/data/getEndPoint.ts b/src/tw-html-nodejs-sync/data/getEndPoint.ts new file mode 100644 index 0000000..30bf6b1 --- /dev/null +++ b/src/tw-html-nodejs-sync/data/getEndPoint.ts @@ -0,0 +1,27 @@ +export const syncRoute = '/tw-html-nodejs-sync/html-node-sync'; +export const statusRoute = '/tw-html-nodejs-sync/status'; +export const clientInfoRoute = '/tw-html-nodejs-sync/client-info'; +export const fullHtmlRoute = '/tw-html-nodejs-sync/get-full-html'; + +/** + * Our custom endpoint that used to sync with the server + */ +export function getSyncEndPoint(ipAddress: string, port: number): string { + return `http://${ipAddress}:${port}${syncRoute}`; +} + +/** + * Official status endpoint + */ +export function getStatusEndPoint(ipAddress: string, port: number): string { + return `http://${ipAddress}:${port}${statusRoute}`; +} + +/** Used for NodeJS server's client page get info from the same origin */ +export function getClientInfoPoint(): string { + return `http://${location.host}${clientInfoRoute}`; +} + +export function getFullHtmlEndPoint(ipAddress: string, port: number): string { + return `http://${location.host}${fullHtmlRoute}`; +} diff --git a/src/tw-html-nodejs-sync/data/mergeTiddler.ts b/src/tw-html-nodejs-sync/data/mergeTiddler.ts new file mode 100644 index 0000000..2be685f --- /dev/null +++ b/src/tw-html-nodejs-sync/data/mergeTiddler.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { applyPatches, makePatches } from '@sanity/diff-match-patch'; +import type { ITiddlerFields } from 'tiddlywiki'; + +/** + * + * @url https://neil.fraser.name/software/diff_match_patch/demos/patch.html + * @param a + * @param b + * @returns + */ +export function mergeTiddler(a: ITiddlerFields, b: ITiddlerFields): ITiddlerFields { + if (a.title !== b.title) { + throw new Error(`Cannot merge tiddlers with different titles: ${a.title} and ${b.title}`); + } + const newerOne = a.modified > b.modified ? a : b; + if (leftIsBinaryRightIsString(a, b)) { + // we choose binary, because we assume binary data is more important than pure text + return a; + } else if (leftIsBinaryRightIsString(b, a)) { + return b; + } else if (isBinaryTiddler(a) && isBinaryTiddler(b)) { + return newerOne; + } + // both is string tiddler, we can merge them using diff-match-patch algorithm + // FIXME: Currently not working, it needs `c` that is an older version from server to work (3-way-merge), otherwise it will just use `b.text` as the merged text + const patches = makePatches(a.text, b.text); + const [mergedText] = applyPatches(patches, a.text); + const fields: ITiddlerFields = { + ...newerOne, + text: mergedText, + }; + return fields; +} + +function leftIsBinaryRightIsString(a: ITiddlerFields, b: ITiddlerFields) { + // a is binary, b is string + if (isBinaryTiddler(a) && !isBinaryTiddler(b)) { + return true; + } + return false; +} + +export function isBinaryTiddler(tiddlerFields: ITiddlerFields): boolean { + const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || 'text/vnd.tiddlywiki']; + return !!contentTypeInfo && contentTypeInfo.encoding === 'base64'; +} diff --git a/src/tw-html-nodejs-sync/data/toTWUTCString.ts b/src/tw-html-nodejs-sync/data/toTWUTCString.ts new file mode 100644 index 0000000..8395391 --- /dev/null +++ b/src/tw-html-nodejs-sync/data/toTWUTCString.ts @@ -0,0 +1,17 @@ +export function pad(number: number) { + if (number < 10) { + return `0${number}`; + } + return String(number); +} +export function toTWUTCString(date: Date) { + return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}${ + pad( + date.getUTCHours(), + ) + }${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}${ + (date.getUTCMilliseconds() / 1000) + .toFixed(3) + .slice(2, 5) + }`; +} diff --git a/src/tw-html-nodejs-sync/develop.tid b/src/tw-html-nodejs-sync/develop.tid new file mode 100755 index 0000000..95f3e48 --- /dev/null +++ b/src/tw-html-nodejs-sync/develop.tid @@ -0,0 +1,7 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/develop +creator: LinOnetwo +type: text/vnd.tiddlywiki + +TidGi-Mobile use [[$:/plugins/linonetwo/tw-html-nodejs-sync/templates/skinny-tiddlywiki5.html]] as template. See [[$:/plugins/linonetwo/tw-html-nodejs-sync/templates/about-skinny-tiddlywiki5-html]] for how it differ from original lazy-all template. + +Use [[SIOC|https://github.com/taurenshaman/semantic-web/blob/cee9e421eed3f31a602f82375fb92fb109d638b7/data/sioc.rdf]] Ontology for server tiddler. diff --git a/src/tw-html-nodejs-sync/getSyncedTiddlersText.ts b/src/tw-html-nodejs-sync/getSyncedTiddlersText.ts new file mode 100644 index 0000000..cbccf61 --- /dev/null +++ b/src/tw-html-nodejs-sync/getSyncedTiddlersText.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import take from 'lodash/take'; +import { ITiddlerFields, ITiddlerFieldsParam } from 'tiddlywiki'; + +export function getSyncedTiddlersText( + changedTiddlersFromClient: Array, + changedTiddlersFromServer: Array, + deletion: { client: string[]; server: string[] }, + options?: { reverse?: boolean }, +) { + const changedTitleDisplayLimit = 5; + + const formatList = (list: string[]) => take(list, changedTitleDisplayLimit).join(' '); + const moreCountText = (list: string[]) => list.length > changedTitleDisplayLimit ? `And ${list.length - changedTitleDisplayLimit} more` : ''; + + const clientText = formatList(changedTiddlersFromClient.map(tiddler => tiddler.caption as string ?? (tiddler.title))); + const clientCount = moreCountText(changedTiddlersFromClient.map(item => item.title as string)); + const serverText = formatList(changedTiddlersFromServer.map(tiddler => tiddler.caption as string ?? (tiddler.title))); + const serverCount = moreCountText(changedTiddlersFromServer.map(item => item.title as string)); + + const deletionClientText = formatList(deletion.client); + const deletionClientCount = moreCountText(deletion.client); + const deletionServerText = formatList(deletion.server); + const deletionServerCount = moreCountText(deletion.server); + + const up = options?.reverse ? '↓' : '↑'; + const down = options?.reverse ? '↑' : '↓'; + + return `${up} ${changedTiddlersFromClient.length} ${down} ${changedTiddlersFromServer.length}${ + changedTiddlersFromClient.length > 0 ? `\n\n${up}: ${clientText} ${clientCount}` : '' + }${changedTiddlersFromServer.length > 0 ? `\n\n${down}: ${serverText} ${serverCount}` : ''}${ + deletion.client.length > 0 ? `\n\nDeleted on Client: ${deletionClientText} ${deletionClientCount}` : '' + }${deletion.server.length > 0 ? `\n\nDeleted on Server: ${deletionServerText} ${deletionServerCount}` : ''}`; +} diff --git a/src/tw-html-nodejs-sync/plugin.info b/src/tw-html-nodejs-sync/plugin.info new file mode 100644 index 0000000..d476aec --- /dev/null +++ b/src/tw-html-nodejs-sync/plugin.info @@ -0,0 +1,13 @@ +{ + "title": "$:/plugins/linonetwo/tw-html-nodejs-sync", + "author": "LinOnetwo", + "description": "Sync data between Mobile HTML (Tiddloid/Quine2) <-> Desktop NodeJS App (TidGi) ", + "core-version": ">=5.1.22", + "plugin-type": "plugin", + "version": "0.6.5", + "list": "readme ui/ServerList", + "dependents": [ + "$:/plugins/tiddlywiki/qrcode", + "$:/plugins/kixam/datepicker" + ] +} \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/readme.tid b/src/tw-html-nodejs-sync/readme.tid new file mode 100755 index 0000000..83d97b0 --- /dev/null +++ b/src/tw-html-nodejs-sync/readme.tid @@ -0,0 +1,68 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/readme +creator: LinOnetwo +type: text/vnd.tiddlywiki + +\define image() + >/> +\end + +! Sync Between NodeJS and Mobile HTML 在桌面端(NodeJS)和移动端(HTML文件)之间同步 + +本插件可以让你在基于NodeJS技术的桌面应用(例如太记)和基于HTML文件的手机端(例如Tiddloid安卓应用)之间同步数据。 + +手机应用 ↔ 桌面应用 ↔ 云端 + +This plugin enables you sync date between NodeJS server App (e.g. TidGi App) and HTML file based mobile App (e.g. Tiddloid Android App). + +Mobile App ↔ Desktop App ↔ Cloud + +!! How to use + +首先在手机端扫码,打开当前这个知识库的网页,然后保存出 HTML 文件后(详见中文教程的[[如何保存|https://tw-cn.netlify.app/#TiddlyGit%E5%A4%AA%E8%AE%B0]]章节),在 Tiddloid 里打开保存的 HTML 文件(可能需要先将文件移出下载目录不然Tiddloid无权限访问,详见其[[说明书|https://github.com/donmor/Tiddloid]])。 + +然后打开[[服务器列表|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList]],录入一下服务器地址: + +# 将上面扫码得到的 URL 复制一下,填入[[服务器列表|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList]]里的「智能识别黏贴框」,然后点击「智能识别」按钮,会自动将 URL 解析后填入相应框内。你也可以点击「扫二维码」按钮开始扫码,扫码结果会自动填入「智能识别黏贴框」内 +# 此时,「服务器IP」和「服务器端口」框里应该要已经填好了 ip 和端口号 +# 你需要填写一下「服务器名」这个框 +# 点击「新增服务器」,会新建一个服务器信息条目,请确认创建此条目 +# 这时新的服务器应该就出现在服务器列表里了,你可以点击「启用同步并立即同步」按钮,这样就可以在桌面端和手机端同步数据了 +# 之后会每五分钟自动同步一次用户创建的条目(不包含插件),如果需要从桌面端同步插件到手机端,需要使用「[[拉取并覆盖|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/DownloadFullHtml]]」功能 + +First scan the code on your cell phone, open the current knowledge base page, then save the HTML file (see the [[How to save|https://tw-cn.netlify.app/#TiddlyGit%E5%A4%AA%E8%AE%B0]] section of the Chinese tutorial for more details) and open the saved HTML file in Tiddloid (You may need to move the file out of the download directory first or Tiddloid will not have access to it, see its [[Instructions|https://github.com/donmor/Tiddloid]] for details). + +Then open the [[Server List|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList]] and enter the following server address. + +# Copy the URL you got from the code above and fill in the "Smart Identify Sticky Box" in [[Server List|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList]], then click the "Smart Identify" button, the URL will be automatically parsed and filled in the corresponding box. You can also click the "Scan QR Code" button to start scanning the code, and the result will be automatically filled into the "Smart Identify Sticky Box". +# At this point, the "Server IP" and "Server Port" boxes should already be filled with the ip and port numbers. +# You need to fill in the "server name" box +# Click on "Add Server", a new server information entry will be created, please confirm to create this entry +# The new server should now appear in the server list, you can click the "Enable sync and sync now" button to sync data between desktop and mobile. +# If you need to sync the plugin from desktop to mobile, you need to use the "[[pull and overwrite|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/DownloadFullHtml]]" function. + +Translated with www.DeepL.com/Translator (free version) + +!! Server list and forms 服务器列表和新增表单 + +[[Server List|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList]] contains syncable servers, and button to add new server. + +!! QrCode of current server 当前服务器的二维码 + +<$set name="content" value={{{ [{$:/info/url/host}addprefix[//]addprefix{$:/info/url/protocol}addsuffix[/tw-html-nodejs-sync/get-full-html]] }}}> + <> + <> +$set> + +Scan to add this server 扫码以添加此服务器 ({{$:/info/url/full}}). + +(二维码:当前服务器的地址) + +Needs official QR plugin [[$:/plugins/tiddlywiki/qrcode]] to work. 需要安装官方二维码插件 [[$:/plugins/tiddlywiki/qrcode]] 才会有二维码出现在上面 + +!! FAQ 常见问题 + +!!! 在外面用了一天之后回家添加服务器,却无法同步到电脑端 When you come home to add a server after using it outside for a day, it won't sync to the computer side + +就是因为你新添加服务器后,它只会同步新建服务器之后新建的条目。这是又因为新建服务器时会根据当前时间添加一个 lastSync 字段,目前在点击同步按钮时,只会同步这个时间点之后的内容。你可以手动把服务器条目里这个字段的时间改早一天,这样就会同步你今天添加的内容了。未来我们将会添加更智能的同步方式,就不再需要这个字段了,就不会遇到这个问题了。 + +This is because when you add a new server, it only syncs the new entries created after the new server is created. This is again because when you create a new server you add a lastSync field based on the current time, and currently when you click the sync button it will only sync after this point in time. You can manually change the time of this field in the server entry to one day earlier, and it will sync the content you added today. In the future we will add a smarter way of syncing so that this field is no longer needed and you won't encounter this problem. diff --git a/src/tw-html-nodejs-sync/scan-qr-widget-style.tid b/src/tw-html-nodejs-sync/scan-qr-widget-style.tid new file mode 100644 index 0000000..bea4fb8 --- /dev/null +++ b/src/tw-html-nodejs-sync/scan-qr-widget-style.tid @@ -0,0 +1,29 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/scan-qr-widget-style.css +tags: $:/tags/Stylesheet +type: text/css + +#scan-qr-widget-loadingMessage { + text-align: center; + padding: 40px; + background-color: #eee; +} + +#scan-qr-widget-canvas { + width: 100%; +} + +#scan-qr-widget-output { + margin-top: 20px; + background: #eee; + padding: 10px; + padding-bottom: 0; +} + +#scan-qr-widget-output div { + padding-bottom: 10px; + word-wrap: break-word; +} + +#scan-qr-widget-noQRFound { + text-align: center; +} diff --git a/src/tw-html-nodejs-sync/scan-qr-widget.ts b/src/tw-html-nodejs-sync/scan-qr-widget.ts new file mode 100644 index 0000000..e1e74b8 --- /dev/null +++ b/src/tw-html-nodejs-sync/scan-qr-widget.ts @@ -0,0 +1,194 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import type { Widget as IWidget, IChangedTiddlers } from 'tiddlywiki'; +import jsQR from 'jsqr-es6'; +import type { Point } from 'jsqr-es6/dist/locator'; + +const Widget = (require('$:/core/modules/widgets/widget.js') as { widget: typeof IWidget }).widget; + +class ScanQRWidget extends Widget { + refresh(_changedTiddlers: IChangedTiddlers) { + return false; + } + + /** id to prevent multiple loops */ + loopId = 0; + + /** + * Lifecycle method: Render this widget into the DOM + */ + render(parent: Node, _nextSibling: Node) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + /** tiddler to put the result */ + const outputTiddler = this.getAttribute('outputTiddler'); + /** + * tiddler contains the open state of this widget. For example: + * + * ```tw5 + * <$reveal type="match" state="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" text="yes"> + <$ScanQRWidget outputTiddler="$:/state/tw-html-nodejs-sync/server/new" stopOnDetect="yes" stateTiddler="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" /> + $reveal> + * ``` + */ + const stateTiddler = this.getAttribute('stateTiddler'); + const outputField = this.getAttribute('field') || 'text'; + const stopOnDetect = this.getAttribute('stopOnDetect') === 'yes' || this.getAttribute('stopOnDetect') === 'true'; + + const containerElement = document.createElement('div'); + containerElement.innerHTML = ` + + 🎥 Unable to access video stream (please make sure you have a webcam enabled) + + + No QR code detected. + Data: + + + `; + this.domNodes.push(containerElement); + this.loopId += 1; + const loopId = this.loopId; + // wait till dom created + requestAnimationFrame(() => void this.jsqr(loopId, containerElement, { outputTiddler, stopOnDetect, stateTiddler, outputField })); + parent.appendChild(containerElement); + } + + async jsqr( + loopId: number, + containerElement: HTMLDivElement, + options: { outputField: string; outputTiddler?: string; stateTiddler?: string; stopOnDetect: boolean }, + ) { + const video = document.createElement('video'); + const canvasElement = document.querySelector('#scan-qr-widget-canvas'); + if (canvasElement === null) { + console.warn('ScanQRWidget: canvasElement is null'); + return; + } + const canvas = canvasElement.getContext('2d'); + const loadingMessage = document.querySelector('#scan-qr-widget-loadingMessage'); + const outputContainer = document.querySelector('#scan-qr-widget-output'); + const outputMessage = document.querySelector('#scan-qr-widget-outputMessage'); + const outputData = document.querySelector('#scan-qr-widget-outputData'); + if (canvas === null || outputData === null) { + console.warn('ScanQRWidget: canvas or outputData is null', { canvas, outputData }); + return; + } + + // Use facingMode: environment to attemt to get the front camera on phones + const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); + + function drawLine(begin: Point, end: Point, color: string | CanvasGradient | CanvasPattern) { + if (canvas === null) { + return; + } + canvas.beginPath(); + canvas.moveTo(begin.x, begin.y); + canvas.lineTo(end.x, end.y); + canvas.lineWidth = 4; + canvas.strokeStyle = color; + canvas.stroke(); + } + + let lastResult: string | undefined; + let hasDetectedResult = false; + + const tick = () => { + if ( + loadingMessage === null || + canvasElement === null || + outputContainer === null || + canvas === null || + outputMessage === null || + outputData === null || + outputData.parentElement === null + ) { + console.warn( + 'ScanQRWidget: !loadingMessage || !canvasElement || !outputContainer || !canvas || !outputMessage || !outputData || !outputData.parentElement, it is null', + { + loadingMessage, + canvasElement, + outputContainer, + canvas, + outputMessage, + outputData, + 'outputData.parentElement': outputData?.parentElement, + }, + ); + + return; + } + loadingMessage.textContent = '⌛ Loading video...'; + if (video.readyState === video.HAVE_ENOUGH_DATA) { + loadingMessage.hidden = true; + canvasElement.hidden = false; + outputContainer.hidden = false; + + canvasElement.height = video.videoHeight; + canvasElement.width = video.videoWidth; + canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height); + const imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height); + const code = jsQR(imageData.data, imageData.width, imageData.height, { + inversionAttempts: 'dontInvert', + }); + outputMessage.hidden = true; + outputData.parentElement.hidden = false; + let result; + if (code === null) { + result = 'No code detected'; + } else { + drawLine(code.location.topLeftCorner, code.location.topRightCorner, '#FF3B58'); + drawLine(code.location.topRightCorner, code.location.bottomRightCorner, '#FF3B58'); + drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, '#FF3B58'); + drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, '#FF3B58'); + result = code.data; + hasDetectedResult = true; + } + + if (result !== lastResult) { + outputData.textContent = outputData.textContent ?? ''; + outputData.textContent += `${result}\n`; + lastResult = result; + // fast check of ip address + if (options.outputTiddler) { + const textFieldTiddler = $tw.wiki.getTiddler(options.outputTiddler); + const newServerInfoTiddler = { + ...textFieldTiddler?.fields, + title: options.outputTiddler, + [options.outputField]: result, + }; + // create if not exists + $tw.wiki.addTiddler(newServerInfoTiddler); + } + } + } + const stopDueToHasResult = options.stopOnDetect && hasDetectedResult; + /** if new loop happened, this.loopId will > loopId, stop current loop */ + const canContinueCurrentLoop = this.loopId === loopId && containerElement.offsetParent !== null; + if (!canContinueCurrentLoop || stopDueToHasResult) { + video.pause(); + video.parentElement?.removeChild(video); + stream.getTracks().forEach(function (track) { + track.stop(); + }); + } + if (stopDueToHasResult && options?.stateTiddler) { + $tw.wiki.addTiddler({ title: options.stateTiddler, text: 'no' }); + } + if (canContinueCurrentLoop && !stopDueToHasResult) { + requestAnimationFrame(tick); + } + }; + + // initialize the first tick + video.srcObject = stream; + video.setAttribute('playsinline', 'true'); // required to tell iOS safari we don't want fullscreen + await video.play(); + requestAnimationFrame(tick); + } +} + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +exports.widget = ScanQRWidget; +exports.ScanQRWidget = ScanQRWidget; diff --git a/src/tw-html-nodejs-sync/scan-qr-widget.ts.meta b/src/tw-html-nodejs-sync/scan-qr-widget.ts.meta new file mode 100644 index 0000000..0ab36c6 --- /dev/null +++ b/src/tw-html-nodejs-sync/scan-qr-widget.ts.meta @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/scan-qr-widget.js +type: application/javascript +module-type: widget diff --git a/src/tw-html-nodejs-sync/server/SaveTemplate/about-skinny-tiddlywiki5-html.tid b/src/tw-html-nodejs-sync/server/SaveTemplate/about-skinny-tiddlywiki5-html.tid new file mode 100644 index 0000000..f19640f --- /dev/null +++ b/src/tw-html-nodejs-sync/server/SaveTemplate/about-skinny-tiddlywiki5-html.tid @@ -0,0 +1,38 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/templates/about-skinny-tiddlywiki5-html + +!! Difference + +!!! $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/lazy-all + +* Add some plugin to saveTiddlerFilter ignore list at the end, and remove useless `+[sort[title]]` +* Replace `{{$:/core/templates/tiddlywiki5.html}}` with our `{{$:/plugins/linonetwo/tw-html-nodejs-sync/templates/skinny-tiddlywiki5.html}}` + +!!! $:/plugins/linonetwo/tw-html-nodejs-sync/templates/skinny-tiddlywiki5.html + +`` part is removed, because it is not used by users on mobile phone: + +``` + + + +`{{$:/core/templates/static.area}}` + + +``` + +The `class="tiddlywiki-tiddler-store"` part (which is included by `$:/core/templates/store.area.template.html`) is removed, because we will use multiple files to store tiddlers. And recreate this part manually, and add to the html string: + +``` + +`{{$:/core/templates/store.area.template.html}}` +``` + +!!! $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/save-lazy-all-tiddler-store + +Only keep the + +> New-style JSON store area, with an old-style store area for compatibility with v5.1.x tooling + +Assume there is no tiddler within `Encrypted` area. + +So we can store the tiddler store as a JSON. \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all-non-skinny-tiddler-store.tid b/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all-non-skinny-tiddler-store.tid new file mode 100644 index 0000000..f458572 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all-non-skinny-tiddler-store.tid @@ -0,0 +1,9 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/save-lazy-all-non-skinny-tiddler-store + +\import $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/lazy-all + +`[` + +<$list filter=<> counter="counter" template="$:/core/templates/html-json-tiddler"/> + +`]` \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all-skinny-tiddler-store.tid b/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all-skinny-tiddler-store.tid new file mode 100644 index 0000000..c126876 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all-skinny-tiddler-store.tid @@ -0,0 +1,9 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/save-lazy-all-skinny-tiddler-store + +\import $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/lazy-all + +`[` + +<$list filter={{{ [] }}} counter="counter" template="$:/core/templates/html-json-skinny-tiddler"/> + +`]` \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all.tid b/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all.tid new file mode 100644 index 0000000..2c8d1f5 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/SaveTemplate/save-lazy-all.tid @@ -0,0 +1,9 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/templates/save/lazy-all + +\define saveTiddlerFilter() +[is[system]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] [is[tiddler]type[application/javascript]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/twcloud/tiddlyweb-sse]] -[[$:/plugins/linonetwo/tidgi-ipc-syncadaptor]] -[[$:/plugins/linonetwo/tidgi-ipc-syncadaptor-ui]] +\end +\define skinnySaveTiddlerFilter() +[!is[system]] -[type[application/javascript]] +\end +{{$:/plugins/linonetwo/tw-html-nodejs-sync/templates/skinny-tiddlywiki5.html}} diff --git a/src/tw-html-nodejs-sync/server/SaveTemplate/skinny-tiddlywiki5.html.tid b/src/tw-html-nodejs-sync/server/SaveTemplate/skinny-tiddlywiki5.html.tid new file mode 100644 index 0000000..4da5854 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/SaveTemplate/skinny-tiddlywiki5.html.tid @@ -0,0 +1,51 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/templates/skinny-tiddlywiki5.html + +<$set name="saveTiddlerAndShadowsFilter" filter="[subfilter] [subfilterplugintiddlers[]]"> +` +`{{$:/core/templates/MOTW.html}}` + + + +`{{{ [enlisttag[$:/tags/RawMarkupWikified/TopHead]] ||$:/core/templates/raw-static-tiddler}}}` + + + + + + + + + + + +`{{$:/core/wiki/title}}` + + + +`{{{ [enlisttag[$:/core/wiki/rawmarkup]] ||$:/core/templates/plain-text-tiddler}}} +{{{ [enlisttag[$:/tags/RawMarkup]] ||$:/core/templates/plain-text-tiddler}}} +{{{ [enlisttag[$:/tags/RawMarkupWikified]] ||$:/core/templates/raw-static-tiddler}}}` + + + +`{{{ [enlisttag[$:/tags/RawMarkupWikified/TopBody]] ||$:/core/templates/raw-static-tiddler}}}` + + +`{{$:/boot/boot.css||$:/core/templates/css-tiddler}}` + + + +`{{{ [is[system]type[application/javascript]library[yes]] ||$:/core/templates/javascript-tiddler}}}` + + + +`{{ $:/boot/bootprefix.js ||$:/core/templates/javascript-tiddler}}` + + + +`{{ $:/boot/boot.js ||$:/core/templates/javascript-tiddler}}` + + +`{{{ [enlisttag[$:/tags/RawMarkupWikified/BottomBody]] ||$:/core/templates/raw-static-tiddler}}}` + +` diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/client-info-endpoint.ts b/src/tw-html-nodejs-sync/server/Tiddloid/client-info-endpoint.ts new file mode 100644 index 0000000..057fb4f --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/client-info-endpoint.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { ServerEndpointHandler } from 'tiddlywiki'; +import type Http from 'http'; +import type { ClientInfoStore } from '../../data/clientInfoStoreClass'; + +exports.method = 'GET'; + +exports.path = /^\/tw-html-nodejs-sync\/client-info$/; + +/** a /status endpoint with CORS (the original one will say CORS error) */ +const handler: ServerEndpointHandler = function handler(request: Http.ClientRequest & Http.InformationEvent, response: Http.ServerResponse, context) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const clientInfoStore: ClientInfoStore = require('$:/plugins/linonetwo/tw-html-nodejs-sync/clientInfoStore.js').store; + // mostly copied from the official repo's core/modules/server/routes/get-status.js + const text = JSON.stringify(clientInfoStore.allClient); + response.setHeader('Access-Control-Allow-Origin', '*'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(text, 'utf8'); +}; +exports.handler = handler; diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/client-info-endpoint.ts.meta b/src/tw-html-nodejs-sync/server/Tiddloid/client-info-endpoint.ts.meta new file mode 100644 index 0000000..8bc24f4 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/client-info-endpoint.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/client-info-endpoint.js +type: application/javascript +module-type: route diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint-options.ts b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint-options.ts new file mode 100644 index 0000000..7aff0cb --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint-options.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { ServerEndpointHandler } from 'tiddlywiki'; +import type Http from 'http'; + +/** this route is adding CORS to the POST in same route */ +exports.method = 'OPTIONS'; + +// route should start with something https://github.com/Jermolene/TiddlyWiki5/issues/4807 +// route is also in src/sync/getEndPoint.ts +exports.path = /^\/tw-html-nodejs-sync\/get-full-html$/; + +// TODO: use this custom endpoint to handle conflict on server side +const handler: ServerEndpointHandler = function handler(request: Http.ClientRequest, response: Http.ServerResponse, context) { + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + response.setHeader('Access-Control-Allow-Headers', '*'); + response.writeHead(200); + response.end(); +}; + +exports.handler = handler; diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint-options.ts.meta b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint-options.ts.meta new file mode 100644 index 0000000..56202e4 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint-options.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/server-get-html-endpoint-options.js +type: application/javascript +module-type: route diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint.ts b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint.ts new file mode 100644 index 0000000..42bef2f --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { OutputMimeTypes, ServerEndpointHandler } from 'tiddlywiki'; +import type Http from 'http'; + +exports.method = 'GET'; + +// route should start with something https://github.com/Jermolene/TiddlyWiki5/issues/4807 +// route is also in src/sync/getEndPoint.ts +exports.path = /^\/tw-html-nodejs-sync\/get-full-html$/; + +// don't use $:/core/save/lazy-images, otherwise image won't show in HTML +// don't use $:/plugins/tiddlywiki/tiddlyweb/save/offline , otherwise `TypeError: undefined is not an object (evaluating '$tw.syncer.syncadaptor')` +const templateName = '$:/core/save/all'; + +const handler: ServerEndpointHandler = function handler(request: Http.ClientRequest, response: Http.ServerResponse, context) { + response.setHeader('Access-Control-Allow-Origin', '*'); + + const downloadType = (context.server.get('root-render-type') as OutputMimeTypes | undefined) ?? 'text/plain'; + const exportedHTMLContent = context.wiki.renderTiddler(downloadType, templateName, { + variables: { + // exclude large file and unused tiddlers, like `core/ui/DownloadFullWiki.tid` + publishFilter: + '-[type[application/msword]] -[type[application/pdf]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/tiddlywiki/tiddlyweb]] -[[$:/plugins/twcloud/tiddlyweb-sse]]', + }, + }); + + try { + response.writeHead(200, { 'Content-Type': (context.server.get('root-serve-type') as OutputMimeTypes | undefined) ?? downloadType }); + response.end(exportedHTMLContent, 'utf8'); + } catch (error) { + response.writeHead(500); + response.end(`Failed to render tiddlers using ${templateName} , ${(error as Error).message} ${(error as Error).stack ?? ''}`, 'utf8'); + } +}; + +exports.handler = handler; diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint.ts.meta b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint.ts.meta new file mode 100644 index 0000000..6e4bc3c --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-get-html-endpoint.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/server-get-html-endpoint.js +type: application/javascript +module-type: route diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-status-endpoint.ts b/src/tw-html-nodejs-sync/server/Tiddloid/server-status-endpoint.ts new file mode 100644 index 0000000..0efa090 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-status-endpoint.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { ServerEndpointHandler } from 'tiddlywiki'; +import type Http from 'http'; +import type { ClientInfoStore } from '../data/clientInfoStoreClass'; +import { getClientInfo } from '../data/getClientInfo'; + +exports.method = 'GET'; + +// route should start with something https://github.com/Jermolene/TiddlyWiki5/issues/4807 +// route is also in src/sync/getEndPoint.ts +exports.path = /^\/tw-html-nodejs-sync\/status$/; + +/** a /status endpoint with CORS (the original one will say CORS error) */ +const handler: ServerEndpointHandler = function handler(request: Http.ClientRequest & Http.InformationEvent, response: Http.ServerResponse, context) { + const clientInfo = getClientInfo(request); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const clientInfoStore: ClientInfoStore = require('$:/plugins/linonetwo/tw-html-nodejs-sync/clientInfoStore.js').store; + clientInfoStore.updateClient(`${clientInfo.Origin ?? ''}${clientInfo['User-Agent'] ?? ''}`, clientInfo); + // mostly copied from the official repo's core/modules/server/routes/get-status.js + const text = JSON.stringify({ + username: context.authenticatedUsername ?? (context.server.get('anon-username') as string | undefined) ?? '', + anonymous: !context.authenticatedUsername, + read_only: !context.server.isAuthorized('writers', context.authenticatedUsername), + space: { + recipe: 'default', + }, + tiddlywiki_version: $tw.version, + }); + response.setHeader('Access-Control-Allow-Origin', '*'); + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(text, 'utf8'); +}; +exports.handler = handler; diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-status-endpoint.ts.meta b/src/tw-html-nodejs-sync/server/Tiddloid/server-status-endpoint.ts.meta new file mode 100644 index 0000000..aaeb0b1 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-status-endpoint.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/server-status-endpoint.js +type: application/javascript +module-type: route diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint-options.ts b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint-options.ts new file mode 100644 index 0000000..f7e880a --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint-options.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { ServerEndpointHandler } from 'tiddlywiki'; +import type Http from 'http'; + +/** this route is adding CORS to the POST in same route */ +exports.method = 'OPTIONS'; + +// route should start with something https://github.com/Jermolene/TiddlyWiki5/issues/4807 +// route is also in src/sync/getEndPoint.ts +exports.path = /^\/tw-html-nodejs-sync\/html-node-sync$/; + +// TODO: use this custom endpoint to handle conflict on server side +const handler: ServerEndpointHandler = function handler(request: Http.ClientRequest, response: Http.ServerResponse, context) { + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + response.setHeader('Access-Control-Allow-Headers', '*'); + response.writeHead(200); + response.end(); +}; + +exports.handler = handler; diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint-options.ts.meta b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint-options.ts.meta new file mode 100644 index 0000000..a0c5542 --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint-options.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/server-sync-endpoint-options.js +type: application/javascript +module-type: route diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint.ts b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint.ts new file mode 100644 index 0000000..adc779c --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type Http from 'http'; +import type { ServerEndpointHandler, Tiddler } from 'tiddlywiki'; +import { ClientInfoStore } from '../data/clientInfoStoreClass'; +import { filterOutNotSyncedTiddlers } from '../data/filterOutNotSyncedTiddlers'; +import { getServerChangeFilter } from '../data/filters'; +import { getClientInfo } from '../data/getClientInfo'; +import { getSyncedTiddlersText } from '../getSyncedTiddlersText'; +import { ConnectionState, ISyncEndPointRequest } from '../types'; + +exports.method = 'POST'; + +// route should start with something https://github.com/Jermolene/TiddlyWiki5/issues/4807 +// route is also in src/sync/getEndPoint.ts +exports.path = /^\/tw-html-nodejs-sync\/html-node-sync$/; + +// TODO: use this custom endpoint to handle conflict on server side +const handler: ServerEndpointHandler = function handler(request: Http.ClientRequest & Http.InformationEvent, response: Http.ServerResponse, context) { + response.setHeader('Access-Control-Allow-Origin', '*'); + + const { tiddlers, lastSync } = $tw.utils.parseJSONSafe(context.data) as ISyncEndPointRequest; + const changedTiddlersFromClient = filterOutNotSyncedTiddlers(tiddlers); + if (!Array.isArray(changedTiddlersFromClient)) { + response.writeHead(400, { 'Content-Type': 'application/json' }); + response.end(`Bad request body, not a tiddler list. ${String(changedTiddlersFromClient)}`, 'utf8'); + } + // get changed tiddlers + const diffTiddlersFilter: string = getServerChangeFilter(lastSync); + const diffTiddlers: string[] = $tw.wiki.compileFilter(diffTiddlersFilter)() ?? []; + const changedTiddlersFromServer = filterOutNotSyncedTiddlers( + diffTiddlers + .map((title) => { + return $tw.wiki.getTiddler(title); + }) + .filter((index): index is Tiddler => index !== undefined) + .map((tiddler) => tiddler.fields), + ); + + try { + context.wiki.addTiddlers(changedTiddlersFromClient); + response.writeHead(201, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(changedTiddlersFromServer), 'utf8'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const clientInfoStore: ClientInfoStore = require('$:/plugins/linonetwo/tw-html-nodejs-sync/clientInfoStore.js').store; + const clientInfo = getClientInfo(request, ConnectionState.onlineActive); + if (clientInfo.Origin !== undefined) { + clientInfoStore.updateClient(`${clientInfo.Origin ?? ''}${clientInfo['User-Agent'] ?? ''}`, { + ...clientInfo, + recentlySyncedString: getSyncedTiddlersText(changedTiddlersFromClient, changedTiddlersFromServer, { client: [], server: [] }, { reverse: true }), + }); + } + } catch (error) { + response.writeHead(500); + response.end(`Failed to add tiddlers ${(error as Error).message} ${(error as Error).stack ?? ''}`, 'utf8'); + } +}; + +exports.handler = handler; diff --git a/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint.ts.meta b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint.ts.meta new file mode 100644 index 0000000..dcc1a4a --- /dev/null +++ b/src/tw-html-nodejs-sync/server/Tiddloid/server-sync-endpoint.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/server-sync-endpoint.js +type: application/javascript +module-type: route diff --git a/src/tw-html-nodejs-sync/smart-recognize-ip-address.ts b/src/tw-html-nodejs-sync/smart-recognize-ip-address.ts new file mode 100644 index 0000000..67e4e7c --- /dev/null +++ b/src/tw-html-nodejs-sync/smart-recognize-ip-address.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import trim from 'lodash/trim'; + +// copy from core's core/modules/startup/rootwidget.js +exports.platforms = ['browser']; +// https://tiddlywiki.com/dev/#StartupMechanism +exports.after = ['rootwidget']; + +function recognize(sourceTiddlerName: string | undefined, tiddlerToFill: string | undefined, fieldName = 'text') { + if (sourceTiddlerName === undefined || tiddlerToFill === undefined) { + return; + } + const textFieldTiddler = $tw.wiki.getTiddler(sourceTiddlerName); + if (textFieldTiddler === undefined) { + return; + } + const text = textFieldTiddler.fields[fieldName]; + if (typeof text !== 'string' || trim(text).length === 0) { + return; + } + // example input is like `http://192.168.10.103:5214/#%E6%89%93%E5%BC%80CDDA%E5%9C%A8Mac%E4%B8%8A%E7%9A%84%E6%95%B0%E6%8D%AE%E6%96%87%E4%BB%B6%E5%A4%B9` + const regex = /(((\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])[.。]){3}(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]))[::](\d{2,5})/gm; + let match: RegExpExecArray | null; + let ipAddress: string | undefined; + let port: string | undefined; + while ((match = regex.exec(text)) !== null) { + if (match.index === regex.lastIndex) { + regex.lastIndex++; + } + match.forEach((match, groupIndex) => { + if (groupIndex === 1) { + ipAddress = match; + } + if (groupIndex === 5) { + port = match; + } + }); + } + if (ipAddress !== undefined || port !== undefined) { + const oldServerInfoTiddler = $tw.wiki.getTiddler(tiddlerToFill); + const newServerInfoTiddler = { + ...oldServerInfoTiddler?.fields, + title: tiddlerToFill, + ipAddress, + port, + }; + $tw.wiki.addTiddler(newServerInfoTiddler); + } +} + +exports.startup = () => { + $tw.rootWidget.addEventListener('tw-html-nodejs-sync-smart-recognize-ip-address', (event) => + recognize(event.paramObject?.from as string, (event.paramObject?.to as string) ?? event.paramObject?.from, event.paramObject?.field as string), + ); +}; diff --git a/src/tw-html-nodejs-sync/smart-recognize-ip-address.ts.meta b/src/tw-html-nodejs-sync/smart-recognize-ip-address.ts.meta new file mode 100644 index 0000000..ccd503e --- /dev/null +++ b/src/tw-html-nodejs-sync/smart-recognize-ip-address.ts.meta @@ -0,0 +1,4 @@ +creator: LinOnetwo +title: $:/plugins/linonetwo/tw-html-nodejs-sync/smart-recognize-ip-address.js +type: application/javascript +module-type: startup diff --git a/src/tw-html-nodejs-sync/types.ts b/src/tw-html-nodejs-sync/types.ts new file mode 100644 index 0000000..e413805 --- /dev/null +++ b/src/tw-html-nodejs-sync/types.ts @@ -0,0 +1,35 @@ +import { ITiddlerFieldsParam } from 'tiddlywiki'; + +export interface ISyncEndPointRequest { + deleted?: string[]; + lastSync: string | undefined; + tiddlers: Array>; +} +export interface ISyncEndPointResponse { + deletes: string[]; + updates: ITiddlerFieldsParam[]; +} + +export interface IClientInfo { + Origin: string; + 'User-Agent': string; + model?: string; + name: string; + os?: string; + /** + * Contains things recently synced + */ + recentlySyncedString?: string; + state?: ConnectionState; + timestamp: number; +} + +export enum ConnectionState { + offline = 'offline', + /** once selected by the user, but now offlined */ + offlineActive = 'offlineActive', + /** online and not selected by the user */ + online = 'online', + /** online and selected by the user */ + onlineActive = 'onlineActive', +} diff --git a/src/tw-html-nodejs-sync/ui/AddNewToServerList.tid b/src/tw-html-nodejs-sync/ui/AddNewToServerList.tid new file mode 100644 index 0000000..627899a --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/AddNewToServerList.tid @@ -0,0 +1,75 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/AddNewToServerList +type: text/vnd.tiddlywiki + + + {{$:/plugins/linonetwo/tw-html-nodejs-sync/ui/DownloadFullHtml}} + + + + + + 智能识别黏贴框 Smart Identify Sticky Box + + + <$edit-text tiddler="$:/state/tw-html-nodejs-sync/server/new" field="text" default=""/> + + + <$button> + 智能识别 Smart Identify + <$action-sendmessage $message="tw-html-nodejs-sync-smart-recognize-ip-address" from="$:/state/tw-html-nodejs-sync/server/new"/> + $button> + + <$reveal type="nomatch" state="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" text="yes"> + <$button> + 扫二维码 Scan QR + <$action-setfield $tiddler="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" text="yes" /> + $button> + $reveal> + <$reveal type="match" state="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" text="yes"> + <$button> + 停止扫码 Stop Scan + <$action-setfield $tiddler="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" text="no" /> + $button> + $reveal> + + + + <$reveal type="match" state="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" text="yes"> + <$ScanQRWidget outputTiddler="$:/state/tw-html-nodejs-sync/server/new" stopOnDetect="yes" stateTiddler="$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open" /> + $reveal> + + + + 服务器名 Server Name + <$edit-text tiddler="$:/state/tw-html-nodejs-sync/server/new" field="name" default="" /> + + + 服务器IP Server IP + <$edit-text tiddler="$:/state/tw-html-nodejs-sync/server/new" field="ipAddress" default="" /> + + + 服务器端口 Server Port + <$edit-text tiddler="$:/state/tw-html-nodejs-sync/server/new" field="port" default="" /> + + + <$button style="width: 160px; margin-top: 10px;"> + 新增服务器 + Add New + <$set name="existedServers" filter={{$:/plugins/linonetwo/tw-html-nodejs-sync/ServerListFilter}} > + <$set name="latestLastSync" filter="[sort[lastSync]limit[1]get[lastSync]]" emptyValue=<> > + <$action-createtiddler + $basetitle={{{ [] +[addprefix[$:/state/tw-html-nodejs-sync/server/]] }}} + text="offline" + name={{$:/state/tw-html-nodejs-sync/server/new!!name}} + caption={{$:/state/tw-html-nodejs-sync/server/new!!name}} + ipAddress={{$:/state/tw-html-nodejs-sync/server/new!!ipAddress}} + port={{$:/state/tw-html-nodejs-sync/server/new!!port}} + lastSync=<> + > + <$action-setfield $tiddler="$:/state/tw-html-nodejs-sync/server/new" text="" name="" ipAddress="" port="" /> + <$action-sendmessage $message="tw-html-nodejs-sync-set-active-server-and-sync" title=<> /> + $action-createtiddler> + $set> + $set> + $button> + diff --git a/src/tw-html-nodejs-sync/ui/ControlPanel/Settings.tid b/src/tw-html-nodejs-sync/ui/ControlPanel/Settings.tid new file mode 100644 index 0000000..ce34fc9 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ControlPanel/Settings.tid @@ -0,0 +1,27 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ControlPanel/Settings +caption: MobileSync +tags: $:/tags/ControlPanel/SettingsTab + +These settings let you customise the behaviour of TW-MobileSync plugin. + +--- + +!! Sync + + +!!! Sync Interval + +Normally, sync happened in local wiki, so it is free, so you can sync it frequently. This setting takes effect after refresh. + +Minutes between sync: + +<$edit-text + tiddler="$:/plugins/linonetwo/tw-html-nodejs-sync/Config/SyncInterval" + field="text" + default="3" + tabindex=-1 + focus=false + cancelPopups="yes" + fileDrop=no + tag="input" +/> diff --git a/src/tw-html-nodejs-sync/ui/DownloadFullHtml.tid b/src/tw-html-nodejs-sync/ui/DownloadFullHtml.tid new file mode 100644 index 0000000..32aa16e --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/DownloadFullHtml.tid @@ -0,0 +1,22 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/DownloadFullHtml +type: text/vnd.tiddlywiki + +<$list filter="[prefix[$:/state/tw-html-nodejs-sync/server/]field:text[onlineActive]]"> + + + 拉取并覆盖 Pull and override + + + 拉取服务端最新完整内容覆盖本地所有内容 Pull the latest complete content from the server to cover all local content + 普通同步没法更新插件和导入的内容,如果你在电脑端更新或安装了插件,则需要在移动端点下面的按钮做一次完整拉取。 + If you have updated or installed the plugin on your computer, you will need to do a full pull on the mobile side by clicking the button below. + 请慎重点击,本地若有未同步内容将丢失!如果确保本地没有未同步内容,则可以放心拉取。 + Please click carefully, local unsynced content will be lost if there is any! If you make sure there is no unsynced content locally, you can pull it without worry. + <$button> + {{!!name}} + 拉取内容覆盖本地 Pull content to cover local + <$action-sendmessage $message="tw-html-nodejs-sync-download-full-html" /> + $button> + + +$list> diff --git a/src/tw-html-nodejs-sync/ui/ServerItemCopyTemplate.tid b/src/tw-html-nodejs-sync/ui/ServerItemCopyTemplate.tid new file mode 100644 index 0000000..968831e --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ServerItemCopyTemplate.tid @@ -0,0 +1,10 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemCopyTemplate +type: text/vnd.tiddlywiki + +<$button class="tw-html-nodejs-sync-copy-button"> + 复制 + Copy + <$action-createtiddler name={{{ [{!!name}addsuffix[copy]] }}} $template={{!!title}}> + <$action-navigate $to=<>/> + $action-createtiddler> +$button> diff --git a/src/tw-html-nodejs-sync/ui/ServerItemViewTemplate.tid b/src/tw-html-nodejs-sync/ui/ServerItemViewTemplate.tid new file mode 100644 index 0000000..718a26b --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ServerItemViewTemplate.tid @@ -0,0 +1,41 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplate +type: text/vnd.tiddlywiki + +{{||$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic}} + + + 更新日期 Last Sync + + 只会同步修改日期在这个日期之后的条目。 Only tiddlers' modified is after this time are synced. + + <$edit-date field="lastSync" showTime="yes" showSeconds="yes" use24hour="yes" /> + + + + + <$edit-text tiddler="$:/state/tw-html-nodejs-sync/server/existed/update" default="" tag="input" /> + + + <$button> + 智能识别 Smart Identify + <$action-sendmessage $message="tw-html-nodejs-sync-smart-recognize-ip-address" from="$:/state/tw-html-nodejs-sync/server/existed/update" to={{!!title}} /> + $button> + + <$reveal type="nomatch" state="$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open" text="yes"> + <$button> + 扫二维码 Scan QR + <$action-setfield $tiddler="$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open" text="yes" /> + $button> + $reveal> + <$reveal type="match" state="$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open" text="yes"> + <$button> + 停止扫码 Stop Scan + <$action-setfield $tiddler="$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open" text="no" /> + $button> + $reveal> + + + +<$reveal type="match" state="$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open" text="yes"> + <$ScanQRWidget outputTiddler="$:/state/tw-html-nodejs-sync/server/existed/update" stopOnDetect="yes" stateTiddler="$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open" /> +$reveal> \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic.tid b/src/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic.tid new file mode 100644 index 0000000..97be336 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic.tid @@ -0,0 +1,35 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic +type: text/vnd.tiddlywiki + + + + + <$link to={{!!title}}> + <$text text={{!!name}}/> + $link> + + {{!!ipAddress}}:{{!!port}} + + {{!!text}} + <$view field=lastSync format=date template="YYYY-0MM-0DD 0hh:0mm:0ss" /> + + + <$reveal type="nomatch" state=<> text="onlineActive" class="tw-html-nodejs-sync-sync-reveal"> + <$button class="tw-html-nodejs-sync-sync-button"> + <$text text={{!!name}}/> + 启用同步并立即同步 + Enable and Sync Now + <$action-sendmessage $message="tw-html-nodejs-sync-set-active-server-and-sync" title={{!!title}} /> + $button> + $reveal> + + <$reveal type="match" state=<> text="onlineActive" class="tw-html-nodejs-sync-sync-reveal"> + <$button class="tw-html-nodejs-sync-sync-button"> + <$text text={{!!name}}/> + 立即同步 + Sync Now + <$action-sendmessage $message="tw-html-nodejs-sync-sync-start" /> + $button> + $reveal> + + diff --git a/src/tw-html-nodejs-sync/ui/ServerItemViewTemplateCascade.tid b/src/tw-html-nodejs-sync/ui/ServerItemViewTemplateCascade.tid new file mode 100644 index 0000000..603eb02 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ServerItemViewTemplateCascade.tid @@ -0,0 +1,5 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplateCascade +tags: $:/tags/ViewTemplateBodyFilter +list-before: $:/config/ViewTemplateBodyFilters/system + +[has:field[ipAddress]has:field[port]then[$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplate]] \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/ServerList.tid b/src/tw-html-nodejs-sync/ui/ServerList.tid new file mode 100644 index 0000000..4dc9830 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ServerList.tid @@ -0,0 +1,13 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList +type: text/vnd.tiddlywiki + + + <$list filter={{$:/plugins/linonetwo/tw-html-nodejs-sync/ServerListFilter}}> + + {{||$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic}} + {{||$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemCopyTemplate}} + + $list> + + +{{$:/plugins/linonetwo/tw-html-nodejs-sync/ui/AddNewToServerList}} diff --git a/src/tw-html-nodejs-sync/ui/ServerListFilter.tid b/src/tw-html-nodejs-sync/ui/ServerListFilter.tid new file mode 100644 index 0000000..cde33ad --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/ServerListFilter.tid @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ServerListFilter + +[prefix[$:/state/tw-html-nodejs-sync/server/]] -[[$:/state/tw-html-nodejs-sync/server/new]] -[[$:/state/tw-html-nodejs-sync/server/new/scan-qr-widget-open]] -[[$:/state/tw-html-nodejs-sync/server/existed/scan-qr-widget-open]] -[[$:/state/tw-html-nodejs-sync/server/existed/update]] \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/DesktopFullHtmlQR.tid b/src/tw-html-nodejs-sync/ui/Sidebar/DesktopFullHtmlQR.tid new file mode 100644 index 0000000..371ce2e --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/DesktopFullHtmlQR.tid @@ -0,0 +1,10 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/DesktopFullHtmlQR +caption: Full (Tiddloid) +type: text/vnd.tiddlywiki + + + <$let content={{{ [{$:/info/url/full}addsuffix[tw-html-nodejs-sync/get-full-html]] }}} size={{{ [{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}getSidebarWidthInPx[]] }}} > + <> + <> + $let> + \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid b/src/tw-html-nodejs-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid new file mode 100644 index 0000000..85f9efb --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid @@ -0,0 +1,10 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/DesktopSkinnyHtmlQR +caption: Skinny (TidGi-Mobile) +type: text/vnd.tiddlywiki + + + <$let content={{{ [{$:/info/url/full}addsuffix[tw-html-nodejs-sync/get-skinny-html]] }}} size={{{ [{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}getSidebarWidthInPx[]] }}} > + <> + <> + $let> + \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/SidebarDesktopContent.tid b/src/tw-html-nodejs-sync/ui/Sidebar/SidebarDesktopContent.tid new file mode 100644 index 0000000..88d5a83 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/SidebarDesktopContent.tid @@ -0,0 +1,44 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/SidebarDesktopContent +type: text/vnd.tiddlywiki + +\define image() +>/> +\end + +<$reveal type="match" state="$:/info/tidgi/enableHTTPAPI" text="no"> + Firstly, Open TidGi-desktop's HTTP API feature on current workspace's setting, see [[Official TidGi Feature Handbook|https://tidgi.fun/#TidGi%20Feature%20Handbook%2FEnabling%20HTTP%20API]] for details. + + 请先开启太记桌面版,当前 Wiki 工作区设置里的 HTTP API,详见[[官方太记功能手册|https://tidgi.fun/#%E5%A4%AA%E8%AE%B0%E5%8A%9F%E8%83%BD%E6%89%8B%E5%86%8C%2F%E5%BC%80%E5%90%AF%20HTTP%20API]]。 +$reveal> + +<$reveal type="nomatch" state="$:/info/tidgi/enableHTTPAPI" text="no"> + +QRCode of Current Server + +<% if [{$:/info/tidgi/enableHTTPAPI}match[yes]] %> + + {{$:/plugins/linonetwo/tw-mobile-sync/ui/Sidebar/DesktopSkinnyHtmlQR}} + +<% else %> + + 需要开启 HTTP API,详见[[官方太记功能手册|https://tidgi.fun/#%E5%A4%AA%E8%AE%B0%E5%8A%9F%E8%83%BD%E6%89%8B%E5%86%8C%2F%E5%BC%80%E5%90%AF%20HTTP%20API]]。 + +<% endif %> + + + + 已连接客户端 + + <$vars compare-put-host-to-end="[get[Origin]prefix[http]else[1]]" compare-put-active-to-start="[get[state]prefix[onlineActive]else[1]]"> + <$list filter="[prefix[$:/state/tw-mobile-sync/clientStatus/]sortsub:numbersortsub:number]"> + + <$text text={{!!name}} /> + {{!!state}} + {{!!os}} + {{!!recentlySyncedString}} + + $list> + $vars> + + +$reveal> \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/SidebarMobileContent.tid b/src/tw-html-nodejs-sync/ui/Sidebar/SidebarMobileContent.tid new file mode 100644 index 0000000..cb33fbb --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/SidebarMobileContent.tid @@ -0,0 +1,29 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/SidebarMobileContent +type: text/vnd.tiddlywiki + + + 与桌面端同步 Sync With Desktop + + <$reveal type="match" state="$:/info/tidgi-mobile" text="yes"> + + Use TidGi-Mobile's wiki list to sync (long press wiki item in the list to open menu) + + 使用 TidGi-Mobile 的 wiki 列表进行同步(长按列表里的 wiki 条目打开菜单) + + $reveal> + + <$reveal type="nomatch" state="$:/info/tidgi-mobile" text="yes"> + + + {{{[prefix[$:/state/tw-html-nodejs-sync/server/]field:text[onlineActive]] ~[prefix[$:/state/tw-html-nodejs-sync/server/]field:text[online]first[]]||$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerItemViewTemplateBasic}}} + + + [[服务器列表|$:/plugins/linonetwo/tw-html-nodejs-sync/ui/ServerList]] + + $reveal> + + + +<$reveal type="nomatch" state="$:/info/tidgi-mobile" text="yes"> +{{$:/plugins/linonetwo/tw-html-nodejs-sync/ui/AddNewToServerList}} +$reveal> diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/SidebarTab.tid b/src/tw-html-nodejs-sync/ui/Sidebar/SidebarTab.tid new file mode 100644 index 0000000..82a68ee --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/SidebarTab.tid @@ -0,0 +1,14 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/SidebarTab +tags: $:/tags/SideBar +caption: MobileSync +type: text/vnd.tiddlywiki + + + + + + {{$:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/SidebarMobileContent}} + + + {{$:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/SidebarDesktopContent}} + \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/getSidebarWidthInPx.js.meta b/src/tw-html-nodejs-sync/ui/Sidebar/getSidebarWidthInPx.js.meta new file mode 100644 index 0000000..21df2e5 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/getSidebarWidthInPx.js.meta @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/filters/getSidebarWidthInPx.js +type: application/javascript +module-type: filteroperator \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/getSidebarWidthInPx.ts b/src/tw-html-nodejs-sync/ui/Sidebar/getSidebarWidthInPx.ts new file mode 100644 index 0000000..76e9380 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/getSidebarWidthInPx.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +exports.getSidebarWidthInPx = function (source: (argument0: (_tiddler: any, input: string) => void) => void, _operator: any, _options: any) { + const results: string[] = []; + source(function (_tiddler: any, input: string) { + if (input.endsWith('px')) { + results.push(input.replace('px', '')); + } else if (input.endsWith('vw')) { + const vwPercentage = Number(input.replace('vw', '')) / 100; + results.push(String(Math.floor(window.innerWidth * vwPercentage))); + } else { + results.push(input); + } + }); + return results; +}; diff --git a/src/tw-html-nodejs-sync/ui/Sidebar/style.tid b/src/tw-html-nodejs-sync/ui/Sidebar/style.tid new file mode 100644 index 0000000..fb31157 --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/Sidebar/style.tid @@ -0,0 +1,27 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/Sidebar/style.css +tags: $:/tags/Stylesheet +type: text/vnd.tiddlywiki + +<$importvariables filter="[[$:/themes/tiddlywiki/vanilla/base]]"> + +.tw-html-nodejs-sync-sidebar-mobile-content, .tw-html-nodejs-sync-sidebar-desktop-content { + width: 100%; +display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; +} + +@media (min-width: <>) { + .tw-html-nodejs-sync-sidebar-mobile-content { + display: none; + } +} + +@media (max-width: <>) { + .tw-html-nodejs-sync-sidebar-desktop-content { + display: none; + } +} + +$importvariables> \ No newline at end of file diff --git a/src/tw-html-nodejs-sync/ui/style.tid b/src/tw-html-nodejs-sync/ui/style.tid new file mode 100644 index 0000000..c31d47a --- /dev/null +++ b/src/tw-html-nodejs-sync/ui/style.tid @@ -0,0 +1,112 @@ +title: $:/plugins/linonetwo/tw-html-nodejs-sync/ui/style.css +tags: $:/tags/Stylesheet +type: text/vnd.tiddlywiki + +<$importvariables filter="[[$:/themes/tiddlywiki/vanilla/base]]"> + +.tw-html-nodejs-sync-server-list { + padding-left: 0; + overflow: auto; + overflow: overlay; +} + +.tw-html-nodejs-sync-sync-button { + width: 100%; height: 100px; display: flex; flex-direction: column; justify-content: center; align-items: center; + max-width: 300px; + min-width: 175px; +} +.tw-html-nodejs-sync-copy-button { + height: 100%; + display: flex; flex-direction: column; justify-content: center; align-items: center; + margin-left: 10px !important; +} + +.tw-html-nodejs-sync-sync-reveal { + margin-left: 20px; +} + +.tw-html-nodejs-sync-new-server-field-fields-container-outer2 textarea, +.tw-html-nodejs-sync-new-server-field-fields-container-outer2 input { + width: 100%; +} +.tw-html-nodejs-sync-new-server-field-fields-container-outer2 { + display: flex; flex-direction: column; +} +.tw-html-nodejs-sync-new-server-field-fields-container { + display: flex; flex-direction: row; justify-content: center; align-items: center; +} +.tw-html-nodejs-sync-new-server-field-fields-container-outer1 { + display: flex; flex-direction: row; +} +@media (max-width: 535px) { + .tw-html-nodejs-sync-new-server-field-fields-container-outer1 { + flex-direction: column; + } +} +.tw-html-nodejs-sync-new-server-field-fields-buttons { + display: flex; flex-direction: column; justify-content: center; align-items: center; +} +@media (max-width: 535px) { + .tw-html-nodejs-sync-new-server-field-fields-container { + flex-direction: column; + } + .tw-html-nodejs-sync-new-server-field-fields-buttons { + width: 100%; + flex-direction: row; + justify-content: space-between; + margin-top: 10px; + } +} + +.tw-html-nodejs-sync-existed-server-field-fields-container { + display: flex; flex-direction: row; +} +.tw-html-nodejs-sync-existed-server-field-fields-buttons { + display: flex; flex-direction: row; justify-content: space-between; +} + + +.tw-html-nodejs-sync-sidebar-fieldset { + display: flex; flex-direction: column; justify-content: center; align-items: center; + width: 100%; + + margin-bottom: 10px; +} + +.tw-html-nodejs-sync-sidebar-desktop-server-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + margin-bottom: 20px; +} +.tw-html-nodejs-sync-sidebar-desktop-server-item h3, .tw-html-nodejs-sync-sidebar-desktop-server-item h5 { + margin: 0; +} +.tw-html-nodejs-sync-sidebar-desktop-server-item h5 { + font-weight: 700; +} +.tw-html-nodejs-sync-sidebar-desktop-server-item p:last-child { + margin-bottom: 15px; +} + +.tw-html-nodejs-sync-server-list-item { + display: flex; flex-direction: row; align-items: center; + justify-content: space-between; +} +@media (max-width: 535px) { + .tw-html-nodejs-sync-server-list-item { + flex-direction: column; + } +} + +.tw-html-nodejs-sync-sidebar-qrcode { + width: auto; + height: auto; +} +.tw-html-nodejs-sync-sidebar-qrcode img { + width: calc({{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}} - 2em); + height: calc({{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}} - 2em); +} + +$importvariables> \ No newline at end of file diff --git a/src/tw-mobile-sync/plugin.info b/src/tw-mobile-sync/plugin.info index a8d4313..880b9e3 100644 --- a/src/tw-mobile-sync/plugin.info +++ b/src/tw-mobile-sync/plugin.info @@ -1,7 +1,7 @@ { "title": "$:/plugins/linonetwo/tw-mobile-sync", "author": "LinOnetwo", - "description": "Sync data between Mobile HTML (Tiddloid/Quine2/TidGi-Mobile) <-> Desktop NodeJS App (TidGi) ", + "description": "Sync data between TidGi-Mobile <-> Desktop NodeJS App (TidGi) ", "core-version": ">=5.1.22", "plugin-type": "plugin", "version": "0.8.2", diff --git a/src/tw-mobile-sync/readme.tid b/src/tw-mobile-sync/readme.tid index a883cff..b08c4ee 100755 --- a/src/tw-mobile-sync/readme.tid +++ b/src/tw-mobile-sync/readme.tid @@ -6,15 +6,19 @@ type: text/vnd.tiddlywiki >/> \end -! Sync Between NodeJS and Mobile HTML 在桌面端(NodeJS)和移动端(HTML文件)之间同步 +! Sync Between NodeJS and TidGi-Mobile 在桌面端(NodeJS)和太记移动端之间同步 -本插件可以让你在基于NodeJS技术的桌面应用(例如太记)和基于HTML文件的手机端(例如Tiddloid安卓应用)之间同步数据。 +本插件可以让你在基于NodeJS技术的桌面应用(例如太记)和太记移动端 App 之间同步数据。 手机应用 ↔ 桌面应用 ↔ 云端 -This plugin enables you sync date between NodeJS server App (e.g. TidGi App) and HTML file based mobile App (e.g. Tiddloid Android App). +不支持 Tiddloid。请改用 `tw-html-nodejs-sync` 插件。 -Mobile App ↔ Desktop App ↔ Cloud +This plugin enables you sync date between NodeJS server App (e.g. TidGi App) and TidGi-Mobile App. + +TidGi-Mobile App ↔ Desktop App ↔ Cloud + +Tiddloid is not supported. Use `tw-html-nodejs-sync` plugin instead. !! How to use diff --git a/src/tw-mobile-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid b/src/tw-mobile-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid index 2b59f47..93ea243 100644 --- a/src/tw-mobile-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid +++ b/src/tw-mobile-sync/ui/Sidebar/DesktopSkinnyHtmlQR.tid @@ -4,7 +4,7 @@ type: text/vnd.tiddlywiki <$let content={{{ [{$:/info/url/full}addsuffix[tw-mobile-sync/get-skinny-html]] }}} size={{{ [{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}getSidebarWidthInPx[]] }}} > - <> <> + <> $let> \ No newline at end of file
{{!!recentlySyncedString}}