## 0.24.26

### New Features

- Automatic display-language changing according to the Obsidian language
  setting.
- Now we can limit files to be synchronised even in the hidden files.
- "Use Request API to avoid `inevitable` CORS problem" has been implemented.
- `Show status icon instead of file warnings banner` has been implemented.

### Improved

- All regular expressions can be inverted by prefixing `!!` now.

### Fixed

- No longer unexpected files will be gathered during hidden file sync.
- No longer broken `\n` and new-line characters during the bucket
  synchronisation.
- We can purge the remote bucket again if we using MinIO instead of AWS S3 or
  Cloudflare R2.
- Purging the remote bucket is now more reliable.
- Some wrong messages have been fixed.

### Behaviour changed

- Entering into the deeper directories to gather the hidden files is now limited
  by `/` or `\/` prefixed ignore filters.

### Etcetera

- Some code has been tidied up.
- Trying less warning-suppressing and be more safer-coding.
- Dependent libraries have been updated to the latest version.
- Some build processes have been separated to `pre` and `post` processes.
This commit is contained in:
vorotamoroz
2025-05-14 13:11:03 +01:00
parent d0e92cff7a
commit 3b8d03a189
15 changed files with 3069 additions and 2198 deletions

4819
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,10 @@
"type": "module",
"scripts": {
"bakei18n": "npx tsx ./src/lib/_tools/bakei18n.ts",
"postbakei18n": "prettier --config ./.prettierrc ./src/lib/src/common/messages/*.ts --write --log-level error",
"dev": "node esbuild.config.mjs",
"build": "npm run bakei18n && node esbuild.config.mjs production",
"prebuild": "npm run bakei18n",
"build": "node esbuild.config.mjs production",
"buildDev": "node esbuild.config.mjs dev",
"lint": "eslint src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
@@ -22,12 +24,12 @@
"license": "MIT",
"devDependencies": {
"@chialab/esbuild-plugin-worker": "^0.18.1",
"@eslint/compat": "^1.2.6",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@eslint/compat": "^1.2.7",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@tsconfig/svelte": "^5.0.4",
"@types/diff-match-patch": "^1.0.36",
"@types/node": "^22.5.4",
"@types/node": "^22.13.8",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
@@ -36,17 +38,17 @@
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"builtin-modules": "^4.0.0",
"esbuild": "0.24.2",
"@typescript-eslint/eslint-plugin": "8.25.0",
"@typescript-eslint/parser": "8.25.0",
"builtin-modules": "5.0.0",
"esbuild": "0.25.0",
"esbuild-svelte": "^0.9.0",
"eslint": "^9.20.1",
"eslint": "^9.21.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-svelte": "^2.46.1",
"eslint-plugin-svelte": "^3.0.2",
"events": "^3.3.0",
"obsidian": "^1.7.2",
"postcss": "^8.5.2",
"obsidian": "^1.8.7",
"postcss": "^8.5.3",
"postcss-load-config": "^6.0.1",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
@@ -58,28 +60,30 @@
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "^3.5.1",
"svelte": "^5.20.1",
"prettier": "3.5.2",
"svelte": "5.28.6",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"transform-pouch": "^2.0.0",
"tslib": "^2.8.1",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
"tsx": "^4.19.4",
"typescript": "5.7.3"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.787.0",
"@aws-sdk/client-s3": "^3.808.0",
"@smithy/md5-js": "^4.0.2",
"@smithy/middleware-apply-body-checksum": "^4.1.0",
"@smithy/fetch-http-handler": "^5.0.2",
"@smithy/protocol-http": "^5.1.0",
"@smithy/querystring-builder": "^4.0.2",
"diff-match-patch": "^1.0.5",
"esbuild-plugin-inline-worker": "^0.1.1",
"fflate": "^0.8.2",
"idb": "^8.0.2",
"idb": "^8.0.3",
"minimatch": "^10.0.1",
"octagonal-wheels": "^0.1.25",
"qrcode-generator": "^1.4.4",
"svelte-check": "^4.1.4",
"svelte-check": "^4.1.7",
"trystero": "^0.21.3",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}

View File

@@ -25,6 +25,8 @@ import {
readContent,
createBlob,
fireAndForget,
type CustomRegExp,
getFileRegExp,
} from "../../lib/src/common/utils.ts";
import {
compareMTime,
@@ -164,12 +166,11 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
return Promise.resolve(true);
}
updateSettingCache() {
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
this.ignorePatterns = ignorePatterns;
const targetFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
this.targetPatterns = targetFilter;
this.shouldSkipFile = [] as FilePathWithPrefixLC[];
// Exclude files handled by customization sync
const configDir = normalizePath(this.app.vault.configDir);
@@ -219,12 +220,10 @@ export class HiddenFileSync extends LiveSyncCommands implements IObsidianModule
? this.settings.syncInternalFilesInterval * 1000
: 0
);
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
this.ignorePatterns = ignorePatterns;
const targetFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
this.targetPatterns = targetFilter;
return Promise.resolve(true);
}
@@ -1683,14 +1682,12 @@ ${messageFetch}${messageOverwrite}${messageMerge}
// <-- Configuration handling
// --> Local Storage SubFunctions
ignorePatterns: RegExp[] = [];
ignorePatterns: CustomRegExp[] = [];
targetPatterns: CustomRegExp[] = [];
async scanInternalFileNames() {
const configDir = normalizePath(this.app.vault.configDir);
const ignoreFilter = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
const ignoreFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
const targetFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
const synchronisedInConfigSync = !this.settings.usePluginSync
? []
: Object.values(this.settings.pluginSyncExtendedSetting)
@@ -1701,7 +1698,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
const root = this.app.vault.getRoot();
const findRoot = root.path;
const filenames = (await this.getFiles(findRoot, [], undefined, ignoreFilter))
const filenames = (await this.getFiles(findRoot, [], targetFilter, ignoreFilter))
.filter((e) => e.startsWith("."))
.filter((e) => !e.startsWith(".trash"));
const files = filenames.filter((path) =>
@@ -1737,7 +1734,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
return result;
}
async getFiles(path: string, ignoreList: string[], filter?: RegExp[], ignoreFilter?: RegExp[]) {
async getFiles(path: string, ignoreList: string[], filter?: CustomRegExp[], ignoreFilter?: CustomRegExp[]) {
let w: ListedFiles;
try {
w = await this.app.vault.adapter.list(path);
@@ -1746,26 +1743,32 @@ ${messageFetch}${messageOverwrite}${messageMerge}
this._log(ex, LOG_LEVEL_VERBOSE);
return [];
}
const filesSrc = [
...w.files
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
.filter((e) => !filter || filter.some((ee) => e.match(ee)))
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))),
];
let files = [] as string[];
for (const file of filesSrc) {
if (!(await this.plugin.$$isIgnoredByIgnoreFiles(file))) {
files.push(file);
for (const file of w.files) {
if (ignoreList && ignoreList.length > 0) {
if (ignoreList.some((e) => file.endsWith(e))) continue;
}
if (filter && filter.length > 0) {
if (!filter.some((e) => e.test(file))) {
continue;
}
}
if (ignoreFilter && ignoreFilter.some((ee) => ee.test(file))) {
continue;
}
if (await this.plugin.$$isIgnoredByIgnoreFiles(file)) continue;
files.push(file);
}
L1: for (const v of w.folders) {
for (const ignore of ignoreList) {
if (v.endsWith(ignore)) {
continue L1;
}
}
if (ignoreFilter && ignoreFilter.some((e) => v.match(e))) {
if (
ignoreFilter &&
ignoreFilter.some((e) => (e.pattern.startsWith("/") || e.pattern.startsWith("\\/")) && e.test(v))
) {
continue L1;
}
if (await this.plugin.$$isIgnoredByIgnoreFiles(v)) {

Submodule src/lib updated: c8bb4fedbb...d53cad1c68

View File

@@ -291,7 +291,8 @@ export default class ObsidianLiveSyncPlugin
performSetup: boolean,
skipInfo: boolean,
compression: boolean,
customHeaders: Record<string, string>
customHeaders: Record<string, string>,
useRequestAPI: boolean
): Promise<
| string
| {

View File

@@ -14,7 +14,7 @@ import type {
import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "./storageLib/utilObsidian.ts";
import { StorageEventManagerObsidian, type StorageEventManager } from "./storageLib/StorageEventManager";
import type { StorageAccess } from "../interfaces/StorageAccess";
import { createBlob } from "../../lib/src/common/utils";
import { createBlob, type CustomRegExp } from "../../lib/src/common/utils";
export class ModuleFileAccessObsidian extends AbstractObsidianModule implements IObsidianModule, StorageAccess {
vaultAccess!: SerializedFileAccess;
@@ -223,8 +223,8 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
async getFilesIncludeHidden(
basePath: string,
includeFilter?: RegExp[],
excludeFilter?: RegExp[],
includeFilter?: CustomRegExp[],
excludeFilter?: CustomRegExp[],
skipFolder: string[] = [".git", ".trash", "node_modules"]
): Promise<FilePath[]> {
let w: ListedFiles;
@@ -239,16 +239,11 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
let files = [] as string[];
for (const file of w.files) {
if (excludeFilter && excludeFilter.some((ee) => file.match(ee))) {
// If excludeFilter and includeFilter are both set, the file will be included in the list.
if (includeFilter) {
if (!includeFilter.some((e) => file.match(e))) continue;
} else {
continue;
}
if (includeFilter && includeFilter.length > 0) {
if (!includeFilter.some((e) => e.test(file))) continue;
}
if (includeFilter) {
if (!includeFilter.some((e) => file.match(e))) continue;
if (excludeFilter && excludeFilter.some((ee) => ee.test(file))) {
continue;
}
if (await this.plugin.$$isIgnoredByIgnoreFiles(file)) continue;
files.push(file);
@@ -259,16 +254,12 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
if (skipFolder.some((e) => folderName === e)) {
continue;
}
if (excludeFilter && excludeFilter.some((e) => v.match(e))) {
if (includeFilter) {
if (!includeFilter.some((e) => v.match(e))) {
continue;
}
}
}
if (includeFilter) {
if (!includeFilter.some((e) => v.match(e))) continue;
if (excludeFilter && excludeFilter.some((e) => e.test(v))) {
continue;
}
if (await this.plugin.$$isIgnoredByIgnoreFiles(v)) {
continue;
}
// OK, deep dive!
files = files.concat(await this.getFilesIncludeHidden(v, includeFilter, excludeFilter, skipFolder));

View File

@@ -12,7 +12,7 @@ import {
type UXFileInfoStub,
type UXInternalFileInfoStub,
} from "../../../lib/src/common/types.ts";
import { delay, fireAndForget } from "../../../lib/src/common/utils.ts";
import { delay, fireAndForget, getFileRegExp } from "../../../lib/src/common/utils.ts";
import { type FileEventItem, type FileEventType } from "../../../common/types.ts";
import { serialized, skipIfDuplicated } from "../../../lib/src/concurrency/lock.ts";
import {
@@ -161,12 +161,10 @@ export class StorageEventManagerObsidian extends StorageEventManager {
if (!this.plugin.settings.syncInternalFiles && !this.plugin.settings.usePluginSync) return;
if (!this.plugin.settings.watchInternalFileChanges) return;
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
if (ignorePatterns.some((e) => path.match(e))) return;
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
if (ignorePatterns.some((e) => e.test(path))) return;
if (!targetPatterns.some((e) => e.test(path))) return;
if (path.endsWith("/")) {
// Folder
return;

View File

@@ -17,17 +17,8 @@ import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.
setNoticeClass(Notice);
async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse> {
const ret = await requestUrl(request);
if (ret.status - (ret.status % 100) !== 200) {
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
if (ret.json) {
er.message = ret.json.reason;
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
}
er.status = ret.status;
throw er;
}
async function fetchByAPI(request: RequestUrlParam, errorAsResult = false): Promise<RequestUrlResponse> {
const ret = await requestUrl({ ...request, throw: !errorAsResult });
return ret;
}
@@ -45,13 +36,7 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
return !this.last_successful_post;
}
async fetchByAPI(
url: string,
localURL: string,
method: string,
authHeader: string,
opts?: RequestInit
): Promise<Response> {
async _fetchByAPI(url: string, authHeader: string, opts?: RequestInit): Promise<Response> {
const body = opts?.body as string;
const transformedHeaders = { ...(opts?.headers as Record<string, string>) };
@@ -65,25 +50,36 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
method: opts?.method,
body: body,
headers: transformedHeaders,
contentType: "application/json",
// contentType: opts.headers,
contentType:
transformedHeaders?.["content-type"] ?? transformedHeaders?.["Content-Type"] ?? "application/json",
};
const r = await fetchByAPI(requestParam, true);
return new Response(r.arrayBuffer, {
headers: r.headers,
status: r.status,
statusText: `${r.status}`,
});
}
async fetchByAPI(
url: string,
localURL: string,
method: string,
authHeader: string,
opts?: RequestInit
): Promise<Response> {
const body = opts?.body as string;
const size = body ? ` (${body.length})` : "";
try {
const r = await this._fetchByAPI(url, authHeader, opts);
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const r = await fetchByAPI(requestParam);
if (method == "POST" || method == "PUT") {
this.last_successful_post = r.status - (r.status % 100) == 200;
} else {
this.last_successful_post = true;
}
this._log(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL_DEBUG);
return new Response(r.arrayBuffer, {
headers: r.headers,
status: r.status,
statusText: `${r.status}`,
});
return r;
} catch (ex) {
this._log(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
// limit only in bulk_docs.
@@ -106,7 +102,8 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
performSetup: boolean,
skipInfo: boolean,
compression: boolean,
customHeaders: Record<string, string>
customHeaders: Record<string, string>,
useRequestAPI: boolean
): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> {
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
@@ -147,15 +144,11 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
headers.append("authorization", authHeader);
}
if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "string") {
// Deprecated configuration, only for backward compatibility.
return await this.fetchByAPI(url, localURL, method, authHeader, { ...opts, headers });
}
// --> native Fetch API.
try {
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
const response: Response = await fetch(url, { ...opts, headers });
const response: Response = await (useRequestAPI
? this._fetchByAPI(url.toString(), authHeader, { ...opts, headers })
: fetch(url, { ...opts, headers }));
if (method == "POST" || method == "PUT") {
this.last_successful_post = response.ok;
} else {
@@ -188,6 +181,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
return response;
} catch (ex) {
if (ex instanceof TypeError) {
if (useRequestAPI) {
this._log("Failed to request by API.");
throw ex;
}
this._log(
"Failed to fetch by native fetch API. Trying to fetch by API to get more information."
);

View File

@@ -7,6 +7,7 @@ import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex";
import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
import type { FilePath } from "../../lib/src/common/types.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { getFileRegExp } from "../../lib/src/common/utils.ts";
declare global {
interface LSEvents {
@@ -200,13 +201,10 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
}
async _dumpFileListIncludeHidden(outFile?: string) {
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
const out = [] as any[];
const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns);
const files = await this.core.storageAccess.getFilesIncludeHidden("", targetPatterns, ignorePatterns);
// console.dir(files);
const webcrypto = await getWebCrypto();
for (const file of files) {

View File

@@ -63,6 +63,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
statusBarLabels!: ReactiveValue<{ message: string; status: string }>;
statusLog = reactiveSource("");
activeFileStatus = reactiveSource("");
notifies: { [key: string]: { notice: Notice; count: number } } = {};
p2pLogCollector = new P2PLogCollector();
@@ -181,10 +182,11 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
: "";
const { message } = statusLineLabel();
const fileStatus = this.activeFileStatus.value;
const status = scheduleMessage + this.statusLog.value;
const fileStatusIcon = `${fileStatus && this.settings.hideFileWarningNotice ? " ⛔ SKIP" : ""}`;
return {
message,
message: `${message}${fileStatusIcon}`,
status,
};
});
@@ -229,7 +231,9 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
return "";
}
async setFileStatus() {
this.messageArea!.innerText = await this.getActiveFileStatus();
const fileStatus = await this.getActiveFileStatus();
this.activeFileStatus.value = fileStatus;
this.messageArea!.innerText = this.settings.hideFileWarningNotice ? "" : fileStatus;
}
onActiveLeafChange() {
fireAndForget(async () => {

View File

@@ -11,9 +11,45 @@ import {
} from "../../lib/src/common/types";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger";
import { encrypt, tryDecrypt } from "octagonal-wheels/encryption";
import { setLang } from "../../lib/src/common/i18n";
import { $msg, setLang } from "../../lib/src/common/i18n";
import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb";
import { getLanguage } from "obsidian";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../lib/src/common/rosetta.ts";
export class ModuleObsidianSettings extends AbstractObsidianModule implements IObsidianModule {
async $everyOnLayoutReady(): Promise<boolean> {
let isChanged = false;
if (this.settings.displayLanguage == "") {
const obsidianLanguage = getLanguage();
if (
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
obsidianLanguage != this.settings.displayLanguage && // Check if the language is different from the current setting
this.settings.displayLanguage != ""
) {
// Check if the current setting is not empty (Means migrated or installed).
this.settings.displayLanguage = obsidianLanguage as I18N_LANGS;
isChanged = true;
setLang(this.settings.displayLanguage);
} else if (this.settings.displayLanguage == "") {
this.settings.displayLanguage = "def";
setLang(this.settings.displayLanguage);
await this.core.$$saveSettingData();
}
}
if (isChanged) {
const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault");
if (
(await this.core.confirm.askSelectStringDialogue($msg(`dialog.yourLanguageAvailable`), ["OK", revert], {
defaultAction: "OK",
title: $msg(`dialog.yourLanguageAvailable.Title`),
})) == revert
) {
this.settings.displayLanguage = "def";
setLang(this.settings.displayLanguage);
}
await this.core.$$saveSettingData();
}
return true;
}
getPassphrase(settings: ObsidianLiveSyncSettings) {
const methods: Record<ConfigPassphraseStore, () => Promise<string | false>> = {
"": () => Promise.resolve("*"),
@@ -199,6 +235,7 @@ export class ModuleObsidianSettings extends AbstractObsidianModule implements IO
}
}
this.settings = settings;
setLang(this.settings.displayLanguage);
if ("workingEncrypt" in this.settings) delete this.settings.workingEncrypt;

View File

@@ -1,31 +1,28 @@
<script lang="ts">
export let patterns = [] as string[];
export let originals = [] as string[];
import type { CustomRegExpSource } from "../../../lib/src/common/types";
import { isInvertedRegExp, isValidRegExp } from "../../../lib/src/common/utils";
export let apply: (args: string[]) => Promise<void> = (_: string[]) => Promise.resolve();
export let patterns = [] as CustomRegExpSource[];
export let originals = [] as CustomRegExpSource[];
export let apply: (args: CustomRegExpSource[]) => Promise<void> = (_: CustomRegExpSource[]) => Promise.resolve();
function revert() {
patterns = [...originals];
}
const CHECK_OK = "✔";
const CHECK_NG = "⚠";
const MARK_MODIFIED = "✏ ";
function checkRegExp(pattern: string) {
if (pattern.trim() == "") return "";
try {
new RegExp(pattern);
return CHECK_OK;
} catch (ex) {
return CHECK_NG;
}
function checkRegExp(pattern: CustomRegExpSource) {
return isValidRegExp(pattern) ? CHECK_OK : CHECK_NG;
}
$: statusName = patterns.map((e) => checkRegExp(e));
$: modified = patterns.map((e, i) => (e != (originals?.[i] ?? "") ? MARK_MODIFIED : ""));
$: isInvertedExp = patterns.map((e) => isInvertedRegExp(e));
function remove(idx: number) {
patterns[idx] = "";
patterns[idx] = "" as CustomRegExpSource;
}
function add() {
patterns = [...patterns, ""];
patterns = [...patterns, "" as CustomRegExpSource];
}
</script>
@@ -33,7 +30,9 @@
{#each patterns as pattern, idx}
<!-- svelte-ignore a11y-label-has-associated-control -->
<li>
<label>{modified[idx]}{statusName[idx]}</label><input type="text" bind:value={pattern} class={modified[idx]} />
<label>{modified[idx]}{statusName[idx]}</label>
<span class="chip">{isInvertedExp[idx] ? "INVERTED" : ""}</span>
<input type="text" bind:value={pattern} class={modified[idx]} />
<button class="iconbutton" on:click={() => remove(idx)}>🗑</button>
</li>
{/each}
@@ -43,8 +42,16 @@
</label>
</li>
<li class="buttons">
<button on:click={() => apply(patterns)} disabled={statusName.some((e) => e === CHECK_NG) || modified.every((e) => e === "")}>Apply </button>
<button on:click={() => revert()} disabled={statusName.some((e) => e === CHECK_NG) || modified.every((e) => e === "")}>Revert </button>
<button
on:click={() => apply(patterns)}
disabled={statusName.some((e) => e === CHECK_NG) || modified.every((e) => e === "")}
>Apply
</button>
<button
on:click={() => revert()}
disabled={statusName.some((e) => e === CHECK_NG) || modified.every((e) => e === "")}
>Revert
</button>
</li>
</ul>
@@ -85,4 +92,14 @@
button.iconbutton {
max-width: 4em;
}
.chip {
background-color: var(--tag-background);
color: var(--tag-color);
padding: var(--size-2-1) var(--size-4-1);
border-radius: 0.5em;
font-size: 0.8em;
}
.chip:empty {
display: none;
}
</style>

View File

@@ -26,15 +26,19 @@ import {
type MetaEntry,
type FilePath,
REMOTE_P2P,
type CustomRegExpSource,
} from "../../../lib/src/common/types.ts";
import {
constructCustomRegExpList,
createBlob,
delay,
getFileRegExp,
isDocContentSame,
isObjectDifferent,
parseHeaderValues,
readAsBlob,
sizeToHumanReadable,
splitCustomRegExpList,
} from "../../../lib/src/common/utils.ts";
import { arrayBufferToBase64Single, versionNumberString2Number } from "../../../lib/src/string_and_binary/convert.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
@@ -1132,7 +1136,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
(paneEl) => {
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleAppearance")).then((paneEl) => {
const languages = Object.fromEntries([
["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")],
// ["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")],
...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]),
]) as Record<I18N_LANGS, string>;
new Setting(paneEl).autoWireDropDown("displayLanguage", {
@@ -1144,6 +1148,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)),
});
new Setting(paneEl).autoWireToggle("showStatusOnStatusbar");
new Setting(paneEl).autoWireToggle("hideFileWarningNotice");
});
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => {
paneEl.addClass("wizardHidden");
@@ -2218,13 +2223,10 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
mount(MultipleRegExpControl, {
target: syncFilesSetting.controlEl,
props: {
patterns: this.editingSettings.syncOnlyRegEx.split("|[]|"),
originals: [...this.editingSettings.syncOnlyRegEx.split("|[]|")],
apply: async (newPatterns: string[]) => {
this.editingSettings.syncOnlyRegEx = newPatterns
.map((e: string) => e.trim())
.filter((e) => e != "")
.join("|[]|");
patterns: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"),
originals: splitCustomRegExpList(this.editingSettings.syncOnlyRegEx, "|[]|"),
apply: async (newPatterns: CustomRegExpSource[]) => {
this.editingSettings.syncOnlyRegEx = constructCustomRegExpList(newPatterns, "|[]|");
await this.saveAllDirtySettings();
this.display();
},
@@ -2241,13 +2243,10 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
mount(MultipleRegExpControl, {
target: nonSyncFilesSetting.controlEl,
props: {
patterns: this.editingSettings.syncIgnoreRegEx.split("|[]|"),
originals: [...this.editingSettings.syncIgnoreRegEx.split("|[]|")],
apply: async (newPatterns: string[]) => {
this.editingSettings.syncIgnoreRegEx = newPatterns
.map((e) => e.trim())
.filter((e) => e != "")
.join("|[]|");
patterns: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"),
originals: splitCustomRegExpList(this.editingSettings.syncIgnoreRegEx, "|[]|"),
apply: async (newPatterns: CustomRegExpSource[]) => {
this.editingSettings.syncIgnoreRegEx = constructCustomRegExpList(newPatterns, "|[]|");
await this.saveAllDirtySettings();
this.display();
},
@@ -2261,14 +2260,32 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
});
});
void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => {
const targetPatternSetting = new Setting(paneEl)
.setName("Target patterns")
.setClass("wizardHidden")
.setDesc("Patterns to match files for syncing");
const patTarget = splitCustomRegExpList(this.editingSettings.syncInternalFilesTargetPatterns, ",");
mount(MultipleRegExpControl, {
target: targetPatternSetting.controlEl,
props: {
patterns: patTarget,
originals: [...patTarget],
apply: async (newPatterns: CustomRegExpSource[]) => {
this.editingSettings.syncInternalFilesTargetPatterns = constructCustomRegExpList(
newPatterns,
","
);
await this.saveAllDirtySettings();
this.display();
},
},
});
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, ^\\.git\\/, \\/obsidian-livesync\\/";
const defaultSkipPatternXPlat =
defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$";
const pat = this.editingSettings.syncInternalFilesIgnorePatterns
.split(",")
.map((x) => x.trim())
.filter((x) => x != "");
const pat = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ",");
const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc("");
mount(MultipleRegExpControl, {
@@ -2276,11 +2293,11 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
props: {
patterns: pat,
originals: [...pat],
apply: async (newPatterns: string[]) => {
this.editingSettings.syncInternalFilesIgnorePatterns = newPatterns
.map((e) => e.trim())
.filter((e) => e != "")
.join(", ");
apply: async (newPatterns: CustomRegExpSource[]) => {
this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList(
newPatterns,
","
);
await this.saveAllDirtySettings();
this.display();
},
@@ -2288,16 +2305,13 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
});
const addDefaultPatterns = async (patterns: string) => {
const oldList = this.editingSettings.syncInternalFilesIgnorePatterns
.split(",")
.map((x) => x.trim())
.filter((x) => x != "");
const newList = patterns
.split(",")
.map((x) => x.trim())
.filter((x) => x != "");
const allSet = new Set([...oldList, ...newList]);
this.editingSettings.syncInternalFilesIgnorePatterns = [...allSet].join(", ");
const oldList = splitCustomRegExpList(this.editingSettings.syncInternalFilesIgnorePatterns, ",");
const newList = splitCustomRegExpList(
patterns as unknown as typeof this.editingSettings.syncInternalFilesIgnorePatterns,
","
);
const allSet = new Set<CustomRegExpSource>([...oldList, ...newList]);
this.editingSettings.syncInternalFilesIgnorePatterns = constructCustomRegExpList([...allSet], ",");
await this.saveAllDirtySettings();
this.display();
};
@@ -2691,17 +2705,20 @@ ${stringifyYaml(pluginConfig)}`;
.setCta()
.onClick(async () => {
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
.replace(/\n| /g, "")
.split(",")
.filter((e) => e)
.map((e) => new RegExp(e, "i"));
const ignorePatterns = getFileRegExp(
this.plugin.settings,
"syncInternalFilesIgnorePatterns"
);
const targetPatterns = getFileRegExp(
this.plugin.settings,
"syncInternalFilesTargetPatterns"
);
this.plugin.localDatabase.hashCaches.clear();
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
const files = this.plugin.settings.syncInternalFiles
? await this.plugin.storageAccess.getFilesIncludeHidden(
"/",
undefined,
targetPatterns,
ignorePatterns
)
: await this.plugin.storageAccess.getFileNames();
@@ -2721,7 +2738,7 @@ ${stringifyYaml(pluginConfig)}`;
i++;
if (i % 25 == 0)
Logger(
`Checking ${i}/${files.length} files \n`,
`Checking ${i}/${allPaths.length} files \n`,
LOG_LEVEL_NOTICE,
"verify-processed"
);
@@ -3087,7 +3104,9 @@ ${stringifyYaml(pluginConfig)}`;
onUpdate: visibleOnly(() => this.isConfiguredAs("disableWorkerForGeneratingChunks", false)),
});
});
void addPanel(paneEl, "Edge case addressing (Networking)").then((paneEl) => {
new Setting(paneEl).autoWireToggle("useRequestAPI");
});
void addPanel(paneEl, "Compatibility (Trouble addressed)").then((paneEl) => {
new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch");
});

View File

@@ -377,6 +377,14 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
name: "Minimum interval for syncing",
desc: "The minimum interval for automatic synchronisation on event.",
},
useRequestAPI: {
name: "Use Request API to avoid `inevitable` CORS problem",
desc: "If enabled, the request API will be used to avoid `inevitable` CORS problems. This is a workaround and may not work in all cases. PLEASE READ THE DOCUMENTATION BEFORE USING THIS OPTION. This is a less-secure option.",
},
hideFileWarningNotice: {
name: "Show status icon instead of file warnings banner",
desc: "If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown.",
},
};
function translateInfo(infoSrc: ConfigurationItem | undefined | false) {
if (!infoSrc) return false;

View File

@@ -7,6 +7,7 @@ import type {
UXFolderInfo,
UXStat,
} from "../../lib/src/common/types";
import type { CustomRegExp } from "../../lib/src/common/utils";
export interface StorageAccess {
deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void>;
@@ -48,8 +49,8 @@ export interface StorageAccess {
getFilesIncludeHidden(
basePath: string,
includeFilter?: RegExp[],
excludeFilter?: RegExp[],
includeFilter?: CustomRegExp[],
excludeFilter?: CustomRegExp[],
skipFolder?: string[]
): Promise<FilePath[]>;
}