## 0.24.18

### Fixed

- Now no chunk creation errors will be raised after switching `Compute revisions for chunks`.
- Some invisible file can be handled correctly (e.g., `writing-goals-history.csv`).
- Fetching configuration from the server is now saves the configuration immediately (if we are not in the wizard).

### Improved

- Mismatched configuration dialogue is now more informative, and rewritten to more user-friendly.
- Applying configuration mismatch is now without rebuilding (at our own risks).
- Now, rebuilding is decided more fine grained.

### Improved internally

- Translations can be nested. i.e., task:`Some procedure`, check: `%{task} checking`, checkfailed: `%{check} failed` produces `Some procedure checking failed`.
This commit is contained in:
vorotamoroz
2025-02-28 11:58:15 +00:00
parent cbcfdc453e
commit 5d70f2c1e9
9 changed files with 281 additions and 180 deletions

Submodule src/lib updated: 9888ee8859...b8e4fa6b9e

View File

@@ -297,7 +297,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
} }
await this.storage.ensureDir(path); await this.storage.ensureDir(path);
const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime }); const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime });
this.storage.touched(path); await this.storage.touched(path);
this.storage.triggerFileEvent(mode, path); this.storage.triggerFileEvent(mode, path);
return ret; return ret;
} }

View File

@@ -30,6 +30,7 @@ import { isAnyNote } from "../../lib/src/common/utils";
import { EVENT_FILE_SAVED, eventHub } from "../../common/events"; import { EVENT_FILE_SAVED, eventHub } from "../../common/events";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator"; import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { globalSlipBoard } from "../../lib/src/bureau/bureau"; import { globalSlipBoard } from "../../lib/src/bureau/bureau";
import { $msg } from "../../lib/src/common/i18n";
const KEY_REPLICATION_ON_EVENT = "replicationOnEvent"; const KEY_REPLICATION_ON_EVENT = "replicationOnEvent";
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000; const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
@@ -46,7 +47,7 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
async setReplicator() { async setReplicator() {
const replicator = await this.core.$anyNewReplicator(); const replicator = await this.core.$anyNewReplicator();
if (!replicator) { if (!replicator) {
this._log("No replicator is available, this is the fatal error.", LOG_LEVEL_NOTICE); this._log($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
return false; return false;
} }
this.core.replicator = replicator; this.core.replicator = replicator;
@@ -79,23 +80,82 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT); updatePreviousExecutionTime(KEY_REPLICATION_ON_EVENT);
} }
} }
/**
* obsolete method. No longer maintained and will be removed in the future.
* @deprecated v0.24.17
* @param showMessage If true, show message to the user.
*/
async cleaned(showMessage: boolean) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
await skipIfDuplicated("cleanup", async () => {
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`;
const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage(
"Cleaned",
message,
[CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS],
CHOICE_DISMISS,
30
);
if (ret == CHOICE_FETCH) {
await this.core.rebuilder.$performRebuildDB("localOnly");
}
if (ret == CHOICE_CLEAN) {
const replicator = this.core.$$getReplicator();
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
this.settings,
this.core.$$isMobile(),
true
);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL_NOTICE);
return false;
}
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
// Perform the synchronisation once.
if (await this.core.replicator.openReplication(this.settings, false, showMessage, true)) {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
await this.core.$$getReplicator().markRemoteResolved(this.settings);
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
} else {
Logger(
"Replication has been cancelled. Please try it again.",
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
}
}
});
}
async $$_replicate(showMessage: boolean = false): Promise<boolean | void> { async $$_replicate(showMessage: boolean = false): Promise<boolean | void> {
//--? //--?
if (!this.core.$$isReady()) return; if (!this.core.$$isReady()) return;
if (isLockAcquired("cleanup")) { if (isLockAcquired("cleanup")) {
Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL_NOTICE); Logger($msg("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
return; return;
} }
if (this.settings.versionUpFlash != "") { if (this.settings.versionUpFlash != "") {
Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL_NOTICE); Logger($msg("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
return; return;
} }
if (!(await this.core.$everyCommitPendingFileEvent())) { if (!(await this.core.$everyCommitPendingFileEvent())) {
Logger("Some file events are pending. Replication has been cancelled.", LOG_LEVEL_NOTICE); Logger($msg("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
return false; return false;
} }
if (!(await this.core.$everyBeforeReplicate(showMessage))) { if (!(await this.core.$everyBeforeReplicate(showMessage))) {
Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE); Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
return false; return false;
} }
@@ -107,106 +167,35 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
} else { } else {
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) { if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) { if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
Logger( await this.cleaned(showMessage);
`The remote database has been cleaned.`,
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
await skipIfDuplicated("cleanup", async () => {
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`;
const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage(
"Cleaned",
message,
[CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS],
CHOICE_DISMISS,
30
);
if (ret == CHOICE_FETCH) {
await this.core.rebuilder.$performRebuildDB("localOnly");
}
if (ret == CHOICE_CLEAN) {
const replicator = this.core.$$getReplicator();
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
this.settings,
this.core.$$isMobile(),
true
);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL_NOTICE);
return false;
}
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
// Perform the synchronisation once.
if (
await this.core.replicator.openReplication(this.settings, false, showMessage, true)
) {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.hashCaches.clear();
await this.core.$$getReplicator().markRemoteResolved(this.settings);
Logger(
"The local database has been cleaned up.",
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
} else {
Logger(
"Replication has been cancelled. Please try it again.",
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
}
}
});
} else { } else {
const message = ` const message = $msg("Replicator.Dialogue.Locked.Message");
The remote database has been rebuilt. const CHOICE_FETCH = $msg("Replicator.Dialogue.Locked.Action.Fetch");
To synchronize, this device must fetch everything again once. const CHOICE_DISMISS = $msg("Replicator.Dialogue.Locked.Action.Dismiss");
Or if you are sure know what had been happened, we can unlock the database from the setting dialog. const CHOICE_UNLOCK = $msg("Replicator.Dialogue.Locked.Action.Unlock");
`; const ret = await this.core.confirm.askSelectStringDialogue(
const CHOICE_FETCH = "Fetch again";
const CHOICE_DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage(
"Locked",
message, message,
[CHOICE_FETCH, CHOICE_DISMISS], [CHOICE_FETCH, CHOICE_UNLOCK, CHOICE_DISMISS],
CHOICE_DISMISS, {
10 title: $msg("Replicator.Dialogue.Locked.Title"),
defaultAction: CHOICE_DISMISS,
timeout: 60,
}
); );
if (ret == CHOICE_FETCH) { if (ret == CHOICE_FETCH) {
const CHOICE_RESTART = "Restart"; this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
const CHOICE_WITHOUT_RESTART = "Without restart"; await this.core.rebuilder.scheduleFetch();
if ( this.core.$$scheduleAppReload();
(await this.core.confirm.askSelectStringDialogue( return;
"Self-hosted LiveSync restarts Obsidian to fetch everything safely. However, you can do it without restarting. Please choose one.", } else if (ret == CHOICE_UNLOCK) {
[CHOICE_RESTART, CHOICE_WITHOUT_RESTART], await this.core.replicator.markRemoteResolved(this.settings);
{ this._log($msg("Replicator.Dialogue.Locked.Message.Unlocked"), LOG_LEVEL_NOTICE);
title: "Fetch again", return;
defaultAction: CHOICE_RESTART,
timeout: 30,
}
)) == CHOICE_RESTART
) {
await this.core.rebuilder.scheduleFetch();
// await this.core.$$scheduleAppReload();
return;
} else {
await this.core.rebuilder.$performRebuildDB("localOnly");
}
} }
} }
} }
} }
} }
return ret; return ret;
} }
@@ -414,7 +403,7 @@ Or if you are sure know what had been happened, we can unlock the database from
): Promise<boolean> { ): Promise<boolean> {
if (!this.core.$$isReady()) return false; if (!this.core.$$isReady()) return false;
if (!(await this.core.$everyBeforeReplicate(showingNotice))) { if (!(await this.core.$everyBeforeReplicate(showingNotice))) {
Logger(`Replication has been cancelled by some module failure`, LOG_LEVEL_NOTICE); Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
return false; return false;
} }
if (!sendChunksInBulkDisabled) { if (!sendChunksInBulkDisabled) {

View File

@@ -2,14 +2,17 @@ import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import { extractObject } from "octagonal-wheels/object"; import { extractObject } from "octagonal-wheels/object";
import { import {
TweakValuesShouldMatchedTemplate, TweakValuesShouldMatchedTemplate,
CompatibilityBreakingTweakValues, IncompatibleChanges,
confName, confName,
type TweakValues, type TweakValues,
type RemoteDBSettings, type RemoteDBSettings,
IncompatibleChangesInSpecificPattern,
CompatibleButLossyChanges,
} from "../../lib/src/common/types.ts"; } from "../../lib/src/common/types.ts";
import { escapeMarkdownValue } from "../../lib/src/common/utils.ts"; import { escapeMarkdownValue } from "../../lib/src/common/utils.ts";
import { AbstractModule } from "../AbstractModule.ts"; import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts"; import type { ICoreModule } from "../ModuleTypes.ts";
import { $msg } from "../../lib/src/common/i18n.ts";
export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule { export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule {
async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> { async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
@@ -28,65 +31,100 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule implements I
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings); const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const items = Object.entries(TweakValuesShouldMatchedTemplate); const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false; let rebuildRequired = false;
let rebuildRecommended = false;
// Making tables: // Making tables:
let table = `| Value name | This device | Configured | \n` + `|: --- |: --- :|: ---- :| \n`; // let table = `| Value name | This device | Configured | \n` + `|: --- |: --- :|: ---- :| \n`;
const tableRows = [];
// const items = [mine,preferred] // const items = [mine,preferred]
for (const v of items) { for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate; const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const valueMine = escapeMarkdownValue(mine[key]); const valueMine = escapeMarkdownValue(mine[key]);
const valuePreferred = escapeMarkdownValue(preferred[key]); const valuePreferred = escapeMarkdownValue(preferred[key]);
if (valueMine == valuePreferred) continue; if (valueMine == valuePreferred) continue;
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) { if (IncompatibleChanges.indexOf(key) !== -1) {
rebuildRequired = true; rebuildRequired = true;
} }
table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`; for (const pattern of IncompatibleChangesInSpecificPattern) {
if (pattern.key !== key) continue;
// if from value supplied, check if current value have been violated : in other words, if the current value is the same as the from value, it should require a rebuild.
const isFromConditionMet = "from" in pattern ? pattern.from === mine[key] : false;
// and, if to value supplied, same as above.
const isToConditionMet = "to" in pattern ? pattern.to === preferred[key] : false;
// if either of them is true, it should require a rebuild, if the pattern is not a recommendation.
if (isFromConditionMet || isToConditionMet) {
if (pattern.isRecommendation) {
rebuildRecommended = true;
} else {
rebuildRequired = true;
}
}
}
if (CompatibleButLossyChanges.indexOf(key) !== -1) {
rebuildRecommended = true;
}
// table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`;
tableRows.push(
$msg("TweakMismatchResolve.Table.Row", {
name: confName(key),
self: valueMine,
remote: valuePreferred,
})
);
} }
const additionalMessage = rebuildRequired const additionalMessage =
? ` rebuildRequired && this.core.settings.isConfigured
? $msg("TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired")
: "";
const additionalMessage2 =
rebuildRecommended && this.core.settings.isConfigured
? $msg("TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended")
: "";
**Note**: We have detected that some of the values are different to make incompatible the local database with the remote database. const table = $msg("TweakMismatchResolve.Table", { rows: tableRows.join("\n") });
If you choose to use the configured values, the local database will be rebuilt, and if you choose to use the values of this device, the remote database will be rebuilt.
Both of them takes a few minutes. Please choose after considering the situation.`
: "";
const message = ` const message = $msg("TweakMismatchResolve.Message.MainTweakResolving", {
Your configuration has not been matched with the one on the remote server. table: table,
(Which you had decided once before, or set by initially synchronised device). additionalMessage: [additionalMessage, additionalMessage2].filter((v) => v).join("\n"),
});
Configured values: const CHOICE_USE_REMOTE = $msg("TweakMismatchResolve.Action.UseRemote");
const CHOICE_USE_REMOTE_WITH_REBUILD = $msg("TweakMismatchResolve.Action.UseRemoteWithRebuild");
const CHOICE_USE_REMOTE_PREVENT_REBUILD = $msg("TweakMismatchResolve.Action.UseRemoteAcceptIncompatible");
const CHOICE_USE_MINE = $msg("TweakMismatchResolve.Action.UseMine");
const CHOICE_USE_MINE_WITH_REBUILD = $msg("TweakMismatchResolve.Action.UseMineWithRebuild");
const CHOICE_USE_MINE_PREVENT_REBUILD = $msg("TweakMismatchResolve.Action.UseMineAcceptIncompatible");
const CHOICE_DISMISS = $msg("TweakMismatchResolve.Action.Dismiss");
${table} const CHOICE_AND_VALUES = [] as [string, [result: TweakValues | boolean, rebuild: boolean]][];
Please select which one you want to use. if (rebuildRequired) {
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [preferred, true]]);
- Use configured: Update settings of this device by configured one on the remote server. CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]);
You should select this if you have changed the settings on ** another device **. CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_PREVENT_REBUILD, [preferred, false]]);
- Update with mine: Update settings on the remote server by the settings of this device. CHOICE_AND_VALUES.push([CHOICE_USE_MINE_PREVENT_REBUILD, [true, false]]);
You should select this if you have changed the settings on ** this device **. } else if (rebuildRecommended) {
- Dismiss: Ignore this message and keep the current settings. CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]);
You cannot synchronise until you resolve this issue without enabling \`Do not check configuration mismatch before replication\`.${additionalMessage}`; CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [true, true]]);
const CHOICE_USE_REMOTE = "Use configured"; CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]);
const CHOICE_USR_MINE = "Update with mine"; } else {
const CHOICE_DISMISS = "Dismiss"; CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]);
const CHOICE_AND_VALUES = [ CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]);
[CHOICE_USE_REMOTE, preferred], }
[CHOICE_USR_MINE, true], CHOICE_AND_VALUES.push([CHOICE_DISMISS, [false, false]]);
[CHOICE_DISMISS, false], const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<
]; string,
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<string, TweakValues | boolean>; [TweakValues | boolean, performRebuild: boolean]
const retKey = await this.core.confirm.confirmWithMessage( >;
"Tweaks Mismatched or Changed", const retKey = await this.core.confirm.askSelectStringDialogue(message, Object.keys(CHOICES), {
message, title: $msg("TweakMismatchResolve.Title.TweakResolving"),
Object.keys(CHOICES), timeout: 60,
CHOICE_DISMISS, defaultAction: CHOICE_DISMISS,
60 });
);
if (!retKey) return [false, false]; if (!retKey) return [false, false];
return [CHOICES[retKey], rebuildRequired]; return CHOICES[retKey];
} }
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> { async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
@@ -143,28 +181,56 @@ Please select which one you want to use.
return { result: false, requireFetch: false }; return { result: false, requireFetch: false };
} }
} }
async $$askUseRemoteConfiguration( async $$askUseRemoteConfiguration(
trialSetting: RemoteDBSettings, trialSetting: RemoteDBSettings,
preferred: TweakValues preferred: TweakValues
): Promise<{ result: false | TweakValues; requireFetch: boolean }> { ): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
const items = Object.entries(TweakValuesShouldMatchedTemplate); const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false; let rebuildRequired = false;
let rebuildRecommended = false;
// Making tables: // Making tables:
let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`; // let table = `| Value name | This device | On Remote | \n` + `|: --- |: ---- :|: ---- :| \n`;
let differenceCount = 0; let differenceCount = 0;
const tableRows = [] as string[];
// const items = [mine,preferred] // const items = [mine,preferred]
for (const v of items) { for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate; const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const valuePreferred = escapeMarkdownValue(preferred[key]); const remoteValueForDisplay = escapeMarkdownValue(preferred[key]);
const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`; const currentValueForDisplay = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])}`;
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) { if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) { if (IncompatibleChanges.indexOf(key) !== -1) {
rebuildRequired = true; rebuildRequired = true;
} }
for (const pattern of IncompatibleChangesInSpecificPattern) {
if (pattern.key !== key) continue;
// if from value supplied, check if current value have been violated : in other words, if the current value is the same as the from value, it should require a rebuild.
const isFromConditionMet =
"from" in pattern ? pattern.from === (trialSetting as TweakValues)?.[key] : false;
// and, if to value supplied, same as above.
const isToConditionMet = "to" in pattern ? pattern.to === preferred[key] : false;
// if either of them is true, it should require a rebuild, if the pattern is not a recommendation.
if (isFromConditionMet || isToConditionMet) {
if (pattern.isRecommendation) {
rebuildRecommended = true;
} else {
rebuildRequired = true;
}
}
}
if (CompatibleButLossyChanges.indexOf(key) !== -1) {
rebuildRecommended = true;
}
} else { } else {
continue; continue;
} }
table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`; tableRows.push(
$msg("TweakMismatchResolve.Table.Row", {
name: confName(key),
self: currentValueForDisplay,
remote: remoteValueForDisplay,
})
);
differenceCount++; differenceCount++;
} }
@@ -174,33 +240,28 @@ Please select which one you want to use.
} }
const additionalMessage = const additionalMessage =
rebuildRequired && this.core.settings.isConfigured rebuildRequired && this.core.settings.isConfigured
? ` ? $msg("TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired")
: "";
>[!WARNING] const additionalMessage2 =
> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required. rebuildRecommended && this.core.settings.isConfigured
***Please ensure that you have time and are connected to a stable network to apply!***` ? $msg("TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended")
: ""; : "";
const message = ` const table = $msg("TweakMismatchResolve.Table", { rows: tableRows.join("\n") });
The settings in the remote database are as follows.
If you want to use these settings, please select "Use configured".
If you want to keep the settings of this device, please select "Dismiss".
${table} const message = $msg("TweakMismatchResolve.Message.Main", {
table: table,
additionalMessage: [additionalMessage, additionalMessage2].filter((v) => v).join("\n"),
});
>[!TIP] const CHOICE_USE_REMOTE = $msg("TweakMismatchResolve.Action.UseConfigured");
> If you want to synchronise all settings, please use \`Sync settings via markdown\` after applying minimal configuration with this feature. const CHOICE_DISMISS = $msg("TweakMismatchResolve.Action.Dismiss");
${additionalMessage}`;
const CHOICE_USE_REMOTE = "Use configured";
const CHOICE_DISMISS = "Dismiss";
// const CHOICE_AND_VALUES = [ // const CHOICE_AND_VALUES = [
// [CHOICE_USE_REMOTE, preferred], // [CHOICE_USE_REMOTE, preferred],
// [CHOICE_DISMISS, false]] // [CHOICE_DISMISS, false]]
const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS]; const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS];
const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, { const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
title: "Use Remote Configuration", title: $msg("TweakMismatchResolve.Title.UseRemoteConfig"),
timeout: 0, timeout: 0,
defaultAction: CHOICE_DISMISS, defaultAction: CHOICE_DISMISS,
}); });

