Compare commits

...
10 Commits
Author SHA1 Message Date
fyears 9aa1bd821e bump to 0.5.12
Release A New Version / build (16.x) (push) Failing after 45s
2024-06-29 18:34:11 +08:00
fyears a967d6ffd9 move onedrive full to below, and add separator 2024-06-29 18:33:41 +08:00
fyears a2ce95441a download at best effort 2024-06-29 00:43:01 +08:00
fyears e74e49538e more logic for potentially the same files in smart conflict 2024-06-29 00:14:53 +08:00
fyears 11c66d7030 fix smart conflict for .obsidian 2024-06-28 23:17:41 +08:00
fyears 56bd4c000e Merge branch 'master' of https://github.com/fyears/remotely-save 2024-06-28 23:06:33 +08:00
fyears a7cc4e2d7e more robust for rclone 2024-06-28 23:06:14 +08:00
fyears f306445e0d save resources 2024-06-28 23:05:22 +08:00
lyiton 9f4de49495 修改一处细节 (#720)
* Update zh_cn.json

* Update zh_cn.json

* Update zh_tw.json
2024-06-24 22:52:25 +08:00
fyears 54c25a70ed add pro tag to onedrive 2024-06-23 18:09:48 +08:00
13 changed files with 295 additions and 78 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.5.11", "version": "0.5.12",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.5.11", "version": "0.5.12",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.5.11", "version": "0.5.12",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev2": "node esbuild.config.mjs --watch", "dev2": "node esbuild.config.mjs --watch",
+150 -30
View File
@@ -200,22 +200,141 @@ export function getFileRename(key: string) {
return res; return res;
} }
function arraysAreEqual(arr1: ArrayBuffer, arr2: ArrayBuffer) {
if (arr1.byteLength !== arr2.byteLength) {
return false;
}
const u1 = new Uint8Array(arr1);
const u2 = new Uint8Array(arr2);
for (let i = 0; i < u1.byteLength; ++i) {
if (u1[i] !== u2[i]) {
return false;
}
}
return true;
}
/**
* 1. download remote
* 2. compare
* 3. if the same, update local but not upload
* 4. if not the same, rename local and save remote
*/
async function tryDuplicateFileForSameSizes(
key: string,
key2: string,
fsLocal: FakeFs,
fsRemote: FakeFs,
uploadCallback: (entity: Entity | undefined) => Promise<any>,
downloadCallback: (entity: Entity | undefined) => Promise<any>
) {
console.debug(`tryDuplicateFileForSameSizes: ${key}`);
// 1. download
const remoteContent = await fsRemote.readFile(key);
// 2. compare
const localContent = await fsLocal.readFile(key);
const eq = arraysAreEqual(localContent, remoteContent);
if (eq) {
// 3. if the same, update local but not upload
// read meta of remote, as if we have downloaded the file
console.debug(`tryDuplicateFileForSameSizes: ${key} content equal`);
const entityRemote = await fsRemote.stat(key);
// write
const downloadResultEntity = await fsLocal.writeFile(
key,
remoteContent,
entityRemote.mtimeCli ?? Date.now(),
entityRemote.mtimeCli ?? Date.now()
);
await downloadCallback(downloadResultEntity);
// no uploadCallback here
} else {
// 4. if not the same, rename local and save remote
console.debug(`tryDuplicateFileForSameSizes: ${key} content not equal`);
await fsLocal.rename(key, key2);
const entityRemote = await fsRemote.stat(key);
const downloadResultEntity = await fsLocal.writeFile(
key,
remoteContent,
entityRemote.mtimeCli ?? Date.now(),
entityRemote.mtimeCli ?? Date.now()
);
await downloadCallback(downloadResultEntity);
const entityLocal = await fsLocal.stat(key2); // key2 here!
const uploadResultEntity = await fsRemote.writeFile(
key2, // key2 here!
localContent,
entityLocal.mtimeCli ?? Date.now(),
entityLocal.mtimeCli ?? Date.now()
);
await uploadCallback(uploadResultEntity);
}
}
/** /**
* local: x.md -> x.dup.md -> upload to remote * local: x.md -> x.dup.md -> upload to remote
* remote: x.md -> download to local -> using original name x.md * remote: x.md -> download to local -> using original name x.md
*/ */
export async function duplicateFile( async function tryDuplicateFileForDiffSizes(
key: string, key: string,
left: FakeFs, key2: string,
right: FakeFs, fsLocal: FakeFs,
uploadCallback: (entity: Entity) => Promise<any>, fsRemote: FakeFs,
downloadCallback: (entity: Entity) => Promise<any> uploadCallback: (entity: Entity | undefined) => Promise<any>,
downloadCallback: (entity: Entity | undefined) => Promise<any>
) {
console.debug(`tryDuplicateFileForDiffSizes: ${key}`);
await fsLocal.rename(key, key2);
/**
* x.dup.md -> upload to remote
*/
async function f1() {
const k = await copyFile(key2, fsLocal, fsRemote);
await uploadCallback(k.entity);
return k.entity;
}
/**
* x.md -> download to local
*/
async function f2() {
const k = await copyFile(key, fsRemote, fsLocal);
await downloadCallback(k.entity);
return k.entity;
}
const [resUpload, resDownload] = await Promise.all([f1(), f2()]);
return {
upload: resUpload,
download: resDownload,
};
}
export async function tryDuplicateFile(
key: string,
fsLocal: FakeFs,
fsRemote: FakeFs,
uploadCallback: (entity: Entity | undefined) => Promise<any>,
downloadCallback: (entity: Entity | undefined) => Promise<any>
) { ) {
let key2 = getFileRename(key); let key2 = getFileRename(key);
let usable = false; let usable = false;
do { do {
try { try {
const s = await left.stat(key2); const s = await fsLocal.stat(key2);
if (s === null || s === undefined) { if (s === null || s === undefined) {
throw Error(`not exist $${key2}`); throw Error(`not exist $${key2}`);
} }
@@ -228,30 +347,31 @@ export async function duplicateFile(
usable = true; usable = true;
} }
} while (!usable); } while (!usable);
await left.rename(key, key2);
/** const localSize = await fsLocal.stat(key);
* x.dup.md -> upload to remote const remoteSize = await fsRemote.stat(key);
*/
async function f1() { if (
const k = await copyFile(key2, left, right); localSize !== undefined &&
await uploadCallback(k.entity); remoteSize !== undefined &&
return k.entity; localSize.sizeRaw === remoteSize.sizeRaw
) {
return await tryDuplicateFileForSameSizes(
key,
key2,
fsLocal,
fsRemote,
uploadCallback,
downloadCallback
);
} else {
return await tryDuplicateFileForDiffSizes(
key,
key2,
fsLocal,
fsRemote,
uploadCallback,
downloadCallback
);
} }
/**
* x.md -> download to local
*/
async function f2() {
const k = await copyFile(key, right, left);
await downloadCallback(k.entity);
return k.entity;
}
const [resUpload, resDownload] = await Promise.all([f1(), f2()]);
return {
upload: resUpload,
download: resDownload,
};
} }
+1 -1
View File
@@ -141,7 +141,7 @@
"modal_proauth_maualinput_conn_fail": "连接失败", "modal_proauth_maualinput_conn_fail": "连接失败",
"settings_onedrivefull": "Onedrive(个人版)(Full)设置", "settings_onedrivefull": "Onedrive(个人版)(Full)设置",
"settings_chooseservice_onedrivefull": "OneDrive(个人版)(Full", "settings_chooseservice_onedrivefull": "OneDrive(个人版)(FullPRO",
"settings_onedrivefull_disclaimer1": "声明:此插件不是微软或 OneDrive 的官方产品。", "settings_onedrivefull_disclaimer1": "声明:此插件不是微软或 OneDrive 的官方产品。",
"settings_onedrivefull_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 OneDrive 有不符合预期的访问,请立刻从 https://microsoft.com/consent 删除记录于此插件的连接鉴权。", "settings_onedrivefull_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 OneDrive 有不符合预期的访问,请立刻从 https://microsoft.com/consent 删除记录于此插件的连接鉴权。",
"settings_onedrivefull_folder": "我们会在您的 OneDrive 上创建此文件夹并在里面同步:/{{remoteBaseDir}}。", "settings_onedrivefull_folder": "我们会在您的 OneDrive 上创建此文件夹并在里面同步:/{{remoteBaseDir}}。",
+1 -1
View File
@@ -141,7 +141,7 @@
"modal_proauth_maualinput_conn_fail": "連線失敗", "modal_proauth_maualinput_conn_fail": "連線失敗",
"settings_onedrivefull": "Onedrive(個人版)(Full)設定", "settings_onedrivefull": "Onedrive(個人版)(Full)設定",
"settings_chooseservice_onedrivefull": "OneDrive(個人版)(Full", "settings_chooseservice_onedrivefull": "OneDrive(個人版)(FullPRO",
"settings_onedrivefull_disclaimer1": "宣告:此外掛不是微軟或 OneDrive 的官方產品。", "settings_onedrivefull_disclaimer1": "宣告:此外掛不是微軟或 OneDrive 的官方產品。",
"settings_onedrivefull_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 OneDrive 有不符合預期的訪問,請立刻從 https://microsoft.com/consent 刪除記錄於此外掛的連線鑑權。", "settings_onedrivefull_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 OneDrive 有不符合預期的訪問,請立刻從 https://microsoft.com/consent 刪除記錄於此外掛的連線鑑權。",
"settings_onedrivefull_folder": "我們會在您的 OneDrive 上建立此資料夾並在裡面同步:/{{remoteBaseDir}}。", "settings_onedrivefull_folder": "我們會在您的 OneDrive 上建立此資料夾並在裡面同步:/{{remoteBaseDir}}。",
+34 -20
View File
@@ -36,7 +36,7 @@ import {
} from "../../src/misc"; } from "../../src/misc";
import type { Profiler } from "../../src/profiler"; import type { Profiler } from "../../src/profiler";
import { checkProRunnableAndFixInplace } from "./account"; import { checkProRunnableAndFixInplace } from "./account";
import { duplicateFile, isMergable, mergeFile } from "./conflictLogic"; import { isMergable, mergeFile, tryDuplicateFile } from "./conflictLogic";
import { import {
clearFileContentHistoryByVaultAndProfile, clearFileContentHistoryByVaultAndProfile,
getFileContentHistoryByVaultAndProfile, getFileContentHistoryByVaultAndProfile,
@@ -296,7 +296,8 @@ const getSyncPlanInplace = async (
syncDirection: SyncDirectionType, syncDirection: SyncDirectionType,
profiler: Profiler | undefined, profiler: Profiler | undefined,
settings: RemotelySavePluginSettings, settings: RemotelySavePluginSettings,
triggerSource: SyncTriggerSourceType triggerSource: SyncTriggerSourceType,
configDir: string
) => { ) => {
profiler?.addIndent(); profiler?.addIndent();
profiler?.insert("getSyncPlanInplace: enter"); profiler?.insert("getSyncPlanInplace: enter");
@@ -582,7 +583,11 @@ const getSyncPlanInplace = async (
if (prevSync === undefined) { if (prevSync === undefined) {
// Didn't exist means both are new // Didn't exist means both are new
if (syncDirection === "bidirectional") { if (syncDirection === "bidirectional") {
if (conflictAction === "keep_newer") { if (
conflictAction === "keep_newer" ||
(conflictAction === "smart_conflict" &&
key.startsWith(`${configDir}/`))
) {
if ( if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >= (local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0) (remote.mtimeCli ?? remote.mtimeSvr ?? 0)
@@ -640,7 +645,11 @@ const getSyncPlanInplace = async (
} else { } else {
// Both exist but don't compare means both are modified // Both exist but don't compare means both are modified
if (syncDirection === "bidirectional") { if (syncDirection === "bidirectional") {
if (conflictAction === "keep_newer") { if (
conflictAction === "keep_newer" ||
(conflictAction === "smart_conflict" &&
key.startsWith(`${configDir}/`))
) {
if ( if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >= (local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0) (remote.mtimeCli ?? remote.mtimeSvr ?? 0)
@@ -1342,27 +1351,31 @@ const dispatchOperationToActualV3 = async (
r.key r.key
); );
const mtimeCli = (await fsLocal.stat(r.key)).mtimeCli!; const mtimeCli = (await fsLocal.stat(r.key)).mtimeCli!;
const { upload, download } = await duplicateFile( await tryDuplicateFile(
r.key, r.key,
fsLocal, fsLocal,
fsEncrypt, fsEncrypt,
async (upload) => { async (upload) => {
// TODO: abstract away the dirty hack if (upload !== undefined) {
fullfillMTimeOfRemoteEntityInplace(upload, mtimeCli); // TODO: abstract away the dirty hack
await upsertPrevSyncRecordByVaultAndProfile( fullfillMTimeOfRemoteEntityInplace(upload, mtimeCli);
db, await upsertPrevSyncRecordByVaultAndProfile(
vaultRandomID, db,
profileID, vaultRandomID,
upload profileID,
); upload
);
}
}, },
async (download) => { async (download) => {
await upsertPrevSyncRecordByVaultAndProfile( if (download !== undefined) {
db, await upsertPrevSyncRecordByVaultAndProfile(
vaultRandomID, db,
profileID, vaultRandomID,
download profileID,
); download
);
}
} }
); );
} }
@@ -1712,7 +1725,8 @@ export async function syncer(
settings.syncDirection ?? "bidirectional", settings.syncDirection ?? "bidirectional",
profiler, profiler,
settings, settings,
triggerSource triggerSource,
configDir
); );
console.debug(`mixedEntityMappings:`); console.debug(`mixedEntityMappings:`);
console.debug(mixedEntityMappings); // for debugging console.debug(mixedEntityMappings); // for debugging
+4 -1
View File
@@ -91,7 +91,8 @@ export class FakeFsEncrypt extends FakeFs {
this.password !== "" ? method : "no password" this.password !== "" ? method : "no password"
})`; })`;
if (method === "rclone-base64") { if (this.password !== "" && method === "rclone-base64") {
// no need to init if no password or not rclone
this.cipherRClone = new rclone.CipherRclone(password, 5); this.cipherRClone = new rclone.CipherRclone(password, 5);
} }
} }
@@ -137,6 +138,8 @@ export class FakeFsEncrypt extends FakeFs {
}; };
} }
} else { } else {
// the config has a password
if (this.method === "unknown") { if (this.method === "unknown") {
return { return {
ok: false, ok: false,
+86 -13
View File
@@ -264,6 +264,9 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
// another possibile prefix // another possibile prefix
const FOURTH_COMMON_PREFIX_RAW = `/drive/items/`; const FOURTH_COMMON_PREFIX_RAW = `/drive/items/`;
// when to use decode?
const remoteBaseDirEncoded = encodeURIComponent(remoteBaseDir);
if ( if (
x.parentReference === undefined || x.parentReference === undefined ||
x.parentReference === null || x.parentReference === null ||
@@ -279,6 +282,8 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
SECOND_COMMON_PREFIX_REGEX SECOND_COMMON_PREFIX_REGEX
); );
const matchThirdPrefixRes = fullPathOriginal.match(THIRD_COMMON_PREFIX_REGEX); const matchThirdPrefixRes = fullPathOriginal.match(THIRD_COMMON_PREFIX_REGEX);
// first
if ( if (
matchFirstPrefixRes !== null && matchFirstPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${remoteBaseDir}`) fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${remoteBaseDir}`)
@@ -286,24 +291,68 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`; const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1); key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if ( } else if (
matchFirstPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchFirstPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// fifth
else if (
matchFifthPrefixRes !== null && matchFifthPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchFifthPrefixRes[0]}${remoteBaseDir}`) fullPathOriginal.startsWith(`${matchFifthPrefixRes[0]}${remoteBaseDir}`)
) { ) {
const foundPrefix = `${matchFifthPrefixRes[0]}${remoteBaseDir}`; const foundPrefix = `${matchFifthPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1); key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if ( } else if (
matchFifthPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchFifthPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchFifthPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// second
else if (
matchSecondPrefixRes !== null && matchSecondPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`) fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`)
) { ) {
const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDir}`; const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1); key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if ( } else if (
matchSecondPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchSecondPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// third
else if (
matchThirdPrefixRes !== null && matchThirdPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchThirdPrefixRes[0]}${remoteBaseDir}`) fullPathOriginal.startsWith(`${matchThirdPrefixRes[0]}${remoteBaseDir}`)
) { ) {
const foundPrefix = `${matchThirdPrefixRes[0]}${remoteBaseDir}`; const foundPrefix = `${matchThirdPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1); key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (x.parentReference.path.startsWith(FOURTH_COMMON_PREFIX_RAW)) { } else if (
matchThirdPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchThirdPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchThirdPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// fourth
else if (x.parentReference.path.startsWith(FOURTH_COMMON_PREFIX_RAW)) {
// it's something like // it's something like
// /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder> // /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder>
// with uri encoded! // with uri encoded!
@@ -321,16 +370,27 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
key = x.name; key = x.name;
} else { } else {
throw Error( throw Error(
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToEntityError( `file/folder with /drive/items/, no idea how to deal with it:
x fullPathOriginal=${fullPathOriginal}
)}` matchFirstPrefixRes=${matchFirstPrefixRes}
matchFifthPrefixRes=${matchFifthPrefixRes}
matchSecondPrefixRes=${matchSecondPrefixRes}
matchThirdPrefixRes=${matchThirdPrefixRes}
${constructFromDriveItemToEntityError(x)}`
); );
} }
} else { }
// others
else {
throw Error( throw Error(
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToEntityError( `file/folder, no idea how to deal with it without known prefix:
x fullPathOriginal=${fullPathOriginal}
)}` matchFirstPrefixRes=${matchFirstPrefixRes}
matchFifthPrefixRes=${matchFifthPrefixRes}
matchSecondPrefixRes=${matchSecondPrefixRes}
matchThirdPrefixRes=${matchThirdPrefixRes}
${constructFromDriveItemToEntityError(x)}`
); );
} }
@@ -926,11 +986,24 @@ export class FakeFsOnedrive extends FakeFs {
).arrayBuffer; ).arrayBuffer;
return content; return content;
} else { } else {
// cannot set no-cache here, will have cors error // so strange, sometimes (!!!)
const content = await ( // we cannot download the file because of CORS
await fetch(downloadUrl, { cache: "no-store" }) try {
).arrayBuffer(); // cannot set no-cache here, will have cors error
return content; const content = await (
await fetch(downloadUrl, { cache: "no-store" })
).arrayBuffer();
return content;
} catch (e) {
// let's try again to bypass the CORS
const content = (
await requestUrl({
url: downloadUrl,
headers: { "Cache-Control": "no-cache" },
})
).arrayBuffer;
return content;
}
} }
} }
+1 -1
View File
@@ -269,7 +269,7 @@
"settings_chooseservice_s3": "S3 or compatible", "settings_chooseservice_s3": "S3 or compatible",
"settings_chooseservice_dropbox": "Dropbox", "settings_chooseservice_dropbox": "Dropbox",
"settings_chooseservice_webdav": "Webdav", "settings_chooseservice_webdav": "Webdav",
"settings_chooseservice_onedrive": "OneDrive for personal (App Folder)", "settings_chooseservice_onedrive": "OneDrive for personal",
"settings_chooseservice_webdis": "Webdis (HTTP for Redis®)", "settings_chooseservice_webdis": "Webdis (HTTP for Redis®)",
"settings_adv": "Advanced Settings", "settings_adv": "Advanced Settings",
"settings_concurrency": "Concurrency", "settings_concurrency": "Concurrency",
+2 -2
View File
@@ -58,7 +58,7 @@
"statusbar_time_days": "{{time}} 天前", "statusbar_time_days": "{{time}} 天前",
"statusbar_time_hours": "{{time}} 小时前", "statusbar_time_hours": "{{time}} 小时前",
"statusbar_time_minutes": "{{time}} 分钟前", "statusbar_time_minutes": "{{time}} 分钟前",
"statusbar_time_lessminute": "1 分钟内", "statusbar_time_lessminute": "1 分钟内",
"statusbar_time_now": "刚刚", "statusbar_time_now": "刚刚",
"statusbar_syncing": "正在同步", "statusbar_syncing": "正在同步",
"statusbar_lastsync_label": "日期:{{date}}", "statusbar_lastsync_label": "日期:{{date}}",
@@ -268,7 +268,7 @@
"settings_chooseservice_s3": "S3 或兼容 S3 的服务", "settings_chooseservice_s3": "S3 或兼容 S3 的服务",
"settings_chooseservice_dropbox": "Dropbox", "settings_chooseservice_dropbox": "Dropbox",
"settings_chooseservice_webdav": "Webdav", "settings_chooseservice_webdav": "Webdav",
"settings_chooseservice_onedrive": "OneDrive(个人版)App Folder", "settings_chooseservice_onedrive": "OneDrive(个人版)",
"settings_chooseservice_webdis": "Webdis (an HTTP interface for Redis)", "settings_chooseservice_webdis": "Webdis (an HTTP interface for Redis)",
"settings_adv": "进阶设置", "settings_adv": "进阶设置",
"settings_concurrency": "并行度", "settings_concurrency": "并行度",
+2 -2
View File
@@ -57,7 +57,7 @@
"statusbar_time_days": "{{time}} 天前", "statusbar_time_days": "{{time}} 天前",
"statusbar_time_hours": "{{time}} 小時前", "statusbar_time_hours": "{{time}} 小時前",
"statusbar_time_minutes": "{{time}} 分鐘前", "statusbar_time_minutes": "{{time}} 分鐘前",
"statusbar_time_lessminute": "1 分鐘內", "statusbar_time_lessminute": "1 分鐘內",
"statusbar_time_now": "剛剛", "statusbar_time_now": "剛剛",
"statusbar_syncing": "正在同步", "statusbar_syncing": "正在同步",
"statusbar_lastsync_label": "日期:{{date}}", "statusbar_lastsync_label": "日期:{{date}}",
@@ -267,7 +267,7 @@
"settings_chooseservice_s3": "S3 或相容 S3 的服務", "settings_chooseservice_s3": "S3 或相容 S3 的服務",
"settings_chooseservice_dropbox": "Dropbox", "settings_chooseservice_dropbox": "Dropbox",
"settings_chooseservice_webdav": "Webdav", "settings_chooseservice_webdav": "Webdav",
"settings_chooseservice_onedrive": "OneDrive(個人版)App Folder", "settings_chooseservice_onedrive": "OneDrive(個人版)",
"settings_chooseservice_webdis": "Webdis (an HTTP interface for Redis®)", "settings_chooseservice_webdis": "Webdis (an HTTP interface for Redis®)",
"settings_adv": "進階設定", "settings_adv": "進階設定",
"settings_concurrency": "並行度", "settings_concurrency": "並行度",
+11 -4
View File
@@ -1921,15 +1921,22 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropdown.addOption("dropbox", t("settings_chooseservice_dropbox")); dropdown.addOption("dropbox", t("settings_chooseservice_dropbox"));
dropdown.addOption("webdav", t("settings_chooseservice_webdav")); dropdown.addOption("webdav", t("settings_chooseservice_webdav"));
dropdown.addOption("onedrive", t("settings_chooseservice_onedrive")); dropdown.addOption("onedrive", t("settings_chooseservice_onedrive"));
dropdown.addOption(
"onedrivefull",
t("settings_chooseservice_onedrivefull")
);
dropdown.addOption("webdis", t("settings_chooseservice_webdis")); dropdown.addOption("webdis", t("settings_chooseservice_webdis"));
dropdown.addOption("separator line", "-----");
(dropdown.selectEl.lastChild as HTMLElement).setAttribute(
"disabled",
"disabled"
);
dropdown.addOption( dropdown.addOption(
"googledrive", "googledrive",
t("settings_chooseservice_googledrive") t("settings_chooseservice_googledrive")
); );
dropdown.addOption(
"onedrivefull",
t("settings_chooseservice_onedrivefull")
);
dropdown.addOption("box", t("settings_chooseservice_box")); dropdown.addOption("box", t("settings_chooseservice_box"));
dropdown.addOption("pcloud", t("settings_chooseservice_pcloud")); dropdown.addOption("pcloud", t("settings_chooseservice_pcloud"));
dropdown.addOption( dropdown.addOption(