mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 13:02:58 +00:00
Reduce Community lint type-safety warnings
This commit is contained in:
@@ -189,7 +189,7 @@ export async function syncWithPeer(
|
|||||||
}
|
}
|
||||||
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
||||||
if (!pushResult || pushResult.ok !== true) {
|
if (!pushResult || pushResult.ok !== true) {
|
||||||
const err = pushResult?.error;
|
const err: unknown = pushResult && "error" in pushResult ? pushResult.error : undefined;
|
||||||
throw err instanceof Error
|
throw err instanceof Error
|
||||||
? err
|
? err
|
||||||
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
|
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
|
||||||
|
|||||||
@@ -8,11 +8,8 @@ import LevelDBAdapter from "pouchdb-adapter-leveldb";
|
|||||||
|
|
||||||
import find from "pouchdb-find";
|
import find from "pouchdb-find";
|
||||||
import transform from "transform-pouch";
|
import transform from "transform-pouch";
|
||||||
//@ts-ignore
|
import { findPathToLeaf, type RevisionTreeNode } from "pouchdb-merge";
|
||||||
import { findPathToLeaf } from "pouchdb-merge";
|
|
||||||
//@ts-ignore
|
|
||||||
import { adapterFun } from "pouchdb-utils";
|
import { adapterFun } from "pouchdb-utils";
|
||||||
//@ts-ignore
|
|
||||||
import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors";
|
import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors";
|
||||||
import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task";
|
import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task";
|
||||||
|
|
||||||
@@ -28,8 +25,32 @@ type PurgeLogDocument = {
|
|||||||
purgeSeq: number;
|
purgeSeq: number;
|
||||||
purges: Array<{ docId: string; rev: string; purgeSeq: number }>;
|
purges: Array<{ docId: string; rev: string; purgeSeq: number }>;
|
||||||
};
|
};
|
||||||
|
type PurgeMultiResultMap = Record<string, unknown>;
|
||||||
|
|
||||||
function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
|
interface PouchDBPrivateDatabase extends PouchDB.Database {
|
||||||
|
adapter: string;
|
||||||
|
purged_infos_limit: number;
|
||||||
|
_getRevisionTree(
|
||||||
|
documentId: string,
|
||||||
|
callback: (error: Error | undefined, revisions?: RevisionTreeNode[]) => void
|
||||||
|
): void;
|
||||||
|
_purge(
|
||||||
|
documentId: string,
|
||||||
|
revisionPath: string[],
|
||||||
|
callback: (error: Error | undefined, result?: PurgeMultiResult) => void
|
||||||
|
): void;
|
||||||
|
purgeMulti(documents: PurgeMultiParam[]): Promise<PurgeMultiResultMap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSuccessfulPurge(value: unknown): value is PurgeMultiResult {
|
||||||
|
return isRecord(value) && value.ok === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendPurgeSeqs(db: PouchDBPrivateDatabase, docs: PurgeMultiParam[]) {
|
||||||
return db
|
return db
|
||||||
.get<PurgeLogDocument>("_local/purges")
|
.get<PurgeLogDocument>("_local/purges")
|
||||||
.then(function (doc) {
|
.then(function (doc) {
|
||||||
@@ -40,18 +61,16 @@ function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
|
|||||||
rev: rev$$1,
|
rev: rev$$1,
|
||||||
purgeSeq,
|
purgeSeq,
|
||||||
});
|
});
|
||||||
//@ts-ignore : missing type def
|
|
||||||
if (doc.purges.length > db.purged_infos_limit) {
|
if (doc.purges.length > db.purged_infos_limit) {
|
||||||
//@ts-ignore : missing type def
|
|
||||||
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
|
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
|
||||||
}
|
}
|
||||||
doc.purgeSeq = purgeSeq;
|
doc.purgeSeq = purgeSeq;
|
||||||
}
|
}
|
||||||
return doc;
|
return doc;
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (error: unknown) {
|
||||||
if (err.status !== 404) {
|
if (!isRecord(error) || error.status !== 404) {
|
||||||
throw err;
|
throw error;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
_id: "_local/purges",
|
_id: "_local/purges",
|
||||||
@@ -71,68 +90,76 @@ function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
|
|||||||
/**
|
/**
|
||||||
* purge multiple documents at once.
|
* purge multiple documents at once.
|
||||||
*/
|
*/
|
||||||
PouchDB.prototype.purgeMulti = adapterFun(
|
const pouchDBPrototype = (PouchDB as typeof PouchDB & { prototype: PouchDBPrivateDatabase }).prototype;
|
||||||
|
|
||||||
|
pouchDBPrototype.purgeMulti = adapterFun<PouchDBPrivateDatabase, [documents: PurgeMultiParam[]], PurgeMultiResultMap>(
|
||||||
"_purgeMulti",
|
"_purgeMulti",
|
||||||
function (
|
function (
|
||||||
|
this: PouchDBPrivateDatabase,
|
||||||
docs: PurgeMultiParam[],
|
docs: PurgeMultiParam[],
|
||||||
callback: (
|
callback: (error?: Error, result?: PurgeMultiResultMap) => void
|
||||||
error: Error,
|
|
||||||
result?: {
|
|
||||||
[x: string]: PurgeMultiResult | Error;
|
|
||||||
}
|
|
||||||
) => void
|
|
||||||
) {
|
) {
|
||||||
//@ts-ignore
|
|
||||||
if (typeof this._purge === "undefined") {
|
if (typeof this._purge === "undefined") {
|
||||||
return callback(
|
return callback(
|
||||||
//@ts-ignore: this ts-ignore might be hiding a `this` bug where we don't have "this" conext.
|
|
||||||
createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.")
|
createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- The adapter task callbacks must retain this PouchDB instance.
|
// eslint-disable-next-line @typescript-eslint/no-this-alias -- The adapter task callbacks must retain this PouchDB instance.
|
||||||
const self = this;
|
const self = this;
|
||||||
const tasks = docs.map(
|
const tasks = docs.map(
|
||||||
(param) => () =>
|
(param) => () =>
|
||||||
new Promise<[PurgeMultiParam, PurgeMultiResult | Error]>((res, rej) => {
|
new Promise<[PurgeMultiParam, unknown]>((res) => {
|
||||||
const [docId, rev$$1] = param;
|
const [docId, rev$$1] = param;
|
||||||
self._getRevisionTree(docId, (error: Error, revs: string[]) => {
|
self._getRevisionTree(docId, (error, revs) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return res([param, error]);
|
return res([param, error]);
|
||||||
}
|
}
|
||||||
if (!revs) {
|
if (!revs) {
|
||||||
return res([param, createError(MISSING_DOC)]);
|
return res([param, createError(MISSING_DOC)]);
|
||||||
}
|
}
|
||||||
let path;
|
let path: string[];
|
||||||
try {
|
try {
|
||||||
path = findPathToLeaf(revs, rev$$1);
|
path = findPathToLeaf(revs, rev$$1);
|
||||||
} catch (error) {
|
} catch (caught: unknown) {
|
||||||
//@ts-ignore
|
const failure = caught instanceof Error && caught.message ? caught.message : caught;
|
||||||
return res([param, error.message || error]);
|
return res([param, failure]);
|
||||||
}
|
}
|
||||||
self._purge(docId, path, (error: Error, result: PurgeMultiResult) => {
|
self._purge(docId, path, (error, result) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return res([param, error]);
|
return res([param, error]);
|
||||||
} else {
|
|
||||||
return res([param, result]);
|
|
||||||
}
|
}
|
||||||
|
return res([param, result]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
(async () => {
|
(async () => {
|
||||||
const ret = await mapAllTasksWithConcurrencyLimit(1, tasks);
|
const ret = await mapAllTasksWithConcurrencyLimit(1, tasks);
|
||||||
const retAll = ret.map((e) => unwrapTaskResult(e)) as [PurgeMultiParam, PurgeMultiResult | Error][];
|
const retAll: Array<[PurgeMultiParam, unknown]> = [];
|
||||||
await appendPurgeSeqs(
|
for (const entry of ret) {
|
||||||
self,
|
const outcome = unwrapTaskResult(entry);
|
||||||
retAll.filter((e) => "ok" in e[1]).map((e) => e[0])
|
if (outcome instanceof Error) {
|
||||||
);
|
throw outcome;
|
||||||
const result = Object.fromEntries(retAll.map((e) => [e[0][0], e[1]]));
|
}
|
||||||
|
retAll.push(outcome);
|
||||||
|
}
|
||||||
|
const successfullyPurged: PurgeMultiParam[] = [];
|
||||||
|
const resultEntries: Array<[string, unknown]> = [];
|
||||||
|
for (const [document, outcome] of retAll) {
|
||||||
|
if (isSuccessfulPurge(outcome)) {
|
||||||
|
successfullyPurged.push(document);
|
||||||
|
}
|
||||||
|
resultEntries.push([document[0], outcome]);
|
||||||
|
}
|
||||||
|
await appendPurgeSeqs(self, successfullyPurged);
|
||||||
|
const result: PurgeMultiResultMap = Object.fromEntries(resultEntries);
|
||||||
return result;
|
return result;
|
||||||
})()
|
})()
|
||||||
//@ts-ignore
|
|
||||||
.then((result) => callback(undefined, result))
|
.then((result) => callback(undefined, result))
|
||||||
.catch((error) => callback(error));
|
.catch((caught: unknown) => {
|
||||||
|
const error = caught instanceof Error ? caught : new Error(String(caught));
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
declare module "pouchdb-merge" {
|
||||||
|
export interface RevisionTreeNode {
|
||||||
|
pos: number;
|
||||||
|
ids: [revision: string, metadata: Record<string, unknown>, branches: RevisionTreeNode["ids"][]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPathToLeaf(revisions: RevisionTreeNode[], targetRevision: string): string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "pouchdb-utils" {
|
||||||
|
export function adapterFun<TThis, TArguments extends unknown[], TResult>(
|
||||||
|
name: string,
|
||||||
|
callback: (this: TThis, ...args: [...TArguments, callback: (error?: Error, result?: TResult) => void]) => void
|
||||||
|
): (this: TThis, ...args: TArguments) => Promise<TResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "pouchdb-errors" {
|
||||||
|
export const MISSING_DOC: unknown;
|
||||||
|
export const UNKNOWN_ERROR: unknown;
|
||||||
|
export function createError(error: unknown, reason?: string): Error;
|
||||||
|
}
|
||||||
@@ -82,6 +82,17 @@ function deserializeFromNodeKV(value: unknown): unknown {
|
|||||||
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deserializeFromNodeKV(v)]));
|
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deserializeFromNodeKV(v)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asKeyString(key: unknown): string {
|
||||||
|
if (typeof key === "string") {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
const serialised = JSON.stringify(key);
|
||||||
|
if (typeof serialised !== "string") {
|
||||||
|
throw new TypeError("The IndexedDB key could not be serialised");
|
||||||
|
}
|
||||||
|
return serialised;
|
||||||
|
}
|
||||||
|
|
||||||
class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
||||||
private filePath: string;
|
private filePath: string;
|
||||||
private data = new Map<string, unknown>();
|
private data = new Map<string, unknown>();
|
||||||
@@ -91,13 +102,6 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
private asKeyString(key: IDBValidKey): string {
|
|
||||||
if (typeof key === "string") {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
return JSON.stringify(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private load() {
|
private load() {
|
||||||
try {
|
try {
|
||||||
const loaded = JSON.parse(nodeFs.readFileSync(this.filePath, "utf-8")) as Record<string, unknown>;
|
const loaded = JSON.parse(nodeFs.readFileSync(this.filePath, "utf-8")) as Record<string, unknown>;
|
||||||
@@ -116,17 +120,17 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(key: IDBValidKey): Promise<T> {
|
async get<T>(key: IDBValidKey): Promise<T> {
|
||||||
return this.data.get(this.asKeyString(key)) as T;
|
return this.data.get(asKeyString(key)) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set<T>(key: IDBValidKey, value: T): Promise<IDBValidKey> {
|
async set<T>(key: IDBValidKey, value: T): Promise<IDBValidKey> {
|
||||||
this.data.set(this.asKeyString(key), value);
|
this.data.set(asKeyString(key), value);
|
||||||
this.flush();
|
this.flush();
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
async del(key: IDBValidKey): Promise<void> {
|
async del(key: IDBValidKey): Promise<void> {
|
||||||
this.data.delete(this.asKeyString(key));
|
this.data.delete(asKeyString(key));
|
||||||
this.flush();
|
this.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,11 +148,12 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
|
|||||||
let filtered = allKeys;
|
let filtered = allKeys;
|
||||||
if (typeof query !== "undefined") {
|
if (typeof query !== "undefined") {
|
||||||
if (this.isIDBKeyRangeLike(query)) {
|
if (this.isIDBKeyRangeLike(query)) {
|
||||||
const lower = query.lower?.toString() ?? "";
|
const lower = query.lower === undefined ? "" : String(query.lower);
|
||||||
const upper = query.upper?.toString() ?? "\uffff";
|
const upper = query.upper === undefined ? "\uffff" : String(query.upper);
|
||||||
filtered = filtered.filter((key) => key >= lower && key <= upper);
|
filtered = filtered.filter((key) => key >= lower && key <= upper);
|
||||||
} else {
|
} else {
|
||||||
const exact = query.toString();
|
const exactValue: unknown = query;
|
||||||
|
const exact = String(exactValue);
|
||||||
filtered = filtered.filter((key) => key === exact);
|
filtered = filtered.filter((key) => key === exact);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,7 +277,15 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
|
|||||||
await getDB().del(`${prefix}${key}`);
|
await getDB().del(`${prefix}${key}`);
|
||||||
},
|
},
|
||||||
keys: async (from: string | undefined, to: string | undefined, count?: number): Promise<string[]> => {
|
keys: async (from: string | undefined, to: string | undefined, count?: number): Promise<string[]> => {
|
||||||
const allKeys = (await getDB().keys(undefined, count)).map((e) => e.toString());
|
const rawKeys: unknown = await getDB().keys(undefined, count);
|
||||||
|
if (!Array.isArray(rawKeys)) {
|
||||||
|
throw new TypeError("The key-value database returned an invalid key list");
|
||||||
|
}
|
||||||
|
const keyList: unknown[] = rawKeys;
|
||||||
|
const allKeys: string[] = [];
|
||||||
|
for (const key of keyList) {
|
||||||
|
allKeys.push(String(key));
|
||||||
|
}
|
||||||
const lower = `${prefix}${from ?? ""}`;
|
const lower = `${prefix}${from ?? ""}`;
|
||||||
const upper = `${prefix}${to ?? "\uffff"}`;
|
const upper = `${prefix}${to ?? "\uffff"}`;
|
||||||
return allKeys
|
return allKeys
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/n
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
|
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
|
||||||
const repoRoot = path.resolve(__dirname, "../../..");
|
const repoRoot = path.resolve(__dirname, "../../..");
|
||||||
const packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
|
||||||
const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
function readVersion(filePath: string): string | undefined {
|
||||||
|
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
|
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return typeof parsed.version === "string" ? parsed.version : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
|
||||||
|
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
const defaultExternal = [
|
const defaultExternal = [
|
||||||
"obsidian",
|
"obsidian",
|
||||||
@@ -113,7 +122,7 @@ export default defineConfig({
|
|||||||
global: "globalThis",
|
global: "globalThis",
|
||||||
nonInteractive: "true",
|
nonInteractive: "true",
|
||||||
// localStorage: "undefined", // Prevent usage of localStorage in the CLI environment
|
// localStorage: "undefined", // Prevent usage of localStorage in the CLI environment
|
||||||
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"),
|
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"),
|
||||||
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"),
|
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -176,7 +176,11 @@ class LiveSyncWebApp {
|
|||||||
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE);
|
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE);
|
||||||
const file = await fileHandle.getFile();
|
const file = await fileHandle.getFile();
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
return JSON.parse(text);
|
const parsed: unknown = JSON.parse(text);
|
||||||
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||||
|
throw new Error("The WebApp settings file does not contain an object");
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
// File doesn't exist yet
|
// File doesn't exist yet
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+223
-148
@@ -5,16 +5,43 @@
|
|||||||
* Obsidian mock, and must not become a general test environment for the plug-in. When the Webapp compatibility boundary
|
* Obsidian mock, and must not become a general test environment for the plug-in. When the Webapp compatibility boundary
|
||||||
* is redesigned, replace this implementation here rather than extending it as a shared Obsidian simulation.
|
* is redesigned, replace this implementation here rather than extending it as a shared Obsidian simulation.
|
||||||
*/
|
*/
|
||||||
export const SettingCache = new Map<any, any>();
|
import type {
|
||||||
//@ts-ignore obsidian global
|
Command,
|
||||||
globalThis.activeDocument = document;
|
DataWriteOptions,
|
||||||
|
ListedFiles,
|
||||||
|
MarkdownFileInfo,
|
||||||
|
PluginManifest,
|
||||||
|
RequestUrlParam,
|
||||||
|
RequestUrlResponse,
|
||||||
|
ValueComponent,
|
||||||
|
} from "obsidian";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DataWriteOptions,
|
||||||
|
ListedFiles,
|
||||||
|
MarkdownFileInfo,
|
||||||
|
PluginManifest,
|
||||||
|
RequestUrlParam,
|
||||||
|
RequestUrlResponse,
|
||||||
|
ValueComponent,
|
||||||
|
};
|
||||||
|
|
||||||
|
type EventCallback = (...args: unknown[]) => unknown;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
activeDocument: Document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingCache = new Map<object, unknown>();
|
||||||
|
window.activeDocument = document;
|
||||||
|
|
||||||
declare const hostPlatform: string | undefined;
|
declare const hostPlatform: string | undefined;
|
||||||
|
|
||||||
globalThis.process = {
|
Reflect.set(window, "process", {
|
||||||
platform: (hostPlatform || "win32") as any,
|
platform: hostPlatform || "win32",
|
||||||
} as any;
|
});
|
||||||
console.warn(`[Obsidian Mock] process.platform is set to ${globalThis.process.platform}`);
|
|
||||||
export class TAbstractFile {
|
export class TAbstractFile {
|
||||||
vault: Vault;
|
vault: Vault;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -55,7 +82,12 @@ export class TFolder extends TAbstractFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventRef {}
|
export class EventRef {
|
||||||
|
constructor(
|
||||||
|
readonly name: string,
|
||||||
|
readonly callback: EventCallback
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
// class StorageMap<T, U> extends Map<T, U> {
|
// class StorageMap<T, U> extends Map<T, U> {
|
||||||
// constructor(saveName?: string) {
|
// constructor(saveName?: string) {
|
||||||
@@ -114,7 +146,7 @@ export class Vault {
|
|||||||
private files: Map<string, TAbstractFile> = new Map();
|
private files: Map<string, TAbstractFile> = new Map();
|
||||||
private contents: Map<string, string | ArrayBuffer> = new Map();
|
private contents: Map<string, string | ArrayBuffer> = new Map();
|
||||||
private root: TFolder;
|
private root: TFolder;
|
||||||
private listeners: Map<string, Set<(...args: any[]) => any>> = new Map();
|
private listeners: Map<string, Set<EventCallback>> = new Map();
|
||||||
|
|
||||||
constructor(vaultName?: string) {
|
constructor(vaultName?: string) {
|
||||||
if (vaultName) {
|
if (vaultName) {
|
||||||
@@ -194,19 +226,16 @@ export class Vault {
|
|||||||
if (this.files.has(path)) throw new Error("File already exists");
|
if (this.files.has(path)) throw new Error("File already exists");
|
||||||
const name = path.split("/").pop() || "";
|
const name = path.split("/").pop() || "";
|
||||||
const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
||||||
let parent = this.getAbstractFileByPath(parentPath);
|
const existingParent = this.getAbstractFileByPath(parentPath);
|
||||||
if (!parent || !(parent instanceof TFolder)) {
|
const parent = existingParent instanceof TFolder ? existingParent : await this.createFolder(parentPath);
|
||||||
parent = await this.createFolder(parentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = new TFile(this, path, name, parent as TFolder);
|
const file = new TFile(this, path, name, parent);
|
||||||
file.stat.size = typeof data === "string" ? new TextEncoder().encode(data).length : data.byteLength;
|
file.stat.size = typeof data === "string" ? new TextEncoder().encode(data).length : data.byteLength;
|
||||||
file.stat.ctime = options?.ctime ?? Date.now();
|
file.stat.ctime = options?.ctime ?? Date.now();
|
||||||
file.stat.mtime = options?.mtime ?? Date.now();
|
file.stat.mtime = options?.mtime ?? Date.now();
|
||||||
this.files.set(path, file);
|
this.files.set(path, file);
|
||||||
this.contents.set(path, data);
|
this.contents.set(path, data);
|
||||||
(parent as TFolder).children.push(file);
|
parent.children.push(file);
|
||||||
// console.dir(this.files);
|
|
||||||
|
|
||||||
this.trigger("create", file);
|
this.trigger("create", file);
|
||||||
return file;
|
return file;
|
||||||
@@ -224,7 +253,6 @@ export class Vault {
|
|||||||
file.stat.mtime = options?.mtime ?? Date.now();
|
file.stat.mtime = options?.mtime ?? Date.now();
|
||||||
file.stat.ctime = options?.ctime ?? file.stat.ctime ?? Date.now();
|
file.stat.ctime = options?.ctime ?? file.stat.ctime ?? Date.now();
|
||||||
file.stat.size = typeof data === "string" ? data.length : data.byteLength;
|
file.stat.size = typeof data === "string" ? data.length : data.byteLength;
|
||||||
console.warn(`[Obsidian Mock ${this.vaultName}] Modified file at path: '${file.path}'`);
|
|
||||||
this.files.set(file.path, file);
|
this.files.set(file.path, file);
|
||||||
this.trigger("modify", file);
|
this.trigger("modify", file);
|
||||||
}
|
}
|
||||||
@@ -252,6 +280,14 @@ export class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(file: TAbstractFile, force?: boolean): Promise<void> {
|
async delete(file: TAbstractFile, force?: boolean): Promise<void> {
|
||||||
|
return this.removeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async trash(file: TAbstractFile, system: boolean): Promise<void> {
|
||||||
|
return this.removeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeFile(file: TAbstractFile): Promise<void> {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
this.files.delete(file.path);
|
this.files.delete(file.path);
|
||||||
this.contents.delete(file.path);
|
this.contents.delete(file.path);
|
||||||
@@ -261,30 +297,27 @@ export class Vault {
|
|||||||
this.trigger("delete", file);
|
this.trigger("delete", file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async trash(file: TAbstractFile, system: boolean): Promise<void> {
|
async removeFromAdapter(file: TAbstractFile): Promise<void> {
|
||||||
await Promise.resolve();
|
return this.removeFile(file);
|
||||||
return this.delete(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on(name: string, callback: (...args: any[]) => any, ctx?: any): EventRef {
|
on(name: string, callback: EventCallback, ctx?: object): EventRef {
|
||||||
if (!this.listeners.has(name)) {
|
const listeners = this.listeners.get(name) ?? new Set<EventCallback>();
|
||||||
this.listeners.set(name, new Set());
|
this.listeners.set(name, listeners);
|
||||||
}
|
const boundCallback = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback;
|
||||||
const boundCallback = ctx ? callback.bind(ctx) : callback;
|
listeners.add(boundCallback);
|
||||||
this.listeners.get(name)!.add(boundCallback);
|
return new EventRef(name, boundCallback);
|
||||||
return { name, callback: boundCallback } as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
off(name: string, callback: any) {
|
off(name: string, callback: EventCallback) {
|
||||||
this.listeners.get(name)?.delete(callback);
|
this.listeners.get(name)?.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
offref(ref: EventRef) {
|
offref(ref: EventRef) {
|
||||||
const { name, callback } = ref as any;
|
this.off(ref.name, ref.callback);
|
||||||
this.off(name, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger(name: string, ...args: any[]) {
|
trigger(name: string, ...args: unknown[]) {
|
||||||
this.listeners.get(name)?.forEach((cb) => cb(...args));
|
this.listeners.get(name)?.forEach((cb) => cb(...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,28 +399,29 @@ export class DataAdapter {
|
|||||||
}
|
}
|
||||||
async remove(path: string): Promise<void> {
|
async remove(path: string): Promise<void> {
|
||||||
const file = this.vault.getAbstractFileByPath(path);
|
const file = this.vault.getAbstractFileByPath(path);
|
||||||
if (file) await this.vault.delete(file);
|
if (file) await this.vault.removeFromAdapter(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Events {
|
class Events {
|
||||||
_eventEmitter = new EventTarget();
|
_eventEmitter = new EventTarget();
|
||||||
_events = new Map<any, any>();
|
_events = new Map<EventCallback, EventListener>();
|
||||||
_eventTarget(cb: any) {
|
_eventTarget(callback: EventCallback): EventListener {
|
||||||
const x = this._events.get(cb);
|
const registered = this._events.get(callback);
|
||||||
if (x) {
|
if (registered) {
|
||||||
return x;
|
return registered;
|
||||||
}
|
}
|
||||||
const callback = (evt: any) => {
|
const eventListener = (event: Event) => {
|
||||||
x(evt?.detail ?? undefined);
|
callback(event instanceof CustomEvent ? event.detail : undefined);
|
||||||
};
|
};
|
||||||
this._events.set(cb, callback);
|
this._events.set(callback, eventListener);
|
||||||
return callback;
|
return eventListener;
|
||||||
}
|
}
|
||||||
on(name: string, cb: any, ctx?: any) {
|
on(name: string, callback: EventCallback, ctx?: object) {
|
||||||
this._eventEmitter.addEventListener(name, this._eventTarget(cb));
|
const registered = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback;
|
||||||
|
this._eventEmitter.addEventListener(name, this._eventTarget(registered));
|
||||||
}
|
}
|
||||||
trigger(name: string, args: any) {
|
trigger(name: string, args: unknown) {
|
||||||
const evt = new CustomEvent(name, {
|
const evt = new CustomEvent(name, {
|
||||||
detail: args,
|
detail: args,
|
||||||
});
|
});
|
||||||
@@ -403,13 +437,13 @@ class Workspace extends Events {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayoutReady(cb: any) {
|
onLayoutReady(callback: () => void) {
|
||||||
// cb();
|
// cb();
|
||||||
// console.log("[Obsidian Mock] Workspace onLayoutReady registered");
|
// console.log("[Obsidian Mock] Workspace onLayoutReady registered");
|
||||||
// this._eventEmitter.addEventListener("layout-ready", () => {
|
// this._eventEmitter.addEventListener("layout-ready", () => {
|
||||||
// console.log("[Obsidian Mock] Workspace layout-ready event triggered");
|
// console.log("[Obsidian Mock] Workspace layout-ready event triggered");
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
cb();
|
callback();
|
||||||
}, 200);
|
}, 200);
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
@@ -434,33 +468,36 @@ export class App {
|
|||||||
}
|
}
|
||||||
vault: Vault;
|
vault: Vault;
|
||||||
workspace: Workspace = new Workspace();
|
workspace: Workspace = new Workspace();
|
||||||
metadataCache: any = {
|
metadataCache = {
|
||||||
on: (name: string, cb: any, ctx?: any) => {},
|
on: (name: string, callback: EventCallback, ctx?: object): EventRef => {
|
||||||
|
const registered = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback;
|
||||||
|
return new EventRef(name, registered);
|
||||||
|
},
|
||||||
getFileCache: (): null => null,
|
getFileCache: (): null => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Plugin {
|
export class Plugin {
|
||||||
app: App;
|
app: App;
|
||||||
manifest: any;
|
manifest: PluginManifest;
|
||||||
settings: any;
|
settings: unknown;
|
||||||
commands: Map<string, any> = new Map();
|
commands: Map<string, Command> = new Map();
|
||||||
constructor(app: App, manifest: any) {
|
constructor(app: App, manifest: PluginManifest) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
}
|
}
|
||||||
async loadData(): Promise<any> {
|
async loadData(): Promise<unknown> {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
return SettingCache.get(this.app) ?? {};
|
return SettingCache.get(this.app) ?? {};
|
||||||
}
|
}
|
||||||
async saveData(data: any): Promise<void> {
|
async saveData(data: unknown): Promise<void> {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
SettingCache.set(this.app, data);
|
SettingCache.set(this.app, data);
|
||||||
}
|
}
|
||||||
onload() {}
|
onload() {}
|
||||||
onunload() {}
|
onunload() {}
|
||||||
addSettingTab(tab: any) {}
|
addSettingTab(tab: PluginSettingTab) {}
|
||||||
addCommand(command: any) {
|
addCommand(command: Command) {
|
||||||
this.commands.set(command.id, command);
|
this.commands.set(command.id, command);
|
||||||
}
|
}
|
||||||
addStatusBarItem() {
|
addStatusBarItem() {
|
||||||
@@ -478,10 +515,14 @@ export class Plugin {
|
|||||||
};
|
};
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
registerView(type: string, creator: any) {}
|
registerView(type: string, creator: () => ItemView) {}
|
||||||
registerObsidianProtocolHandler(handler: any) {}
|
registerObsidianProtocolHandler(handler: (params: Record<string, string>) => unknown) {}
|
||||||
registerEvent(handler: any) {}
|
registerEvent(handler: EventRef) {}
|
||||||
registerDomEvent(target: any, eventName: string, handler: any) {}
|
registerDomEvent<K extends keyof HTMLElementEventMap>(
|
||||||
|
target: HTMLElement,
|
||||||
|
eventName: K,
|
||||||
|
handler: (event: HTMLElementEventMap[K]) => unknown
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Notice {
|
export class Notice {
|
||||||
@@ -489,11 +530,8 @@ export class Notice {
|
|||||||
private static _counter = 0;
|
private static _counter = 0;
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
this._key = Notice._counter++;
|
this._key = Notice._counter++;
|
||||||
console.log(`Notice [${this._key}]:`, message);
|
|
||||||
}
|
|
||||||
setMessage(message: string) {
|
|
||||||
console.log(`Notice [${this._key}]:`, message);
|
|
||||||
}
|
}
|
||||||
|
setMessage(message: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Modal {
|
export class Modal {
|
||||||
@@ -558,7 +596,7 @@ export const Platform = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class Menu {
|
export class Menu {
|
||||||
addItem(cb: (item: MenuItem) => any) {
|
addItem(cb: (item: MenuItem) => unknown) {
|
||||||
cb(new MenuItem());
|
cb(new MenuItem());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -571,7 +609,7 @@ export class MenuItem {
|
|||||||
setIcon(icon: string) {
|
setIcon(icon: string) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
onClick(cb: (evt: MouseEvent) => any) {
|
onClick(cb: (evt: MouseEvent) => unknown) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,7 +622,7 @@ export class Component {
|
|||||||
|
|
||||||
export class ButtonComponent extends Component {
|
export class ButtonComponent extends Component {
|
||||||
buttonEl: HTMLButtonElement = document.createElement("button");
|
buttonEl: HTMLButtonElement = document.createElement("button");
|
||||||
private clickHandler: ((evt: MouseEvent) => any) | null = null;
|
private clickHandler: ((evt: MouseEvent) => unknown) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -602,7 +640,7 @@ export class ButtonComponent extends Component {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(cb: (evt: MouseEvent) => any) {
|
onClick(cb: (evt: MouseEvent) => unknown) {
|
||||||
this.clickHandler = cb;
|
this.clickHandler = cb;
|
||||||
this.buttonEl.removeEventListener("click", this.clickHandler);
|
this.buttonEl.removeEventListener("click", this.clickHandler);
|
||||||
this.buttonEl.addEventListener("click", (evt) => cb(evt as MouseEvent));
|
this.buttonEl.addEventListener("click", (evt) => cb(evt as MouseEvent));
|
||||||
@@ -627,7 +665,7 @@ export class ButtonComponent extends Component {
|
|||||||
|
|
||||||
export class TextComponent extends Component {
|
export class TextComponent extends Component {
|
||||||
inputEl: HTMLInputElement = document.createElement("input");
|
inputEl: HTMLInputElement = document.createElement("input");
|
||||||
private changeHandler: ((value: string) => any) | null = null;
|
private changeHandler: ((value: string) => unknown) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -635,7 +673,7 @@ export class TextComponent extends Component {
|
|||||||
this.inputEl.type = "text";
|
this.inputEl.type = "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(cb: (value: string) => any) {
|
onChange(cb: (value: string) => unknown) {
|
||||||
this.changeHandler = cb;
|
this.changeHandler = cb;
|
||||||
this.inputEl.removeEventListener("change", this.handleChange);
|
this.inputEl.removeEventListener("change", this.handleChange);
|
||||||
this.inputEl.addEventListener("change", this.handleChange);
|
this.inputEl.addEventListener("change", this.handleChange);
|
||||||
@@ -671,7 +709,7 @@ export class TextComponent extends Component {
|
|||||||
|
|
||||||
export class ToggleComponent extends Component {
|
export class ToggleComponent extends Component {
|
||||||
inputEl: HTMLInputElement = document.createElement("input");
|
inputEl: HTMLInputElement = document.createElement("input");
|
||||||
private changeHandler: ((value: boolean) => any) | null = null;
|
private changeHandler: ((value: boolean) => unknown) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -679,7 +717,7 @@ export class ToggleComponent extends Component {
|
|||||||
this.inputEl.type = "checkbox";
|
this.inputEl.type = "checkbox";
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(cb: (value: boolean) => any) {
|
onChange(cb: (value: boolean) => unknown) {
|
||||||
this.changeHandler = cb;
|
this.changeHandler = cb;
|
||||||
this.inputEl.addEventListener("change", (evt) => {
|
this.inputEl.addEventListener("change", (evt) => {
|
||||||
const target = evt.target as HTMLInputElement;
|
const target = evt.target as HTMLInputElement;
|
||||||
@@ -701,7 +739,7 @@ export class ToggleComponent extends Component {
|
|||||||
|
|
||||||
export class DropdownComponent extends Component {
|
export class DropdownComponent extends Component {
|
||||||
selectEl: HTMLSelectElement = document.createElement("select");
|
selectEl: HTMLSelectElement = document.createElement("select");
|
||||||
private changeHandler: ((value: string) => any) | null = null;
|
private changeHandler: ((value: string) => unknown) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -723,7 +761,7 @@ export class DropdownComponent extends Component {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(cb: (value: string) => any) {
|
onChange(cb: (value: string) => unknown) {
|
||||||
this.changeHandler = cb;
|
this.changeHandler = cb;
|
||||||
this.selectEl.addEventListener("change", (evt) => {
|
this.selectEl.addEventListener("change", (evt) => {
|
||||||
const target = evt.target as HTMLSelectElement;
|
const target = evt.target as HTMLSelectElement;
|
||||||
@@ -745,7 +783,7 @@ export class DropdownComponent extends Component {
|
|||||||
|
|
||||||
export class SliderComponent extends Component {
|
export class SliderComponent extends Component {
|
||||||
inputEl: HTMLInputElement = document.createElement("input");
|
inputEl: HTMLInputElement = document.createElement("input");
|
||||||
private changeHandler: ((value: number) => any) | null = null;
|
private changeHandler: ((value: number) => unknown) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -753,7 +791,7 @@ export class SliderComponent extends Component {
|
|||||||
this.inputEl.type = "range";
|
this.inputEl.type = "range";
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(cb: (value: number) => any) {
|
onChange(cb: (value: number) => unknown) {
|
||||||
this.changeHandler = cb;
|
this.changeHandler = cb;
|
||||||
this.inputEl.addEventListener("change", (evt) => {
|
this.inputEl.addEventListener("change", (evt) => {
|
||||||
const target = evt.target as HTMLInputElement;
|
const target = evt.target as HTMLInputElement;
|
||||||
@@ -816,72 +854,115 @@ export class Setting {
|
|||||||
this.controlEl.addClass(c);
|
this.controlEl.addClass(c);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
addText(cb: (text: TextComponent) => any) {
|
addText(cb: (text: TextComponent) => unknown) {
|
||||||
const component = new TextComponent();
|
const component = new TextComponent();
|
||||||
this.controlEl.appendChild(component.inputEl);
|
this.controlEl.appendChild(component.inputEl);
|
||||||
cb(component);
|
cb(component);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
addToggle(cb: (toggle: ToggleComponent) => any) {
|
addToggle(cb: (toggle: ToggleComponent) => unknown) {
|
||||||
const component = new ToggleComponent();
|
const component = new ToggleComponent();
|
||||||
cb(component);
|
cb(component);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
addButton(cb: (btn: ButtonComponent) => any) {
|
addButton(cb: (btn: ButtonComponent) => unknown) {
|
||||||
const btn = new ButtonComponent();
|
const btn = new ButtonComponent();
|
||||||
this.controlEl.appendChild(btn.buttonEl);
|
this.controlEl.appendChild(btn.buttonEl);
|
||||||
cb(btn);
|
cb(btn);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
addDropdown(cb: (dropdown: DropdownComponent) => any) {
|
addDropdown(cb: (dropdown: DropdownComponent) => unknown) {
|
||||||
const component = new DropdownComponent();
|
const component = new DropdownComponent();
|
||||||
cb(component);
|
cb(component);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
addSlider(cb: (slider: SliderComponent) => any) {
|
addSlider(cb: (slider: SliderComponent) => unknown) {
|
||||||
const component = new SliderComponent();
|
const component = new SliderComponent();
|
||||||
cb(component);
|
cb(component);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTMLElement extensions
|
function applyDomElementInfo(element: HTMLElement, info?: DomElementInfo | string): void {
|
||||||
|
if (typeof info === "string") {
|
||||||
|
element.textContent = info;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!info) return;
|
||||||
|
if (info.cls) {
|
||||||
|
const classes = Array.isArray(info.cls) ? info.cls : info.cls.split(" ");
|
||||||
|
element.classList.add(...classes.filter((className) => className !== ""));
|
||||||
|
}
|
||||||
|
if (info.text !== undefined) {
|
||||||
|
element.replaceChildren(info.text);
|
||||||
|
}
|
||||||
|
if (info.attr) {
|
||||||
|
for (const [name, value] of Object.entries(info.attr)) {
|
||||||
|
if (value === null) element.removeAttribute(name);
|
||||||
|
else element.setAttribute(name, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.title !== undefined) element.title = info.title;
|
||||||
|
if (info.value !== undefined && "value" in element) element.value = info.value;
|
||||||
|
if (info.type !== undefined && "type" in element) element.type = info.type;
|
||||||
|
if (info.placeholder !== undefined && "placeholder" in element) element.placeholder = info.placeholder;
|
||||||
|
if (info.href !== undefined) element.setAttribute("href", info.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLElement extensions used by the Webapp compatibility implementation.
|
||||||
if (typeof HTMLElement !== "undefined") {
|
if (typeof HTMLElement !== "undefined") {
|
||||||
const proto = HTMLElement.prototype as any;
|
const proto = HTMLElement.prototype;
|
||||||
proto.createDiv = function (o?: any) {
|
proto.createDiv = function (
|
||||||
const div = document.createElement("div");
|
this: HTMLElement,
|
||||||
if (o?.cls) div.addClass(o.cls);
|
info?: DomElementInfo | string,
|
||||||
if (o?.text) div.setText(o.text);
|
callback?: (element: HTMLDivElement) => void
|
||||||
this.appendChild(div);
|
): HTMLDivElement {
|
||||||
return div;
|
const element = document.createElement("div");
|
||||||
|
applyDomElementInfo(element, info);
|
||||||
|
this.appendChild(element);
|
||||||
|
callback?.(element);
|
||||||
|
return element;
|
||||||
};
|
};
|
||||||
proto.createEl = function (tag: string, o?: any) {
|
proto.createEl = function <K extends keyof HTMLElementTagNameMap>(
|
||||||
const el = document.createElement(tag);
|
this: HTMLElement,
|
||||||
if (o?.cls) el.addClass(o.cls);
|
tag: K,
|
||||||
if (o?.text) el.setText(o.text);
|
info?: DomElementInfo | string,
|
||||||
this.appendChild(el);
|
callback?: (element: HTMLElementTagNameMap[K]) => void
|
||||||
return el;
|
): HTMLElementTagNameMap[K] {
|
||||||
|
const element = document.createElement(tag);
|
||||||
|
applyDomElementInfo(element, info);
|
||||||
|
this.appendChild(element);
|
||||||
|
callback?.(element);
|
||||||
|
return element;
|
||||||
};
|
};
|
||||||
proto.createSpan = function (o?: any) {
|
proto.createSpan = function (
|
||||||
return this.createEl("span", o);
|
this: HTMLElement,
|
||||||
|
info?: DomElementInfo | string,
|
||||||
|
callback?: (element: HTMLSpanElement) => void
|
||||||
|
): HTMLSpanElement {
|
||||||
|
const element = document.createElement("span");
|
||||||
|
applyDomElementInfo(element, info);
|
||||||
|
this.appendChild(element);
|
||||||
|
callback?.(element);
|
||||||
|
return element;
|
||||||
};
|
};
|
||||||
proto.empty = function () {
|
proto.empty = function (this: HTMLElement): void {
|
||||||
this.innerHTML = "";
|
this.replaceChildren();
|
||||||
};
|
};
|
||||||
proto.setText = function (t: string) {
|
proto.setText = function (this: HTMLElement, text: string): void {
|
||||||
this.textContent = t;
|
this.textContent = text;
|
||||||
};
|
};
|
||||||
proto.addClass = function (c: string) {
|
proto.addClass = function (this: HTMLElement, className: string): void {
|
||||||
this.classList.add(c);
|
this.classList.add(className);
|
||||||
};
|
};
|
||||||
proto.removeClass = function (c: string) {
|
proto.removeClass = function (this: HTMLElement, className: string): void {
|
||||||
this.classList.remove(c);
|
this.classList.remove(className);
|
||||||
};
|
};
|
||||||
proto.toggleClass = function (c: string, b: boolean) {
|
proto.toggleClass = function (this: HTMLElement, className: string, value: boolean): void {
|
||||||
this.classList.toggle(c, b);
|
this.classList.toggle(className, value);
|
||||||
};
|
};
|
||||||
proto.hasClass = function (c: string) {
|
proto.hasClass = function (this: HTMLElement, className: string): boolean {
|
||||||
return this.classList.contains(c);
|
return this.classList.contains(className);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,10 +977,19 @@ export class FuzzySuggestModal<T> {
|
|||||||
throw new Error("Not implemented.");
|
throw new Error("Not implemented.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseHtmlFragment(html: string): DocumentFragment {
|
||||||
|
const parsed = new DOMParser().parseFromString(html, "text/html");
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (const child of [...parsed.body.childNodes]) {
|
||||||
|
fragment.appendChild(document.importNode(child, true));
|
||||||
|
}
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
export class MarkdownRenderer {
|
export class MarkdownRenderer {
|
||||||
static render(app: App, md: string, el: HTMLElement, path: string, component: Component) {
|
static render(app: App, md: string, el: HTMLElement, path: string, component: Component) {
|
||||||
// eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's trusted Markdown renderer boundary.
|
el.replaceChildren(parseHtmlFragment(md));
|
||||||
el.innerHTML = md;
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -910,26 +1000,23 @@ export class WorkspaceLeaf {}
|
|||||||
|
|
||||||
export function sanitizeHTMLToDom(html: string) {
|
export function sanitizeHTMLToDom(html: string) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
// eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's sanitised-HTML API contract.
|
div.appendChild(parseHtmlFragment(html));
|
||||||
div.innerHTML = html;
|
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addIcon() {}
|
export function addIcon() {}
|
||||||
export const debounce = (fn: any) => fn;
|
export function debounce<Arguments extends unknown[], Result>(
|
||||||
export async function request(options: any) {
|
fn: (...args: Arguments) => Result
|
||||||
|
): (...args: Arguments) => Result {
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
export async function request(options: RequestUrlParam | string): Promise<string> {
|
||||||
const result = await requestUrl(options);
|
const result = await requestUrl(options);
|
||||||
return result.text;
|
return result.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestUrl({
|
export async function requestUrl(options: RequestUrlParam | string): Promise<RequestUrlResponse> {
|
||||||
body,
|
const { body, headers, method, url, contentType } = typeof options === "string" ? { url: options } : options;
|
||||||
headers,
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
contentType,
|
|
||||||
}: RequestUrlParam): Promise<RequestUrlResponse> {
|
|
||||||
// console.log("[requestUrl] Mock called:", { method, url, contentType });
|
|
||||||
const reqHeadersObj: Record<string, string> = {};
|
const reqHeadersObj: Record<string, string> = {};
|
||||||
for (const key of Object.keys(headers || {})) {
|
for (const key of Object.keys(headers || {})) {
|
||||||
reqHeadersObj[key.toLowerCase()] = headers[key];
|
reqHeadersObj[key.toLowerCase()] = headers[key];
|
||||||
@@ -940,21 +1027,21 @@ export async function requestUrl({
|
|||||||
reqHeadersObj["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
reqHeadersObj["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
||||||
reqHeadersObj["Pragma"] = "no-cache";
|
reqHeadersObj["Pragma"] = "no-cache";
|
||||||
reqHeadersObj["Expires"] = "0";
|
reqHeadersObj["Expires"] = "0";
|
||||||
const result = await fetch(url, {
|
const result = await window.fetch(url, {
|
||||||
method: method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
...reqHeadersObj,
|
...reqHeadersObj,
|
||||||
},
|
},
|
||||||
|
|
||||||
body: body,
|
body,
|
||||||
});
|
});
|
||||||
const headersObj: Record<string, string> = {};
|
const headersObj: Record<string, string> = {};
|
||||||
result.headers.forEach((value, key) => {
|
result.headers.forEach((value, key) => {
|
||||||
headersObj[key] = value;
|
headersObj[key] = value;
|
||||||
});
|
});
|
||||||
let json = undefined;
|
let json: unknown;
|
||||||
let text = undefined;
|
let text = "";
|
||||||
let arrayBuffer = undefined;
|
let arrayBuffer = new ArrayBuffer(0);
|
||||||
try {
|
try {
|
||||||
const isJson = result.headers.get("content-type")?.includes("application/json");
|
const isJson = result.headers.get("content-type")?.includes("application/json");
|
||||||
arrayBuffer = await result.arrayBuffer();
|
arrayBuffer = await result.arrayBuffer();
|
||||||
@@ -965,9 +1052,8 @@ export async function requestUrl({
|
|||||||
if (isJson) {
|
if (isJson) {
|
||||||
json = await JSON.parse(text || "{}");
|
json = await JSON.parse(text || "{}");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.warn("Failed to parse response:", e);
|
json = undefined;
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: result.status,
|
status: result.status,
|
||||||
@@ -977,11 +1063,12 @@ export async function requestUrl({
|
|||||||
arrayBuffer: arrayBuffer,
|
arrayBuffer: arrayBuffer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function stringifyYaml(obj: any) {
|
export function stringifyYaml(obj: unknown): string {
|
||||||
return JSON.stringify(obj);
|
return JSON.stringify(obj);
|
||||||
}
|
}
|
||||||
export function parseYaml(s: string) {
|
export function parseYaml(s: string): unknown {
|
||||||
return JSON.parse(s);
|
const parsed: unknown = JSON.parse(s);
|
||||||
|
return parsed;
|
||||||
}
|
}
|
||||||
export function getLanguage() {
|
export function getLanguage() {
|
||||||
return "en";
|
return "en";
|
||||||
@@ -997,15 +1084,3 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|||||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||||
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer;
|
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataWriteOptions = any;
|
|
||||||
export type PluginManifest = any;
|
|
||||||
export type RequestUrlParam = any;
|
|
||||||
export type RequestUrlResponse = any;
|
|
||||||
export type MarkdownFileInfo = any;
|
|
||||||
export type ListedFiles = {
|
|
||||||
files: string[];
|
|
||||||
folders: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ValueComponent = any;
|
|
||||||
|
|||||||
@@ -15,6 +15,18 @@ export type VaultHistoryItem = {
|
|||||||
|
|
||||||
type VaultHistoryValue = VaultHistoryItem;
|
type VaultHistoryValue = VaultHistoryItem;
|
||||||
|
|
||||||
|
function isVaultHistoryValue(value: unknown): value is VaultHistoryValue {
|
||||||
|
if (typeof value !== "object" || value === null) return false;
|
||||||
|
const candidate = value as Partial<VaultHistoryValue>;
|
||||||
|
return (
|
||||||
|
typeof candidate.id === "string" &&
|
||||||
|
typeof candidate.name === "string" &&
|
||||||
|
typeof candidate.handle === "object" &&
|
||||||
|
candidate.handle !== null &&
|
||||||
|
typeof candidate.lastUsedAt === "number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function makeVaultKey(id: string): string {
|
function makeVaultKey(id: string): string {
|
||||||
return `${VAULT_KEY_PREFIX}${id}`;
|
return `${VAULT_KEY_PREFIX}${id}`;
|
||||||
}
|
}
|
||||||
@@ -87,7 +99,7 @@ export class VaultHistoryStore {
|
|||||||
|
|
||||||
async getLastUsedVaultId(): Promise<string | null> {
|
async getLastUsedVaultId(): Promise<string | null> {
|
||||||
return this.withStore("readonly", async (store) => {
|
return this.withStore("readonly", async (store) => {
|
||||||
const value = await this.requestAsPromise(store.get(LAST_USED_KEY));
|
const value: unknown = await this.requestAsPromise(store.get(LAST_USED_KEY));
|
||||||
return typeof value === "string" ? value : null;
|
return typeof value === "string" ? value : null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,20 +107,21 @@ export class VaultHistoryStore {
|
|||||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||||
return this.withStore("readonly", async (store) => {
|
return this.withStore("readonly", async (store) => {
|
||||||
const keys = await this.requestAsPromise(store.getAllKeys());
|
const keys = await this.requestAsPromise(store.getAllKeys());
|
||||||
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
const values = await this.requestAsPromise<unknown[]>(store.getAll() as IDBRequest<unknown[]>);
|
||||||
const items: VaultHistoryItem[] = [];
|
const items: VaultHistoryItem[] = [];
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = String(keys[i]);
|
const key = keys[i];
|
||||||
|
if (typeof key !== "string") continue;
|
||||||
const id = parseVaultId(key);
|
const id = parseVaultId(key);
|
||||||
const value = values[i] as Partial<VaultHistoryValue> | undefined;
|
const value = values[i];
|
||||||
if (!id || !value || !value.handle || !value.name) {
|
if (!id || !isVaultHistoryValue(value)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
id,
|
id,
|
||||||
name: String(value.name),
|
name: value.name,
|
||||||
handle: value.handle,
|
handle: value.handle,
|
||||||
lastUsedAt: Number(value.lastUsedAt || 0),
|
lastUsedAt: value.lastUsedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
items.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
items.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ import istanbul from "vite-plugin-istanbul";
|
|||||||
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
|
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const repoRoot = path.resolve(__dirname, "../../..");
|
const repoRoot = path.resolve(__dirname, "../../..");
|
||||||
const packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
|
||||||
const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
function readVersion(filePath: string): string | undefined {
|
||||||
|
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||||
|
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return typeof parsed.version === "string" ? parsed.version : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
|
||||||
|
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
|
||||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -57,8 +66,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"),
|
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"),
|
||||||
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"),
|
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"),
|
||||||
global: "globalThis",
|
global: "globalThis",
|
||||||
hostPlatform: JSON.stringify(process.platform || "linux"),
|
hostPlatform: JSON.stringify(process.platform || "linux"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export const _OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueData
|
|||||||
db = await _openDB();
|
db = await _openDB();
|
||||||
databaseCache[dbKey] = db;
|
databaseCache[dbKey] = db;
|
||||||
}
|
}
|
||||||
return await db.get(storeKey, key);
|
const value: unknown = await db.get(storeKey, key);
|
||||||
|
return value as T;
|
||||||
},
|
},
|
||||||
async set<T>(key: IDBValidKey, value: T) {
|
async set<T>(key: IDBValidKey, value: T) {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
type CouchDBConfigurationSection = Record<string, string | undefined>;
|
||||||
|
|
||||||
|
export interface CouchDBConfiguration {
|
||||||
|
admins: CouchDBConfigurationSection;
|
||||||
|
chttpd: CouchDBConfigurationSection;
|
||||||
|
chttpd_auth: CouchDBConfigurationSection;
|
||||||
|
couchdb: CouchDBConfigurationSection;
|
||||||
|
cors: CouchDBConfigurationSection;
|
||||||
|
httpd: CouchDBConfigurationSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseSection(value: unknown): CouchDBConfigurationSection {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: Array<[string, string]> = [];
|
||||||
|
for (const [key, entry] of Object.entries(value)) {
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
entries.push([key, entry]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normaliseCouchDBConfiguration(value: unknown): CouchDBConfiguration {
|
||||||
|
const source = isRecord(value) ? value : {};
|
||||||
|
return {
|
||||||
|
admins: normaliseSection(source.admins),
|
||||||
|
chttpd: normaliseSection(source.chttpd),
|
||||||
|
chttpd_auth: normaliseSection(source.chttpd_auth),
|
||||||
|
couchdb: normaliseSection(source.couchdb),
|
||||||
|
cors: normaliseSection(source.cors),
|
||||||
|
httpd: normaliseSection(source.httpd),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { App, PluginManifest } from "@/deps.ts";
|
||||||
|
|
||||||
|
export interface ObsidianCommunityPluginManager {
|
||||||
|
enabledPlugins: ReadonlySet<string>;
|
||||||
|
manifests: PluginManifest[];
|
||||||
|
loadPlugin(pluginId: string): Promise<void>;
|
||||||
|
unloadPlugin(pluginId: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluginManifest(value: unknown): value is PluginManifest {
|
||||||
|
return (
|
||||||
|
isRecord(value) &&
|
||||||
|
typeof value.id === "string" &&
|
||||||
|
typeof value.name === "string" &&
|
||||||
|
(value.dir === undefined || typeof value.dir === "string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringSet(value: unknown): value is ReadonlySet<string> {
|
||||||
|
if (!(value instanceof Set)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const entries = value as ReadonlySet<unknown>;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (typeof entry !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invokeLifecycleMethod(
|
||||||
|
manager: Record<string, unknown>,
|
||||||
|
methodName: "loadPlugin" | "unloadPlugin",
|
||||||
|
pluginId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const method = manager[methodName];
|
||||||
|
if (typeof method !== "function") {
|
||||||
|
throw new TypeError(`Obsidian does not expose ${methodName}`);
|
||||||
|
}
|
||||||
|
const result: unknown = Reflect.apply(method, manager, [pluginId]);
|
||||||
|
await result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getObsidianCommunityPluginManager(app: App): ObsidianCommunityPluginManager {
|
||||||
|
const managerValue: unknown = Reflect.get(app, "plugins");
|
||||||
|
if (!isRecord(managerValue) || !isRecord(managerValue.manifests) || !isStringSet(managerValue.enabledPlugins)) {
|
||||||
|
throw new TypeError("Obsidian does not expose the community plug-in manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifests = Object.values(managerValue.manifests).filter(isPluginManifest);
|
||||||
|
return {
|
||||||
|
enabledPlugins: managerValue.enabledPlugins,
|
||||||
|
manifests,
|
||||||
|
loadPlugin: async (pluginId) => await invokeLifecycleMethod(managerValue, "loadPlugin", pluginId),
|
||||||
|
unloadPlugin: async (pluginId) => await invokeLifecycleMethod(managerValue, "unloadPlugin", pluginId),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { App } from "@/deps.ts";
|
||||||
|
|
||||||
|
function getSettingsManager(app: App): Record<string, unknown> {
|
||||||
|
const manager: unknown = Reflect.get(app, "setting");
|
||||||
|
if (typeof manager !== "object" || manager === null) {
|
||||||
|
throw new TypeError("Obsidian does not expose the settings manager");
|
||||||
|
}
|
||||||
|
return manager as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeSettingsMethod(app: App, methodName: string, args: unknown[] = []): void {
|
||||||
|
const manager = getSettingsManager(app);
|
||||||
|
const method = manager[methodName];
|
||||||
|
if (typeof method !== "function") {
|
||||||
|
throw new TypeError(`Obsidian does not expose settings.${methodName}`);
|
||||||
|
}
|
||||||
|
Reflect.apply(method, manager, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openObsidianSettings(app: App, tabId: string): void {
|
||||||
|
invokeSettingsMethod(app, "open");
|
||||||
|
invokeSettingsMethod(app, "openTabById", [tabId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeObsidianSettings(app: App): void {
|
||||||
|
invokeSettingsMethod(app, "close");
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import type PouchDB from "pouchdb-core";
|
||||||
import {
|
import {
|
||||||
Notice,
|
Notice,
|
||||||
type PluginManifest,
|
type PluginManifest,
|
||||||
@@ -76,18 +77,12 @@ import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
|||||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||||
import type { LiveSyncCore } from "@/main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||||
|
import type { OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts";
|
||||||
|
import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlugins.ts";
|
||||||
|
|
||||||
const d = "\u200b";
|
const d = "\u200b";
|
||||||
const d2 = "\n";
|
const d2 = "\n";
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface OPTIONAL_SYNC_FEATURES {
|
|
||||||
DISABLE: "DISABLE";
|
|
||||||
CUSTOMIZE: "CUSTOMIZE";
|
|
||||||
DISABLE_CUSTOM: "DISABLE_CUSTOM";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function serialize(data: PluginDataEx): string {
|
function serialize(data: PluginDataEx): string {
|
||||||
// For higher performance, create custom plug-in data strings.
|
// For higher performance, create custom plug-in data strings.
|
||||||
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
// Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely.
|
||||||
@@ -254,7 +249,8 @@ function deserialize<T>(str: string[], def: T) {
|
|||||||
return JSON.parse(str.join("")) as T;
|
return JSON.parse(str.join("")) as T;
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
return parseYaml(str.join(""));
|
const parsed: unknown = parseYaml(str.join(""));
|
||||||
|
return parsed as T;
|
||||||
} catch {
|
} catch {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
@@ -1104,12 +1100,11 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
await delay(100);
|
await delay(100);
|
||||||
this._log(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE);
|
this._log(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE);
|
||||||
if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") {
|
if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") {
|
||||||
//@ts-ignore
|
const pluginManager = getObsidianCommunityPluginManager(this.app);
|
||||||
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
|
const pluginManifest = pluginManager.manifests.find(
|
||||||
//@ts-ignore
|
(manifest) =>
|
||||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
pluginManager.enabledPlugins.has(manifest.id) &&
|
||||||
const pluginManifest = manifests.find(
|
manifest.dir == `${baseDir}/plugins/${data.name}`
|
||||||
(manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`
|
|
||||||
);
|
);
|
||||||
if (pluginManifest) {
|
if (pluginManifest) {
|
||||||
this._log(
|
this._log(
|
||||||
@@ -1117,10 +1112,8 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
"plugin-reload-" + pluginManifest.id
|
"plugin-reload-" + pluginManifest.id
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
await pluginManager.unloadPlugin(pluginManifest.id);
|
||||||
await this.app.plugins.unloadPlugin(pluginManifest.id);
|
await pluginManager.loadPlugin(pluginManifest.id);
|
||||||
// @ts-ignore
|
|
||||||
await this.app.plugins.loadPlugin(pluginManifest.id);
|
|
||||||
this._log(
|
this._log(
|
||||||
`Plugin reloaded: ${pluginManifest.name}`,
|
`Plugin reloaded: ${pluginManifest.name}`,
|
||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
@@ -1200,7 +1193,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
const updatedPluginKey = "popupUpdated-plugins";
|
const updatedPluginKey = "popupUpdated-plugins";
|
||||||
scheduleTask(updatedPluginKey, 1000, async () => {
|
scheduleTask(updatedPluginKey, 1000, async () => {
|
||||||
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0));
|
||||||
//@ts-ignore
|
//@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl.
|
||||||
const isShown = popup?.noticeEl?.isShown();
|
const isShown = popup?.noticeEl?.isShown();
|
||||||
if (!isShown) {
|
if (!isShown) {
|
||||||
memoObject(updatedPluginKey, new Notice(fragment, 0));
|
memoObject(updatedPluginKey, new Notice(fragment, 0));
|
||||||
@@ -1208,7 +1201,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
scheduleTask(updatedPluginKey + "-close", 20000, () => {
|
||||||
const popup = retrieveMemoObject<Notice>(updatedPluginKey);
|
const popup = retrieveMemoObject<Notice>(updatedPluginKey);
|
||||||
if (!popup) return;
|
if (!popup) return;
|
||||||
//@ts-ignore
|
//@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl.
|
||||||
if (popup?.noticeEl?.isShown()) {
|
if (popup?.noticeEl?.isShown()) {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
}
|
}
|
||||||
@@ -1250,12 +1243,14 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
if (path.toLowerCase().endsWith("/manifest.json")) {
|
if (path.toLowerCase().endsWith("/manifest.json")) {
|
||||||
const v = readString(new Uint8Array(contentBin));
|
const v = readString(new Uint8Array(contentBin));
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(v);
|
const json: unknown = JSON.parse(v);
|
||||||
if ("version" in json) {
|
if (typeof json === "object" && json !== null) {
|
||||||
version = `${json.version}`;
|
if ("version" in json) {
|
||||||
}
|
version = String(json.version);
|
||||||
if ("name" in json) {
|
}
|
||||||
displayName = `${json.name}`;
|
if ("name" in json) {
|
||||||
|
displayName = String(json.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this._log(
|
this._log(
|
||||||
@@ -1456,7 +1451,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
const oldC = await this.localDatabase.getDBEntryFromMeta(old, false, false);
|
const oldC = await this.localDatabase.getDBEntryFromMeta(old, false, false);
|
||||||
if (oldC) {
|
if (oldC) {
|
||||||
const d = (await deserialize(getDocDataAsArray(oldC.data), {})) as PluginDataEx;
|
const d = deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx;
|
||||||
if (d.files.length == dt.files.length) {
|
if (d.files.length == dt.files.length) {
|
||||||
const diffs = d.files
|
const diffs = d.files
|
||||||
.map((previous) => ({
|
.map((previous) => ({
|
||||||
@@ -1709,11 +1704,11 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _allConfigureOptionalSyncFeature(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
private async _allConfigureOptionalSyncFeature(mode: OptionalSyncFeatureMode) {
|
||||||
await this.configureHiddenFileSync(mode);
|
await this.configureHiddenFileSync(mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
async configureHiddenFileSync(mode: OptionalSyncFeatureMode) {
|
||||||
if (mode == "DISABLE") {
|
if (mode == "DISABLE") {
|
||||||
// this.plugin.settings.usePluginSync = false;
|
// this.plugin.settings.usePluginSync = false;
|
||||||
// await this.plugin.saveSettings();
|
// await this.plugin.saveSettings();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PluginManifest, type ListedFiles } from "@/deps.ts";
|
import { type ListedFiles } from "@/deps.ts";
|
||||||
import {
|
import {
|
||||||
type LoadedEntry,
|
type LoadedEntry,
|
||||||
type FilePathWithPrefix,
|
type FilePathWithPrefix,
|
||||||
@@ -55,20 +55,13 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
|||||||
import type { LiveSyncCore } from "@/main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
import { tryGetFilePath } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
import { tryGetFilePath } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||||
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
|
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
|
||||||
|
import type { OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts";
|
||||||
|
import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlugins.ts";
|
||||||
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
||||||
|
|
||||||
const HIDDEN_FILE_NOTICE_GROUP = "hidden-file-changes";
|
const HIDDEN_FILE_NOTICE_GROUP = "hidden-file-changes";
|
||||||
const HIDDEN_FILE_NOTICE_DURATION_MS = 20_000;
|
const HIDDEN_FILE_NOTICE_DURATION_MS = 20_000;
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface OPTIONAL_SYNC_FEATURES {
|
|
||||||
FETCH: "FETCH";
|
|
||||||
OVERWRITE: "OVERWRITE";
|
|
||||||
MERGE: "MERGE";
|
|
||||||
DISABLE: "DISABLE";
|
|
||||||
DISABLE_HIDDEN: "DISABLE_HIDDEN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getComparingMTime(
|
function getComparingMTime(
|
||||||
doc: (MetaEntry | LoadedEntry | false) | UXFileInfo | UXStat | null | undefined,
|
doc: (MetaEntry | LoadedEntry | false) | UXFileInfo | UXStat | null | undefined,
|
||||||
includeDeleted = false
|
includeDeleted = false
|
||||||
@@ -1233,11 +1226,10 @@ Offline Changed files: ${files.length}`;
|
|||||||
const noticeGroups = this.services.context.noticeGroups;
|
const noticeGroups = this.services.context.noticeGroups;
|
||||||
let hasNoticeItems = false;
|
let hasNoticeItems = false;
|
||||||
try {
|
try {
|
||||||
//@ts-ignore
|
const pluginManager = getObsidianCommunityPluginManager(this.app);
|
||||||
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
|
const enabledPluginManifests = pluginManager.manifests.filter((manifest) =>
|
||||||
//@ts-ignore
|
pluginManager.enabledPlugins.has(manifest.id)
|
||||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
);
|
||||||
const enabledPluginManifests = manifests.filter((e) => enabledPlugins.has(e.id));
|
|
||||||
const modifiedManifests = enabledPluginManifests.filter((e) => updatedFolders.indexOf(e?.dir ?? "") >= 0);
|
const modifiedManifests = enabledPluginManifests.filter((e) => updatedFolders.indexOf(e?.dir ?? "") >= 0);
|
||||||
for (const manifest of modifiedManifests) {
|
for (const manifest of modifiedManifests) {
|
||||||
// If notified about plug-ins, reloading Obsidian may not be necessary.
|
// If notified about plug-ins, reloading Obsidian may not be necessary.
|
||||||
@@ -1255,10 +1247,8 @@ Offline Changed files: ${files.length}`;
|
|||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
"plugin-reload-" + updatePluginId
|
"plugin-reload-" + updatePluginId
|
||||||
);
|
);
|
||||||
// @ts-ignore -- Obsidian does not expose the plug-in lifecycle methods in its public API.
|
await pluginManager.unloadPlugin(updatePluginId);
|
||||||
await this.app.plugins.unloadPlugin(updatePluginId);
|
await pluginManager.loadPlugin(updatePluginId);
|
||||||
// @ts-ignore -- Obsidian does not expose the plug-in lifecycle methods in its public API.
|
|
||||||
await this.app.plugins.loadPlugin(updatePluginId);
|
|
||||||
this._log(
|
this._log(
|
||||||
`Plugin reloaded: ${updatePluginName}`,
|
`Plugin reloaded: ${updatePluginName}`,
|
||||||
LOG_LEVEL_NOTICE,
|
LOG_LEVEL_NOTICE,
|
||||||
@@ -1793,12 +1783,12 @@ Offline Changed files: ${files.length}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --> Configuration handling
|
// --> Configuration handling
|
||||||
private async _allConfigureOptionalSyncFeature(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
private async _allConfigureOptionalSyncFeature(mode: OptionalSyncFeatureMode) {
|
||||||
await this.configureHiddenFileSync(mode);
|
await this.configureHiddenFileSync(mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
async configureHiddenFileSync(mode: OptionalSyncFeatureMode) {
|
||||||
const result = await configureHiddenFileSyncMode(mode, {
|
const result = await configureHiddenFileSyncMode(mode, {
|
||||||
disable: async () => {
|
disable: async () => {
|
||||||
// await this.core.$allSuspendExtraSync();
|
// await this.core.$allSuspendExtraSync();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
type HiddenFileSyncMode = "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN";
|
import type { HiddenFileSyncMode, OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts";
|
||||||
|
|
||||||
type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe";
|
type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe";
|
||||||
|
|
||||||
type ConfigureHiddenFileSyncHandlers = {
|
type ConfigureHiddenFileSyncHandlers = {
|
||||||
@@ -9,23 +10,25 @@ type ConfigureHiddenFileSyncHandlers = {
|
|||||||
|
|
||||||
export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled";
|
export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled";
|
||||||
|
|
||||||
function getInitialiseDirection(mode: keyof OPTIONAL_SYNC_FEATURES): HiddenFileSyncDirection | false {
|
function getInitialiseDirection(mode: HiddenFileSyncMode): HiddenFileSyncDirection | false {
|
||||||
if (mode == "FETCH") return "pullForce";
|
if (mode == "FETCH") return "pullForce";
|
||||||
if (mode == "OVERWRITE") return "pushForce";
|
if (mode == "OVERWRITE") return "pushForce";
|
||||||
if (mode == "MERGE") return "safe";
|
if (mode == "MERGE") return "safe";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDisableMode(mode: keyof OPTIONAL_SYNC_FEATURES): boolean {
|
function isDisableMode(
|
||||||
|
mode: OptionalSyncFeatureMode
|
||||||
|
): mode is Extract<HiddenFileSyncMode, "DISABLE" | "DISABLE_HIDDEN"> {
|
||||||
return mode == "DISABLE" || mode == "DISABLE_HIDDEN";
|
return mode == "DISABLE" || mode == "DISABLE_HIDDEN";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES): mode is HiddenFileSyncMode {
|
function isHiddenFileSyncMode(mode: OptionalSyncFeatureMode): mode is HiddenFileSyncMode {
|
||||||
return mode == "FETCH" || mode == "OVERWRITE" || mode == "MERGE" || isDisableMode(mode);
|
return mode == "FETCH" || mode == "OVERWRITE" || mode == "MERGE" || isDisableMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function configureHiddenFileSyncMode(
|
export async function configureHiddenFileSyncMode(
|
||||||
mode: keyof OPTIONAL_SYNC_FEATURES,
|
mode: OptionalSyncFeatureMode,
|
||||||
handlers: ConfigureHiddenFileSyncHandlers
|
handlers: ConfigureHiddenFileSyncHandlers
|
||||||
): Promise<ConfigureHiddenFileSyncResult> {
|
): Promise<ConfigureHiddenFileSyncResult> {
|
||||||
if (!isHiddenFileSyncMode(mode)) {
|
if (!isHiddenFileSyncMode(mode)) {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface OptionalSyncFeatures {
|
||||||
|
DISABLE: "DISABLE";
|
||||||
|
CUSTOMIZE: "CUSTOMIZE";
|
||||||
|
DISABLE_CUSTOM: "DISABLE_CUSTOM";
|
||||||
|
FETCH: "FETCH";
|
||||||
|
OVERWRITE: "OVERWRITE";
|
||||||
|
MERGE: "MERGE";
|
||||||
|
DISABLE_HIDDEN: "DISABLE_HIDDEN";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OptionalSyncFeatureMode = keyof OptionalSyncFeatures;
|
||||||
|
export type HiddenFileSyncMode = Extract<
|
||||||
|
OptionalSyncFeatureMode,
|
||||||
|
"FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN"
|
||||||
|
>;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface OPTIONAL_SYNC_FEATURES {
|
||||||
|
DISABLE: "DISABLE";
|
||||||
|
CUSTOMIZE: "CUSTOMIZE";
|
||||||
|
DISABLE_CUSTOM: "DISABLE_CUSTOM";
|
||||||
|
FETCH: "FETCH";
|
||||||
|
OVERWRITE: "OVERWRITE";
|
||||||
|
MERGE: "MERGE";
|
||||||
|
DISABLE_HIDDEN: "DISABLE_HIDDEN";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { serialized } from "octagonal-wheels/concurrency/lock";
|
|||||||
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||||
|
import type PouchDB from "pouchdb-core";
|
||||||
|
|
||||||
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
||||||
type ReplicateResultProcessorState = {
|
type ReplicateResultProcessorState = {
|
||||||
|
|||||||
@@ -12,10 +12,22 @@ import type {
|
|||||||
UXFileInfoStub,
|
UXFileInfoStub,
|
||||||
UXFolderInfo,
|
UXFolderInfo,
|
||||||
UXInternalFileInfoStub,
|
UXInternalFileInfoStub,
|
||||||
|
UXStat,
|
||||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import type { LiveSyncCore } from "@/main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
import type { FileAccessObsidian } from "@/serviceModules/FileAccessObsidian.ts";
|
import type { FileAccessObsidian } from "@/serviceModules/FileAccessObsidian.ts";
|
||||||
|
|
||||||
|
function isUXStat(value: unknown): value is UXStat {
|
||||||
|
if (typeof value !== "object" || value === null) return false;
|
||||||
|
const stat = value as Partial<UXStat>;
|
||||||
|
return (
|
||||||
|
typeof stat.size === "number" &&
|
||||||
|
typeof stat.ctime === "number" &&
|
||||||
|
typeof stat.mtime === "number" &&
|
||||||
|
(stat.type === "file" || stat.type === "folder")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function TFileToUXFileInfo(
|
export async function TFileToUXFileInfo(
|
||||||
core: LiveSyncCore,
|
core: LiveSyncCore,
|
||||||
file: TFile,
|
file: TFile,
|
||||||
@@ -55,8 +67,8 @@ export async function InternalFileToUXFileInfo(
|
|||||||
prefix: string = ICHeader
|
prefix: string = ICHeader
|
||||||
): Promise<UXFileInfo> {
|
): Promise<UXFileInfo> {
|
||||||
const name = fullPath.split("/").pop() as string;
|
const name = fullPath.split("/").pop() as string;
|
||||||
const stat = await vaultAccess.tryAdapterStat(fullPath);
|
const stat: unknown = await vaultAccess.tryAdapterStat(fullPath);
|
||||||
if (stat == null) throw new Error(`File not found: ${fullPath}`);
|
if (!isUXStat(stat)) throw new Error(`File not found: ${fullPath}`);
|
||||||
if (stat.type == "folder") throw new Error(`File not found: ${fullPath}`);
|
if (stat.type == "folder") throw new Error(`File not found: ${fullPath}`);
|
||||||
const file = await vaultAccess.adapterReadAuto(fullPath);
|
const file = await vaultAccess.adapterReadAuto(fullPath);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ function requestTimeout(timeoutInMs: number = 0): Promise<never> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normaliseRequestBody(body: unknown): string | ArrayBuffer | undefined {
|
||||||
|
if (typeof body === "string" || body instanceof ArrayBuffer) return body;
|
||||||
|
if (ArrayBuffer.isView(body)) {
|
||||||
|
if (body.buffer instanceof ArrayBuffer) return body.buffer;
|
||||||
|
return new Uint8Array(body.buffer).slice().buffer;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is close to origin implementation of FetchHttpHandler
|
* This is close to origin implementation of FetchHttpHandler
|
||||||
* https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts
|
* https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts
|
||||||
@@ -64,7 +73,7 @@ export class ObsHttpHandler extends FetchHttpHandler {
|
|||||||
urlObj.host = this.reverseProxyNoSignUrl;
|
urlObj.host = this.reverseProxyNoSignUrl;
|
||||||
url = urlObj.href;
|
url = urlObj.href;
|
||||||
}
|
}
|
||||||
const body = method === "GET" || method === "HEAD" ? undefined : request.body;
|
const body: unknown = method === "GET" || method === "HEAD" ? undefined : request.body;
|
||||||
|
|
||||||
const transformedHeaders: Record<string, string> = {};
|
const transformedHeaders: Record<string, string> = {};
|
||||||
for (const key of Object.keys(request.headers)) {
|
for (const key of Object.keys(request.headers)) {
|
||||||
@@ -80,10 +89,7 @@ export class ObsHttpHandler extends FetchHttpHandler {
|
|||||||
contentType = transformedHeaders["content-type"];
|
contentType = transformedHeaders["content-type"];
|
||||||
}
|
}
|
||||||
|
|
||||||
let transformedBody = body;
|
const transformedBody = normaliseRequestBody(body);
|
||||||
if (ArrayBuffer.isView(body)) {
|
|
||||||
transformedBody = new Uint8Array(body.buffer).buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const param: RequestUrlParam = {
|
const param: RequestUrlParam = {
|
||||||
body: transformedBody,
|
body: transformedBody,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
loadDocumentHistoryPreference,
|
loadDocumentHistoryPreference,
|
||||||
saveDocumentHistoryPreference,
|
saveDocumentHistoryPreference,
|
||||||
} from "./documentHistoryPreferences.ts";
|
} from "./documentHistoryPreferences.ts";
|
||||||
|
import type PouchDB from "pouchdb-core";
|
||||||
|
|
||||||
function isImage(path: string) {
|
function isImage(path: string) {
|
||||||
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { globalSlipBoard } from "@vrtmrz/livesync-commonlib/compat/bureau/bureau
|
|||||||
export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string;
|
export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Slips extends LSSlips {
|
interface Slips {
|
||||||
"conflict-resolved": typeof CANCELLED | MergeDialogResult;
|
"conflict-resolved": typeof CANCELLED | MergeDialogResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
import { isObjectDifferent } from "octagonal-wheels/object";
|
import { isObjectDifferent } from "octagonal-wheels/object";
|
||||||
import { EVENT_SETTING_SAVED, eventHub } from "@/common/events";
|
import { EVENT_SETTING_SAVED, eventHub } from "@/common/events";
|
||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
import { fireAndForget } from "octagonal-wheels/promises";
|
||||||
import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
import {
|
||||||
import { parseYaml, stringifyYaml } from "@/deps";
|
DEFAULT_SETTINGS,
|
||||||
|
type FilePathWithPrefix,
|
||||||
|
type ObsidianLiveSyncSettings,
|
||||||
|
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
|
import { parseYaml, stringifyYaml, type Editor, type MarkdownView } from "@/deps";
|
||||||
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||||
@@ -28,7 +32,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractModule {
|
|||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-import-config",
|
id: "livesync-import-config",
|
||||||
name: "Parse setting file",
|
name: "Parse setting file",
|
||||||
editorCheckCallback: (checking, editor, ctx) => {
|
editorCheckCallback: (checking: boolean, editor: Editor, ctx: MarkdownView) => {
|
||||||
if (checking) {
|
if (checking) {
|
||||||
const doc = editor.getValue();
|
const doc = editor.getValue();
|
||||||
const ret = this.extractSettingFromWholeText(doc);
|
const ret = this.extractSettingFromWholeText(doc);
|
||||||
@@ -104,7 +108,11 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractModule {
|
|||||||
const { body } = await this.parseSettingFromMarkdown(filename);
|
const { body } = await this.parseSettingFromMarkdown(filename);
|
||||||
let newSetting = {} as Partial<ObsidianLiveSyncSettings>;
|
let newSetting = {} as Partial<ObsidianLiveSyncSettings>;
|
||||||
try {
|
try {
|
||||||
newSetting = parseYaml(body);
|
const parsed: unknown = parseYaml(body);
|
||||||
|
if (typeof parsed !== "object" || parsed === null) {
|
||||||
|
throw new TypeError("The YAML settings must contain an object");
|
||||||
|
}
|
||||||
|
newSetting = parsed;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this._log("Could not parse YAML", LOG_LEVEL_NOTICE);
|
this._log("Could not parse YAML", LOG_LEVEL_NOTICE);
|
||||||
this._log(ex, LOG_LEVEL_VERBOSE);
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
|||||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
|
||||||
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "@/common/events.ts";
|
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "@/common/events.ts";
|
||||||
import type { LiveSyncCore } from "@/main.ts";
|
import type { LiveSyncCore } from "@/main.ts";
|
||||||
|
import { openObsidianSettings } from "@/common/obsidianSettings.ts";
|
||||||
|
|
||||||
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
||||||
settingTab!: ObsidianLiveSyncSettingTab;
|
settingTab!: ObsidianLiveSyncSettingTab;
|
||||||
@@ -20,11 +21,7 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSetting() {
|
openSetting() {
|
||||||
// Undocumented API
|
openObsidianSettings(this.app, "obsidian-livesync");
|
||||||
//@ts-ignore
|
|
||||||
this.app.setting.open();
|
|
||||||
//@ts-ignore
|
|
||||||
this.app.setting.openTabById("obsidian-livesync");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get appId() {
|
get appId() {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import { paneMaintenance } from "./PaneMaintenance.ts";
|
|||||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||||
import { JournalSyncCore } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncCore";
|
import { JournalSyncCore } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncCore";
|
||||||
import { MinioStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/replication/journal/objectstore/MinioStorageAdapter";
|
import { MinioStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/replication/journal/objectstore/MinioStorageAdapter";
|
||||||
|
import { closeObsidianSettings } from "@/common/obsidianSettings.ts";
|
||||||
|
|
||||||
// For creating a document
|
// For creating a document
|
||||||
// const toc = new Set<string>();
|
// const toc = new Set<string>();
|
||||||
@@ -99,6 +100,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
// Buffered Settings for comparing.
|
// Buffered Settings for comparing.
|
||||||
initialSettings?: typeof this.editingSettings;
|
initialSettings?: typeof this.editingSettings;
|
||||||
|
|
||||||
|
private copySettingValue(target: object | undefined, source: object, key: AllSettingItemKey): void {
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("Initial settings have not been loaded");
|
||||||
|
}
|
||||||
|
const value: unknown = Reflect.get(source, key);
|
||||||
|
Reflect.set(target, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply editing setting to the plug-in.
|
* Apply editing setting to the plug-in.
|
||||||
* @param keys setting keys for applying
|
* @param keys setting keys for applying
|
||||||
@@ -111,10 +120,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
// this.initialSettings[k] = this.editingSettings[k];
|
// this.initialSettings[k] = this.editingSettings[k];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
this.copySettingValue(this.core.settings, this.editingSettings, k);
|
||||||
this.core.settings[k] = this.editingSettings[k];
|
this.copySettingValue(this.initialSettings, this.core.settings, k);
|
||||||
//@ts-ignore
|
|
||||||
this.initialSettings[k] = this.core.settings[k];
|
|
||||||
}
|
}
|
||||||
keys.forEach((e) => this.refreshSetting(e));
|
keys.forEach((e) => this.refreshSetting(e));
|
||||||
}
|
}
|
||||||
@@ -149,14 +156,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
appliedKeys.push(k);
|
appliedKeys.push(k);
|
||||||
if (k in OnDialogSettingsDefault) {
|
if (k in OnDialogSettingsDefault) {
|
||||||
await this.saveLocalSetting(k as keyof OnDialogSettings);
|
await this.saveLocalSetting(k as keyof OnDialogSettings);
|
||||||
//@ts-ignore
|
this.copySettingValue(this.initialSettings, this.editingSettings, k);
|
||||||
this.initialSettings[k] = this.editingSettings[k];
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
this.copySettingValue(this.core.settings, this.editingSettings, k);
|
||||||
this.core.settings[k] = this.editingSettings[k];
|
this.copySettingValue(this.initialSettings, this.core.settings, k);
|
||||||
//@ts-ignore
|
|
||||||
this.initialSettings[k] = this.core.settings[k];
|
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,15 +238,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
const localSetting = this.reloadAllLocalSettings();
|
const localSetting = this.reloadAllLocalSettings();
|
||||||
if (key in this.core.settings) {
|
if (key in this.core.settings) {
|
||||||
if (key in localSetting) {
|
if (key in localSetting) {
|
||||||
//@ts-ignore
|
this.copySettingValue(this.initialSettings, localSetting, key);
|
||||||
this.initialSettings[key] = localSetting[key];
|
this.copySettingValue(this.editingSettings, localSetting, key);
|
||||||
//@ts-ignore
|
|
||||||
this.editingSettings[key] = localSetting[key];
|
|
||||||
} else {
|
} else {
|
||||||
//@ts-ignore
|
this.copySettingValue(this.initialSettings, this.core.settings, key);
|
||||||
this.initialSettings[key] = this.core.settings[key];
|
this.copySettingValue(this.editingSettings, this.initialSettings ?? {}, key);
|
||||||
//@ts-ignore
|
|
||||||
this.editingSettings[key] = this.initialSettings[key];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() };
|
this.editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() };
|
||||||
@@ -308,8 +308,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeSetting() {
|
closeSetting() {
|
||||||
//@ts-ignore :
|
closeObsidianSettings(this.plugin.app);
|
||||||
this.plugin.app.setting.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleElement(element: HTMLElement, func: OnUpdateFunc) {
|
handleElement(element: HTMLElement, func: OnUpdateFunc) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MarkdownRenderer } from "@/deps.ts";
|
import { MarkdownRenderer } from "@/deps.ts";
|
||||||
import { fireAndForget } from "octagonal-wheels/promises";
|
import { fireAndForget } from "octagonal-wheels/promises";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
//@ts-ignore
|
declare const UPDATE_INFO: string;
|
||||||
const updateInformation: string = UPDATE_INFO || "";
|
const updateInformation: string = UPDATE_INFO || "";
|
||||||
|
|
||||||
export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void {
|
export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { requestToCouchDBWithCredentials } from "@/common/utils";
|
import { requestToCouchDBWithCredentials } from "@/common/utils";
|
||||||
import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
||||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
import {
|
||||||
|
LOG_LEVEL_INFO,
|
||||||
|
LOG_LEVEL_NOTICE,
|
||||||
|
LOG_LEVEL_VERBOSE,
|
||||||
|
Logger,
|
||||||
|
} from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import { fireAndForget, parseHeaderValues } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
import { fireAndForget, parseHeaderValues } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||||
import { isCloudantURI } from "@vrtmrz/livesync-commonlib/compat/pouchdb/utils_couchdb";
|
import { isCloudantURI } from "@vrtmrz/livesync-commonlib/compat/pouchdb/utils_couchdb";
|
||||||
import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib";
|
import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib";
|
||||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||||
import { isUnauthorizedError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
import { isUnauthorizedError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||||
|
import { normaliseCouchDBConfiguration } from "@/common/couchdbConfiguration";
|
||||||
|
|
||||||
export const checkConfig = async (
|
export const checkConfig = async (
|
||||||
checkResultDiv: HTMLDivElement | undefined,
|
checkResultDiv: HTMLDivElement | undefined,
|
||||||
@@ -43,7 +49,7 @@ export const checkConfig = async (
|
|||||||
undefined,
|
undefined,
|
||||||
customHeaders
|
customHeaders
|
||||||
);
|
);
|
||||||
const responseConfig = r.json;
|
const responseConfig = normaliseCouchDBConfiguration(r.json as unknown);
|
||||||
|
|
||||||
const addConfigFixButton = (title: string, key: string, value: string) => {
|
const addConfigFixButton = (title: string, key: string, value: string) => {
|
||||||
if (!checkResultDiv) return;
|
if (!checkResultDiv) return;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { isCloudantURI } from "@vrtmrz/livesync-commonlib/compat/pouchdb/utils_c
|
|||||||
import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib";
|
import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib";
|
||||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||||
import { isUnauthorizedError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
import { isUnauthorizedError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||||
|
import { normaliseCouchDBConfiguration } from "@/common/couchdbConfiguration";
|
||||||
|
|
||||||
export type ResultMessage = { message: string; classes: string[] };
|
export type ResultMessage = { message: string; classes: string[] };
|
||||||
export type ResultErrorMessage = { message: string; result: "error"; classes: string[] };
|
export type ResultErrorMessage = { message: string; result: "error"; classes: string[] };
|
||||||
@@ -115,7 +116,7 @@ export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) =>
|
|||||||
undefined,
|
undefined,
|
||||||
customHeaders
|
customHeaders
|
||||||
);
|
);
|
||||||
const responseConfig = r.json;
|
const responseConfig = normaliseCouchDBConfiguration(r.json as unknown);
|
||||||
addMessage($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]);
|
addMessage($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]);
|
||||||
addMessage($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]);
|
addMessage($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]);
|
||||||
addMessage($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]);
|
addMessage($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]);
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import {
|
|||||||
} from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
} from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
||||||
import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext";
|
import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext";
|
||||||
|
|
||||||
export function normaliseObsidianSettingsData(
|
export function normaliseObsidianSettingsData(data: unknown): ObsidianLiveSyncSettings | undefined {
|
||||||
data: ObsidianLiveSyncSettings | null | undefined
|
if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined;
|
||||||
): ObsidianLiveSyncSettings | undefined {
|
return data as ObsidianLiveSyncSettings;
|
||||||
return data ?? undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ObsidianSettingService<T extends ObsidianServiceContext> extends SettingService<T> {
|
export class ObsidianSettingService<T extends ObsidianServiceContext> extends SettingService<T> {
|
||||||
|
|||||||
Reference in New Issue
Block a user