View File

@@ -1,4 +1,4 @@
import { normalizePath, TFile, TFolder, type ListedFiles } from "obsidian"; import { TFile, TFolder, type ListedFiles } from "obsidian";
import { SerializedFileAccess } from "./storageLib/SerializedFileAccess"; import { SerializedFileAccess } from "./storageLib/SerializedFileAccess";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
@@ -60,7 +60,23 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
if (file instanceof TFile) { if (file instanceof TFile) {
return this.vaultAccess.vaultModify(file, data, opt); return this.vaultAccess.vaultModify(file, data, opt);
} else if (file === null) { } else if (file === null) {
return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile; if (!path.endsWith(".md")) {
// Very rare case, we encountered this case with `writing-goals-history.csv` file.
// Indeed, that file not appears in the File Explorer, but it exists in the vault.
// Hence, we cannot retrieve the file from the vault by getAbstractFileByPath, and we cannot write it via vaultModify.
// It makes `File already exists` error.
// Therefore, we need to write it via adapterWrite.
// Maybe there are others like this, so I will write it via adapterWrite.
// This is a workaround for the issue, but I don't know if this is the right solution.
// (So limits to non-md files).
// Has Obsidian been patched?, anyway, writing directly might be a safer approach.
// However, does changes of that file trigger file-change event?
await this.vaultAccess.adapterWrite(path, data, opt);
// For safety, check existence
return await this.vaultAccess.adapterExists(path);
} else {
return (await this.vaultAccess.vaultCreate(path, data, opt)) instanceof TFile;
}
} else { } else {
this._log(`Could not write file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE); this._log(`Could not write file (Possibly already exists as a folder): ${path}`, LOG_LEVEL_VERBOSE);
return false; return false;
@@ -158,8 +174,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
} }
} }
triggerFileEvent(event: string, path: string): void { triggerFileEvent(event: string, path: string): void {
// this.app.vault.trigger("file-change", path); const file = this.vaultAccess.getAbstractFileByPath(path);
this.vaultAccess.trigger(event, this.vaultAccess.getAbstractFileByPath(normalizePath(path))); if (file === null) return;
this.vaultAccess.trigger(event, file);
} }
async triggerHiddenFile(path: string): Promise<void> { async triggerHiddenFile(path: string): Promise<void> {
//@ts-ignore internal function //@ts-ignore internal function
@@ -258,9 +275,9 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
} }
return files as FilePath[]; return files as FilePath[];
} }
touched(file: UXFileInfoStub | FilePathWithPrefix): void { async touched(file: UXFileInfoStub | FilePathWithPrefix): Promise<void> {
const path = typeof file === "string" ? file : file.path; const path = typeof file === "string" ? file : file.path;
this.vaultAccess.touch(path as FilePath); await this.vaultAccess.touch(path as FilePath);
} }
recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean { recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean {
const xFile = typeof file === "string" ? (this.vaultAccess.getAbstractFileByPath(file) as TFile) : file; const xFile = typeof file === "string" ? (this.vaultAccess.getAbstractFileByPath(file) as TFile) : file;

View File

@@ -199,9 +199,15 @@ export class SerializedFileAccess {
touchedFiles: string[] = []; touchedFiles: string[] = [];
touch(file: TFile | FilePath) { _statInternal(file: FilePath) {
const f = file instanceof TFile ? file : (this.getAbstractFileByPath(file) as TFile); return this.app.vault.adapter.stat(file);
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`; }
async touch(file: TFile | FilePath) {
const path = file instanceof TFile ? (file.path as FilePath) : file;
const statOrg = file instanceof TFile ? file.stat : await this._statInternal(path);
const stat = statOrg || { mtime: 0, size: 0 };
const key = `${path}-${stat.mtime}-${stat.size}`;
this.touchedFiles.unshift(key); this.touchedFiles.unshift(key);
this.touchedFiles = this.touchedFiles.slice(0, 100); this.touchedFiles = this.touchedFiles.slice(0, 100);
} }

View File

@@ -1,4 +1,4 @@
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js"; import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js";
import { type ObsidianLiveSyncSettings } from "../../lib/src/common/types.js"; import { type ObsidianLiveSyncSettings } from "../../lib/src/common/types.js";
import { import {
EVENT_REQUEST_OPEN_P2P, EVENT_REQUEST_OPEN_P2P,
@@ -25,7 +25,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
} }
const issues = Object.entries(r.rules); const issues = Object.entries(r.rules);
if (issues.length == 0) { if (issues.length == 0) {
this._log($msg("Doctor.Message.NoIssues"), LOG_LEVEL_NOTICE); this._log(
$msg("Doctor.Message.NoIssues"),
activateReason !== "updated" ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
return; return;
} else { } else {
const OPT_YES = `${$msg("Doctor.Button.Yes")}` as const; const OPT_YES = `${$msg("Doctor.Button.Yes")}` as const;
@@ -68,7 +71,7 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
[RuleLevel.Must]: $msg("Doctor.Level.Must"), [RuleLevel.Must]: $msg("Doctor.Level.Must"),
}; };
const level = value.level ? levelMap[value.level] : "Unknown"; const level = value.level ? levelMap[value.level] : "Unknown";
const options = [OPT_FIX]; const options = [OPT_FIX] as [typeof OPT_FIX | typeof OPT_SKIP | typeof OPT_FIXBUTNOREBUILD];
if ((!skipRebuild && value.requireRebuild) || value.requireRebuildLocal) { if ((!skipRebuild && value.requireRebuild) || value.requireRebuildLocal) {
options.push(OPT_FIXBUTNOREBUILD); options.push(OPT_FIXBUTNOREBUILD);
} }

View File

@@ -1614,8 +1614,33 @@ The pane also can be launched by \`P2P Replicator\` command from the Command Pal
const newTweaks = const newTweaks =
await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting); await this.plugin.$$checkAndAskUseRemoteConfiguration(trialSetting);
if (newTweaks.result !== false) { if (newTweaks.result !== false) {
this.editingSettings = { ...this.editingSettings, ...newTweaks.result }; if (this.inWizard) {
this.requestUpdate(); this.editingSettings = { ...this.editingSettings, ...newTweaks.result };
this.requestUpdate();
return;
} else {
this.closeSetting();
this.plugin.settings = { ...this.plugin.settings, ...newTweaks.result };
if (newTweaks.requireFetch) {
if (
(await this.plugin.confirm.askYesNoDialog(
$msg("SettingTab.Message.AskRebuild"),
{
defaultOption: "Yes",
}
)) == "no"
) {
await this.plugin.$$saveSettingData();
return;
}
await this.plugin.$$saveSettingData();
await this.plugin.rebuilder.scheduleFetch();
await this.plugin.$$scheduleAppReload();
return;
} else {
await this.plugin.$$saveSettingData();
}
}
} }
}) })
); );

View File

@@ -38,7 +38,7 @@ export interface StorageAccess {
getFiles(): UXFileInfoStub[]; getFiles(): UXFileInfoStub[];
getFileNames(): FilePathWithPrefix[]; getFileNames(): FilePathWithPrefix[];
touched(file: UXFileInfoStub | FilePathWithPrefix): void; touched(file: UXFileInfoStub | FilePathWithPrefix): Promise<void>;
recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean; recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): boolean;
clearTouched(): void; clearTouched(): void;