1
0
mirror of https://github.com/vrtmrz/obsidian-livesync.git synced 2025-08-10 22:11:45 +02:00

Add translation ids

This commit is contained in:
CRuiz
2025-01-22 13:41:18 -06:00
parent f2b667d75e
commit 73782c5389
15 changed files with 506 additions and 453 deletions

93
README_es.md Normal file
View File

@@ -0,0 +1,93 @@
<!-- For translation: 20240227r0 -->
# Self-hosted LiveSync
[Documentación en inglés](./README_ja.md) - [Documentación en japonés](./README_ja.md) - [Documentación en chino](./README_cn.md).
Self-hosted LiveSync es un plugin de sincronización implementado por la comunidad, disponible en todas las plataformas compatibles con Obsidian y utiliza CouchDB o Almacenamiento de Objetos (por ejemplo, MinIO, S3, R2, etc.) como servidor.
![Demostración de Obsidian Live Sync](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
Nota: Este plugin no puede sincronizarse con el "Obsidian Sync" oficial.
## Características
- Sincroniza bóvedas de manera eficiente con menos tráfico.
- Buen manejo de modificaciones en conflicto.
- Fusión automática para conflictos simples.
- Uso de soluciones de código abierto para el servidor.
- Pueden usarse soluciones compatibles.
- Soporte de cifrado de extremo a extremo.
- Sincronización de configuraciones, fragmentos, temas y complementos a través de [Sincronización de personalización \(Beta\)](#customization-sync) o [Sincronización de archivos ocultos](#hiddenfilesync)
- WebClip de [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
Este plugin puede ser útil para investigadores, ingenieros y desarrolladores que necesitan mantener sus notas totalmente autoalojadas por razones de seguridad, o para aquellos que deseen tener la tranquilidad de saber que sus notas son totalmente privadas.
>[!IMPORTANTE]
> - Antes de instalar o actualizar este plugin, realice un respaldo de su bóveda.
> - No active este plugin junto con otra solución de sincronización al mismo tiempo (incluyendo iCloud y Obsidian Sync).
> - Este es un plugin de sincronización, no una solución de respaldo. No confíe en él para realizar respaldos.
## Cómo usar
### Configuración en 3 minutos - CouchDB en fly.io
**Recomendado para principiantes**
[![Configuración de LiveSync en Fly.io 2024 usando Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](https://www.youtube.com/watch?v=7sa_I1832Xc)
1. [Configurar CouchDB en fly.io](docs/setup_flyio_es.md)
2. Configurar el plugin en [Configuración rápida](docs/quick_setup_es.md)
### Configuración manual
1. Configurar el servidor
1. [Configurar CouchDB en fly.io](docs/setup_flyio_es.md)
2. [Configurar su CouchDB](docs/setup_own_server_es.md)
2. Configura el plugin en [Configuración rápida](docs/quick_setup_es.md)
> [!CONSEJO]
> Actualmente, fly.io ya no es gratuito. Afortunadamente, aunque hay algunos problemas, aún podemos usar IBM Cloudant. Aquí está como [Configurar IBM Cloudant](docs/setup_cloudant.md). ¡Se actualizará pronto!
## Información en la barra de estado
El estado de sincronización se muestra en la barra de estado con los siguientes iconos.
- Indicador de actividad
- 📲 Solicitud de red
- Estado
- ⏹️ Detenido
- 💤 LiveSync activado. Esperando cambios
- ⚡️ Sincronización en progreso
- ⚠ Ocurrió un error
- Indicador estadístico
- ↑ Chunks y metadatos subidos
- ↓ Chunks y metadatos descargados
- Indicador de progreso
- 📥 Elementos transferidos sin procesar
- 📄 Operación de base de datos en curso
- 💾 Procesos de escritura en almacenamiento en curso
- ⏳ Procesos de lectura en almacenamiento en curso
- 🛫 Procesos de lectura en almacenamiento pendientes
- 📬 Procesos de lectura en almacenamiento por lotes
- ⚙️ Procesos de almacenamiento de archivos ocultos en curso o pendientes
- 🧩 Chunks en espera
- 🔌 Elementos de personalización en curso (Configuración, fragmentos y plugins)
Para prevenir la corrupción de archivos y bases de datos, antes de detener Obsidian espere hasta que todos los indicadores de progreso hayan desaparecido (el plugin también intentará reanudar, sin embargo). Especialmente en caso de que haya eliminado o renombrado archivos.
## Consejos y Solución de Problemas
Si tienes problemas para hacer funcionar el plugin, consulta: [Consejos y solución de problemas](docs/troubleshooting_es.md).
## Agradecimientos
El proyecto ha progresado y mantenido en armonía gracias a:
- Muchos [Colaboradores](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors)
- Muchos [Patrocinadores de GitHub](https://github.com/sponsors/vrtmrz#sponsors)
- Programas comunitarios de JetBrains / Soporte para Proyectos de Código Abierto <img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo." height="24">
Que aquellos que han contribuido sean honrados y recordados por su amabilidad y generosidad.
## Licencia
Licenciado bajo la Licencia MIT.

View File

@@ -25,10 +25,10 @@ npm run buildDev
## Make messages to be translated ## Make messages to be translated
1. Find the message that you want to be translated. 1. Find the message that you want to be translated.
2. Change the literal to a tagged template literal using `$f`, like below. 2. Change the literal to use `$tf`, like below.
```diff ```diff
- Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT); - Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT);
+ Logger($f`Could not determine passphrase to save data.json! You probably make the configuration sure again!`, LOG_LEVEL_URGENT); + Logger($tf('someKeyForPassphraseError'), LOG_LEVEL_URGENT);
``` ```
3. Make the PR to `https://github.com/vrtmrz/obsidian-livesync`. 3. Make the PR to `https://github.com/vrtmrz/obsidian-livesync`.
4. Follow the steps of "Add translations for already defined terms" to add the translations. 4. Follow the steps of "Add translations for already defined terms" to add the translations.

View File

@@ -71,6 +71,7 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts"; import type { IObsidianModule } from "../../modules/AbstractObsidianModule.ts";
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts"; import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
import { PluginDialogModal } from "./PluginDialogModal.ts"; import { PluginDialogModal } from "./PluginDialogModal.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
const d = "\u200b"; const d = "\u200b";
const d2 = "\n"; const d2 = "\n";
@@ -446,7 +447,7 @@ export class ConfigSync extends LiveSyncCommands implements IObsidianModule {
this.showPluginSyncModal(); this.showPluginSyncModal();
}, },
}); });
this.addRibbonIcon("custom-sync", "Show Customization sync", () => { this.addRibbonIcon("custom-sync", $tf("cmdConfigSync.showCustomizationSync"), () => {
this.showPluginSyncModal(); this.showPluginSyncModal();
}).addClass("livesync-ribbon-showcustom"); }).addClass("livesync-ribbon-showcustom");
eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => this.showPluginSyncModal()); eventHub.onEvent(EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, () => this.showPluginSyncModal());

View File

@@ -211,7 +211,7 @@ export class ModuleFileHandler extends AbstractModule implements ICoreModule {
? await this.db.fetchEntryMeta(entryInfo, undefined, true) ? await this.db.fetchEntryMeta(entryInfo, undefined, true)
: await this.db.fetchEntryMeta(entryInfo.path, undefined, true); : await this.db.fetchEntryMeta(entryInfo.path, undefined, true);
if (!docEntry) { if (!docEntry) {
this._log(`File ${file?.path} is not exist on the database`, LOG_LEVEL_VERBOSE); this._log(`File ${entryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE);
return false; return false;
} }
const path = getPath(docEntry); const path = getPath(docEntry);

View File

@@ -1,4 +1,4 @@
import { $f } from "../../lib/src/common/i18n"; import { $tf } from "../../lib/src/common/i18n";
import { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts"; import { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
import { initializeStores } from "../../common/stores.ts"; import { initializeStores } from "../../common/stores.ts";
import { AbstractModule } from "../AbstractModule.ts"; import { AbstractModule } from "../AbstractModule.ts";
@@ -13,7 +13,7 @@ export class ModuleLocalDatabaseObsidian extends AbstractModule implements ICore
await this.localDatabase.close(); await this.localDatabase.close();
} }
const vaultName = this.core.$$getVaultName(); const vaultName = this.core.$$getVaultName();
this._log($f`Waiting for ready...`); this._log($tf("moduleLocalDatabase.logWaitingForReady"));
this.core.localDatabase = new LiveSyncLocalDB(vaultName, this.core); this.core.localDatabase = new LiveSyncLocalDB(vaultName, this.core);
initializeStores(vaultName); initializeStores(vaultName);
return await this.localDatabase.initializeDatabase(); return await this.localDatabase.initializeDatabase();

View File

@@ -2,34 +2,24 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-w
import { AbstractModule } from "../AbstractModule.ts"; import { AbstractModule } from "../AbstractModule.ts";
import { sizeToHumanReadable } from "octagonal-wheels/number"; import { sizeToHumanReadable } from "octagonal-wheels/number";
import type { ICoreModule } from "../ModuleTypes.ts"; import type { ICoreModule } from "../ModuleTypes.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
export class ModuleCheckRemoteSize extends AbstractModule implements ICoreModule { export class ModuleCheckRemoteSize extends AbstractModule implements ICoreModule {
async $allScanStat(): Promise<boolean> { async $allScanStat(): Promise<boolean> {
this._log(`Checking storage sizes`, LOG_LEVEL_VERBOSE); this._log($tf("moduleCheckRemoteSize.logCheckingStorageSizes"), LOG_LEVEL_VERBOSE);
if (this.settings.notifyThresholdOfRemoteStorageSize < 0) { if (this.settings.notifyThresholdOfRemoteStorageSize < 0) {
const message = `We can set a maximum database capacity warning, **to take action before running out of space on the remote storage**. const message = $tf("moduleCheckRemoteSize.msgSetDBCapacity");
Do you want to enable this? const ANSWER_0 = $tf("moduleCheckRemoteSize.optionNoWarn");
const ANSWER_800 = $tf("moduleCheckRemoteSize.option800MB");
> [!MORE]- const ANSWER_2000 = $tf("moduleCheckRemoteSize.option2GB");
> - 0: Do not warn about storage size. const ASK_ME_NEXT_TIME = $tf("moduleCheckRemoteSize.optionAskMeLater");
> This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.
> - 800: Warn if the remote storage size exceeds 800MB.
> This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.
> - 2000: Warn if the remote storage size exceeds 2GB.
If we have reached the limit, we will be asked to enlarge the limit step by step.
`;
const ANSWER_0 = "No, never warn please";
const ANSWER_800 = "800MB (Cloudant, fly.io)";
const ANSWER_2000 = "2GB (Standard)";
const ASK_ME_NEXT_TIME = "Ask me later";
const ret = await this.core.confirm.askSelectStringDialogue( const ret = await this.core.confirm.askSelectStringDialogue(
message, message,
[ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME], [ANSWER_0, ANSWER_800, ANSWER_2000, ASK_ME_NEXT_TIME],
{ {
defaultAction: ASK_ME_NEXT_TIME, defaultAction: ASK_ME_NEXT_TIME,
title: "Setting up database size notification", title: $tf("moduleCheckRemoteSize.titleDatabaseSizeNotify"),
timeout: 40, timeout: 40,
} }
); );
@@ -51,40 +41,28 @@ If we have reached the limit, we will be asked to enlarge the limit step by step
if (estimatedSize) { if (estimatedSize) {
const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024; const maxSize = this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024;
if (estimatedSize > maxSize) { if (estimatedSize > maxSize) {
const message = `**Your database is getting larger!** But do not worry, we can address it now. The time before running out of space on the remote storage. const message = $tf("moduleCheckRemoteSize.msgDatabaseGrowing", {
estimatedSize: sizeToHumanReadable(estimatedSize),
| Measured size | Configured size | maxSize: sizeToHumanReadable(maxSize),
| --- | --- | });
| ${sizeToHumanReadable(estimatedSize)} | ${sizeToHumanReadable(maxSize)} |
> [!MORE]-
> If you have been using it for many years, there may be unreferenced chunks - that is, garbage - accumulating in the database. Therefore, we recommend rebuilding everything. It will probably become much smaller.
>
> If the volume of your vault is simply increasing, it is better to rebuild everything after organizing the files. Self-hosted LiveSync does not delete the actual data even if you delete it to speed up the process. It is roughly [documented](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).
>
> If you don't mind the increase, you can increase the notification limit by 100MB. This is the case if you are running it on your own server. However, it is better to rebuild everything from time to time.
>
> [!WARNING]
> If you perform rebuild everything, make sure all devices are synchronised. The plug-in will merge as much as possible, though.
\n`;
const newMax = ~~(estimatedSize / 1024 / 1024) + 100; const newMax = ~~(estimatedSize / 1024 / 1024) + 100;
const ANSWER_ENLARGE_LIMIT = `increase to ${newMax}MB`; const ANSWER_ENLARGE_LIMIT = $tf("moduleCheckRemoteSize.optionIncreaseLimit", {
const ANSWER_REBUILD = "Rebuild Everything Now"; newMax: newMax.toString(),
const ANSWER_IGNORE = "Dismiss"; });
const ANSWER_REBUILD = $tf("moduleCheckRemoteSize.optionRebuildAll");
const ANSWER_IGNORE = $tf("moduleCheckRemoteSize.optionDismiss");
const ret = await this.core.confirm.askSelectStringDialogue( const ret = await this.core.confirm.askSelectStringDialogue(
message, message,
[ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE], [ANSWER_ENLARGE_LIMIT, ANSWER_REBUILD, ANSWER_IGNORE],
{ {
defaultAction: ANSWER_IGNORE, defaultAction: ANSWER_IGNORE,
title: "Remote storage size exceeded the limit", title: $tf("moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded"),
timeout: 60, timeout: 60,
} }
); );
if (ret == ANSWER_REBUILD) { if (ret == ANSWER_REBUILD) {
const ret = await this.core.confirm.askYesNoDialog( const ret = await this.core.confirm.askYesNoDialog(
"This may take a bit of a long time. Do you really want to rebuild everything now?", $tf("moduleCheckRemoteSize.msgConfirmRebuild"),
{ defaultOption: "No" } { defaultOption: "No" }
); );
if (ret == "yes") { if (ret == "yes") {
@@ -95,7 +73,9 @@ If we have reached the limit, we will be asked to enlarge the limit step by step
} else if (ret == ANSWER_ENLARGE_LIMIT) { } else if (ret == ANSWER_ENLARGE_LIMIT) {
this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100; this.settings.notifyThresholdOfRemoteStorageSize = ~~(estimatedSize / 1024 / 1024) + 100;
this._log( this._log(
`Threshold has been enlarged to ${this.settings.notifyThresholdOfRemoteStorageSize}MB`, $tf("moduleCheckRemoteSize.logThresholdEnlarged", {
size: this.settings.notifyThresholdOfRemoteStorageSize.toString(),
}),
LOG_LEVEL_NOTICE LOG_LEVEL_NOTICE
); );
await this.core.saveSettings(); await this.core.saveSettings();
@@ -104,11 +84,18 @@ If we have reached the limit, we will be asked to enlarge the limit step by step
} }
this._log( this._log(
`Remote storage size: ${sizeToHumanReadable(estimatedSize)} exceeded ${sizeToHumanReadable(this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024)} `, $tf("moduleCheckRemoteSize.logExceededWarning", {
measuredSize: sizeToHumanReadable(estimatedSize),
notifySize: sizeToHumanReadable(
this.settings.notifyThresholdOfRemoteStorageSize * 1024 * 1024
),
}),
LOG_LEVEL_INFO LOG_LEVEL_INFO
); );
} else { } else {
this._log(`Remote storage size: ${sizeToHumanReadable(estimatedSize)}`, LOG_LEVEL_INFO); this._log($tf("moduleCheckRemoteSize.logCurrentStorageSize", {
measuredSize: sizeToHumanReadable(estimatedSize),
}), LOG_LEVEL_INFO);
} }
} }
} }

View File

@@ -1,3 +1,4 @@
// ModuleInputUIObsidian.ts
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task"; import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts"; import { disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject } from "../../common/utils.ts";
@@ -10,6 +11,7 @@ import {
} from "./UILib/dialogs.ts"; } from "./UILib/dialogs.ts";
import { Notice } from "../../deps.ts"; import { Notice } from "../../deps.ts";
import type { Confirm } from "../interfaces/Confirm.ts"; import type { Confirm } from "../interfaces/Confirm.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
// This module cannot be a common module because it depends on Obsidian's API. // This module cannot be a common module because it depends on Obsidian's API.
// However, we have to make compatible one for other platform. // However, we have to make compatible one for other platform.
@@ -23,23 +25,28 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
askYesNo(message: string): Promise<"yes" | "no"> { askYesNo(message: string): Promise<"yes" | "no"> {
return askYesNo(this.app, message); return askYesNo(this.app, message);
} }
askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> { askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> {
return askString(this.app, title, key, placeholder, isPassword); return askString(this.app, title, key, placeholder, isPassword);
} }
async askYesNoDialog( async askYesNoDialog(
message: string, message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" } opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = {}
): Promise<"yes" | "no"> { ): Promise<"yes" | "no"> {
const defaultTitle = $tf("moduleInputUIObsidian.defaultTitleConfirmation");
const yesLabel = $tf("moduleInputUIObsidian.optionYes");
const noLabel = $tf("moduleInputUIObsidian.optionNo");
const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel;
const ret = await confirmWithMessageWithWideButton( const ret = await confirmWithMessageWithWideButton(
this.plugin, this.plugin,
opt.title || "Confirmation", opt.title || defaultTitle,
message, message,
["Yes", "No"], [yesLabel, noLabel],
opt.defaultOption ?? "No", defaultOption,
opt.timeout opt.timeout
); );
return ret == "Yes" ? "yes" : "no"; return ret === yesLabel ? "yes" : "no";
} }
askSelectString(message: string, items: string[]): Promise<string> { askSelectString(message: string, items: string[]): Promise<string> {
@@ -51,9 +58,10 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
buttons: string[], buttons: string[],
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number } opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false> { ): Promise<(typeof buttons)[number] | false> {
const defaultTitle = $tf("moduleInputUIObsidian.defaultTitleSelect");
return confirmWithMessageWithWideButton( return confirmWithMessageWithWideButton(
this.plugin, this.plugin,
opt.title || "Select", opt.title || defaultTitle,
message, message,
buttons, buttons,
opt.defaultAction, opt.defaultAction,
@@ -91,6 +99,7 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
}); });
}); });
} }
confirmWithMessage( confirmWithMessage(
title: string, title: string,
contentMd: string, contentMd: string,

View File

@@ -8,16 +8,12 @@ import {
} from "../../common/events.ts"; } from "../../common/events.ts";
import { AbstractModule } from "../AbstractModule.ts"; import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts"; import type { ICoreModule } from "../ModuleTypes.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
const URI_DOC = "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use";
export class ModuleMigration extends AbstractModule implements ICoreModule { export class ModuleMigration extends AbstractModule implements ICoreModule {
async migrateDisableBulkSend() { async migrateDisableBulkSend() {
if (this.settings.sendChunksBulk) { if (this.settings.sendChunksBulk) {
this._log( this._log($tf('moduleMigration.logBulkSendCorrupted'), LOG_LEVEL_NOTICE);
"Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled.",
LOG_LEVEL_NOTICE
);
this.settings.sendChunksBulk = false; this.settings.sendChunksBulk = false;
this.settings.sendChunksBulkMaxSize = 1; this.settings.sendChunksBulkMaxSize = 1;
await this.saveSettings(); await this.saveSettings();
@@ -28,7 +24,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
const current = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; const current = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
// Check each migrations(old -> current) // Check each migrations(old -> current)
if (!(await this.migrateToCaseInsensitive(old, current))) { if (!(await this.migrateToCaseInsensitive(old, current))) {
this._log(`Migration failed or cancelled from ${old} to ${current}`, LOG_LEVEL_NOTICE); this._log($tf('moduleMigration.logMigrationFailed', {
old: old.toString(),
current: current.toString()
}), LOG_LEVEL_NOTICE);
return; return;
} }
} }
@@ -68,10 +67,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
remoteChecked = true; remoteChecked = true;
} }
} else { } else {
this._log("Failed to fetch remote tweak values", LOG_LEVEL_INFO); this._log($tf('moduleMigration.logFetchRemoteTweakFailed'), LOG_LEVEL_INFO);
} }
} catch (ex) { } catch (ex) {
this._log("Could not get remote tweak values", LOG_LEVEL_INFO); this._log($tf('moduleMigration.logRemoteTweakUnavailable'), LOG_LEVEL_INFO);
this._log(ex, LOG_LEVEL_VERBOSE); this._log(ex, LOG_LEVEL_VERBOSE);
} }
@@ -82,27 +81,21 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
this.settings.handleFilenameCaseSensitive = true; this.settings.handleFilenameCaseSensitive = true;
this.settings.doNotUseFixedRevisionForChunks = true; this.settings.doNotUseFixedRevisionForChunks = true;
this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE; this.settings.settingVersion = SETTING_VERSION_SUPPORT_CASE_INSENSITIVE;
this._log(`Migrated to db:${current} with the same behaviour as before`, LOG_LEVEL_INFO); this._log($tf('moduleMigration.logMigratedSameBehaviour', {
current: current.toString()
}), LOG_LEVEL_INFO);
await this.saveSettings(); await this.saveSettings();
return true; return true;
} }
const message = `As you may already know, the self-hosted LiveSync has changed its default behaviour and database structure. const message = $tf('moduleMigration.msgFetchRemoteAgain');
const OPTION_FETCH = $tf('moduleMigration.optionYesFetchAgain');
And thankfully, with your time and efforts, the remote database appears to have already been migrated. Congratulations! const DISMISS = $tf('moduleMigration.optionNoAskAgain');
However, we need a bit more. The configuration of this device is not compatible with the remote database. We will need to fetch the remote database again. Should we fetch from the remote again now?
___Note: We cannot synchronise until the configuration has been changed and the database has been fetched again.___
___Note2: The chunks are completely immutable, we can fetch only the metadata and difference.___
`;
const OPTION_FETCH = "Yes, fetch again";
const DISMISS = "No, please ask again";
const options = [OPTION_FETCH, DISMISS]; const options = [OPTION_FETCH, DISMISS];
const ret = await this.core.confirm.confirmWithMessage( const ret = await this.core.confirm.confirmWithMessage(
"Case Sensitivity", $tf('moduleMigration.titleCaseSensitivity'),
message, message,
options, options,
"No, please ask again", DISMISS,
40 40
); );
if (ret == OPTION_FETCH) { if (ret == OPTION_FETCH) {
@@ -114,7 +107,7 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an
await this.core.rebuilder.scheduleFetch(); await this.core.rebuilder.scheduleFetch();
return; return;
} catch (ex) { } catch (ex) {
this._log("Failed to create redflag2", LOG_LEVEL_VERBOSE); this._log($tf('moduleMigration.logRedflag2CreationFail'), LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE); this._log(ex, LOG_LEVEL_VERBOSE);
} }
return false; return false;
@@ -123,33 +116,19 @@ ___Note2: The chunks are completely immutable, we can fetch only the metadata an
} }
} }
const ENABLE_BOTH = "Enable both"; const ENABLE_BOTH = $tf('moduleMigration.optionEnableBoth');
const ENABLE_FILENAME_CASE_INSENSITIVE = "Enable only #1"; const ENABLE_FILENAME_CASE_INSENSITIVE = $tf('moduleMigration.optionEnableFilenameCaseInsensitive');
const ENABLE_FIXED_REVISION_FOR_CHUNKS = "Enable only #2"; const ENABLE_FIXED_REVISION_FOR_CHUNKS = $tf('moduleMigration.optionEnableFixedRevisionForChunks');
const ADJUST_TO_REMOTE = "Adjust to remote"; const ADJUST_TO_REMOTE = $tf('moduleMigration.optionAdjustRemote');
const DISMISS = "Decide it later"; const KEEP = $tf('moduleMigration.optionKeepPreviousBehaviour');
const KEEP = "Keep previous behaviour"; const DISMISS = $tf('moduleMigration.optionDecideLater');
const message = `Since v0.23.21, the self-hosted LiveSync has changed the default behaviour and database structure. The following changes have been made: const message = $tf('moduleMigration.msgSinceV02321');
1. **Case sensitivity of filenames**
The handling of filenames is now case-insensitive. This is a beneficial change for most platforms, other than Linux and iOS, which do not manage filename case sensitivity effectively.
(On These, a warning will be displayed for files with the same name but different cases).
2. **Revision handling of the chunks**
Chunks are immutable, which allows their revisions to be fixed. This change will enhance the performance of file saving.
___However, to enable either of these changes, both remote and local databases need to be rebuilt. This process takes a few minutes, and we recommend doing it when you have ample time.___
- If you wish to maintain the previous behaviour, you can skip this process by using \`${KEEP}\`.
- If you do not have enough time, please choose \`${DISMISS}\`. You will be prompted again later.
- If you have rebuilt the database on another device, please select \`${DISMISS}\` and try synchronizing again. Since a difference has been detected, you will be prompted again.
`;
const options = [ENABLE_BOTH, ENABLE_FILENAME_CASE_INSENSITIVE, ENABLE_FIXED_REVISION_FOR_CHUNKS]; const options = [ENABLE_BOTH, ENABLE_FILENAME_CASE_INSENSITIVE, ENABLE_FIXED_REVISION_FOR_CHUNKS];
if (remoteChecked) { if (remoteChecked) {
options.push(ADJUST_TO_REMOTE); options.push(ADJUST_TO_REMOTE);
} }
options.push(KEEP, DISMISS); options.push(KEEP, DISMISS);
const ret = await this.core.confirm.confirmWithMessage("Case Sensitivity", message, options, DISMISS, 40); const ret = await this.core.confirm.confirmWithMessage($tf('moduleMigration.titleCaseSensitivity'), message, options, DISMISS, 40);
console.dir(ret); console.dir(ret);
switch (ret) { switch (ret) {
case ENABLE_BOTH: case ENABLE_BOTH:
@@ -181,20 +160,14 @@ ___However, to enable either of these changes, both remote and local databases n
} }
async initialMessage() { async initialMessage() {
const message = `Your device has **not been set up yet**. Let me guide you through the setup process. const message = $tf('moduleMigration.msgInitialSetup', {
URI_DOC: $tf('moduleMigration.docUri'),
Please keep in mind that every dialogue content can be copied to the clipboard. If you need to refer to it later, you can paste it into a note in Obsidian. You can also translate it into your language using a translation tool. });
const USE_SETUP = $tf('moduleMigration.optionHaveSetupUri');
First, do you have **Setup URI**? const NEXT = $tf('moduleMigration.optionNoSetupUri');
Note: If you do not know what it is, please refer to the [documentation](${URI_DOC}).
`;
const USE_SETUP = "Yes, I have";
const NEXT = "No, I do not have";
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_SETUP, NEXT], { const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_SETUP, NEXT], {
title: "Welcome to Self-hosted LiveSync", title: $tf('moduleMigration.titleWelcome'),
defaultAction: USE_SETUP, defaultAction: USE_SETUP,
}); });
if (ret === USE_SETUP) { if (ret === USE_SETUP) {
@@ -207,17 +180,13 @@ Note: If you do not know what it is, please refer to the [documentation](${URI_D
} }
async askAgainForSetupURI() { async askAgainForSetupURI() {
const message = `We strongly recommend that you generate a set-up URI and use it. const message = $tf('moduleMigration.msgRecommendSetupUri');
If you do not have knowledge about it, please refer to the [documentation](${URI_DOC}) (Sorry again, but it is important). const USE_MINIMAL = $tf('moduleMigration.optionSetupWizard');
const USE_SETUP = $tf('moduleMigration.optionManualSetup');
How do you want to set it up manually?`; const NEXT = $tf('moduleMigration.optionRemindNextLaunch');
const USE_MINIMAL = "Take me into the setup wizard";
const USE_SETUP = "Set it up all manually";
const NEXT = "Remind me at the next launch";
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], { const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], {
title: "Recommendation to use Setup URI", title: $tf('moduleMigration.titleRecommendSetupUri'),
defaultAction: USE_MINIMAL, defaultAction: USE_MINIMAL,
}); });
if (ret === USE_MINIMAL) { if (ret === USE_MINIMAL) {
@@ -235,7 +204,7 @@ How do you want to set it up manually?`;
async $everyOnFirstInitialize(): Promise<boolean> { async $everyOnFirstInitialize(): Promise<boolean> {
if (!this.localDatabase.isReady) { if (!this.localDatabase.isReady) {
this._log(`Something went wrong! The local database is not ready`, LOG_LEVEL_NOTICE); this._log($tf('moduleMigration.logLocalDatabaseNotReady'), LOG_LEVEL_NOTICE);
return false; return false;
} }
if (this.settings.isConfigured) { if (this.settings.isConfigured) {
@@ -245,10 +214,7 @@ How do you want to set it up manually?`;
if (!this.settings.isConfigured) { if (!this.settings.isConfigured) {
// Case sensitivity // Case sensitivity
if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) { if (!(await this.initialMessage()) || !(await this.askAgainForSetupURI())) {
this._log( this._log($tf('moduleMigration.logSetupCancelled'), LOG_LEVEL_NOTICE);
"The setup has been cancelled, Self-hosted LiveSync waiting for your setup!",
LOG_LEVEL_NOTICE
);
return false; return false;
} }
} }

View File

@@ -2,6 +2,7 @@ import { fireAndForget } from "octagonal-wheels/promises";
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts"; import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts"; import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule { export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> { $everyOnloadStart(): Promise<boolean> {
@@ -16,7 +17,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
</g>` </g>`
); );
this.addRibbonIcon("replicate", "Replicate", async () => { this.addRibbonIcon("replicate", $tf("moduleObsidianMenu.replicate"), async () => {
await this.core.$$replicate(true); await this.core.$$replicate(true);
}).addClass("livesync-ribbon-replicate"); }).addClass("livesync-ribbon-replicate");

View File

@@ -3,6 +3,7 @@
import { logMessages } from "../../../lib/src/mock_and_interop/stores"; import { logMessages } from "../../../lib/src/mock_and_interop/stores";
import { reactive, type ReactiveInstance } from "../../../lib/src/dataobject/reactive"; import { reactive, type ReactiveInstance } from "../../../lib/src/dataobject/reactive";
import { Logger } from "../../../lib/src/common/logger"; import { Logger } from "../../../lib/src/common/logger";
import { $tf as tf, currentLang as lang } from "../../../lib/src/common/i18n.ts";
let unsubscribe: () => void; let unsubscribe: () => void;
let messages = [] as string[]; let messages = [] as string[];
@@ -21,7 +22,7 @@
onMount(async () => { onMount(async () => {
const _logMessages = reactive(() => logMessages.value); const _logMessages = reactive(() => logMessages.value);
_logMessages.onChanged(updateLog); _logMessages.onChanged(updateLog);
Logger("Log window opened"); Logger(tf("logPane.logWindowOpened", {}, lang));
unsubscribe = () => _logMessages.offChanged(updateLog); unsubscribe = () => _logMessages.offChanged(updateLog);
}); });
onDestroy(() => { onDestroy(() => {
@@ -31,12 +32,21 @@
</script> </script>
<div class="logpane"> <div class="logpane">
<!-- <h1>Self-hosted LiveSync Log</h1> --> <!-- <h1>{tf("logPane.title", {}, lang)}</h1> -->
<div class="control"> <div class="control">
<div class="row"> <div class="row">
<label><input type="checkbox" bind:checked={wrapRight} /><span>Wrap</span></label> <label>
<label><input type="checkbox" bind:checked={autoScroll} /><span>Auto scroll</span></label> <input type="checkbox" bind:checked={wrapRight} />
<label><input type="checkbox" bind:checked={suspended} /><span>Pause</span></label> <span>{tf("logPane.wrap", {}, lang)}</span>
</label>
<label>
<input type="checkbox" bind:checked={autoScroll} />
<span>{tf("logPane.autoScroll", {}, lang)}</span>
</label>
<label>
<input type="checkbox" bind:checked={suspended} />
<span>{tf("logPane.pause", {}, lang)}</span>
</label>
</div> </div>
</div> </div>
<div class="log" bind:this={scroll}> <div class="log" bind:this={scroll}>

View File

@@ -1,6 +1,7 @@
import { ItemView, WorkspaceLeaf } from "obsidian"; import { ItemView, WorkspaceLeaf } from "obsidian";
import LogPaneComponent from "./LogPane.svelte"; import LogPaneComponent from "./LogPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts"; import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
export const VIEW_TYPE_LOG = "log-log"; export const VIEW_TYPE_LOG = "log-log";
//Log view //Log view
export class LogPaneView extends ItemView { export class LogPaneView extends ItemView {
@@ -24,7 +25,8 @@ export class LogPaneView extends ItemView {
} }
getDisplayText() { getDisplayText() {
return "Self-hosted LiveSync Log"; // TODO: This function is not reactive and does not update the title based on the current language
return $tf("logPane.title");
} }
async onOpen() { async onOpen() {

View File

@@ -26,6 +26,7 @@ import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/
import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts"; import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
import { serialized } from "octagonal-wheels/concurrency/lock"; import { serialized } from "octagonal-wheels/concurrency/lock";
import { $tf } from "src/lib/src/common/i18n.ts";
// This module cannot be a core module because it depends on the Obsidian UI. // This module cannot be a core module because it depends on the Obsidian UI.
@@ -292,7 +293,7 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
<path d="m106 346v44h70v-44zm45 16h-20v-8h20z"/> <path d="m106 346v44h70v-44zm45 16h-20v-8h20z"/>
</g>` </g>`
); );
this.addRibbonIcon("view-log", "Show log", () => { this.addRibbonIcon("view-log", $tf("moduleLog.showLog"), () => {
void this.core.$$showView(VIEW_TYPE_LOG); void this.core.$$showView(VIEW_TYPE_LOG);
}).addClass("livesync-ribbon-showlog"); }).addClass("livesync-ribbon-showlog");

View File

@@ -30,6 +30,7 @@ import {
type AllNumericItemKey, type AllNumericItemKey,
type AllBooleanItemKey, type AllBooleanItemKey,
} from "./settingConstants.ts"; } from "./settingConstants.ts";
import { $tf } from "src/lib/src/common/i18n.ts";
export class LiveSyncSetting extends Setting { export class LiveSyncSetting extends Setting {
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
@@ -86,7 +87,7 @@ export class LiveSyncSetting extends Setting {
autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption) { autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption) {
const conf = getConfig(key); const conf = getConfig(key);
if (!conf) { if (!conf) {
// throw new Error(`No such setting item :${key}`) // throw new Error($tf("liveSyncSetting.errorNoSuchSettingItem", { key }));
return; return;
} }
const name = `${conf.name}${statusDisplay(conf.status)}`; const name = `${conf.name}${statusDisplay(conf.status)}`;
@@ -216,7 +217,15 @@ export class LiveSyncSetting extends Setting {
text.inputEl.toggleClass("sls-item-invalid-value", false); text.inputEl.toggleClass("sls-item-invalid-value", false);
await this.commitValue(value); await this.commitValue(value);
} else { } else {
this.setTooltip(`The value should ${opt.clampMin || "~"} < value < ${opt.clampMax || "~"}`); this.setTooltip(
$tf(
"liveSyncSetting.valueShouldBeInRange",
{
min: opt.clampMin?.toString() || "~",
max: opt.clampMax?.toString() || "~",
}
)
);
text.inputEl.toggleClass("sls-item-invalid-value", true); text.inputEl.toggleClass("sls-item-invalid-value", true);
lastError = true; lastError = true;
return false; return false;
@@ -268,7 +277,7 @@ export class LiveSyncSetting extends Setting {
this.addButton((button) => { this.addButton((button) => {
this.applyButtonComponent = button; this.applyButtonComponent = button;
this.watchDirtyKeys = unique([...keys, ...this.watchDirtyKeys]); this.watchDirtyKeys = unique([...keys, ...this.watchDirtyKeys]);
button.setButtonText(text ?? "Apply"); button.setButtonText(text ?? $tf("liveSyncSettings.btnApply"));
button.onClick(async () => { button.onClick(async () => {
await LiveSyncSetting.env.saveSettings(keys); await LiveSyncSetting.env.saveSettings(keys);
LiveSyncSetting.env.reloadAllSettings(); LiveSyncSetting.env.reloadAllSettings();
@@ -363,7 +372,9 @@ export class LiveSyncSetting extends Setting {
} }
if (this.holdValue && this.selfKey) { if (this.holdValue && this.selfKey) {
const isDirty = LiveSyncSetting.env.isDirty(this.selfKey); const isDirty = LiveSyncSetting.env.isDirty(this.selfKey);
const alt = isDirty ? `Original: ${LiveSyncSetting.env.initialSettings![this.selfKey]}` : ""; const alt = isDirty
? $tf("liveSyncSetting.originalValue", { value: String(LiveSyncSetting.env.initialSettings?.[this.selfKey] ?? "") })
: "";
this.controlEl.toggleClass("sls-item-dirty", isDirty); this.controlEl.toggleClass("sls-item-dirty", isDirty);
if (!this.hasPassword) { if (!this.hasPassword) {
this.nameEl.toggleClass("sls-item-dirty-help", isDirty); this.nameEl.toggleClass("sls-item-dirty-help", isDirty);

View File

@@ -60,7 +60,7 @@ import {
getConfName, getConfName,
} from "./settingConstants.ts"; } from "./settingConstants.ts";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../../lib/src/common/rosetta.ts"; import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../../lib/src/common/rosetta.ts";
import { $t } from "../../../lib/src/common/i18n.ts"; import { $t, $tf } from "../../../lib/src/common/i18n.ts";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises"; import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises";
@@ -117,11 +117,11 @@ type OnSavedHandler<T extends AllSettingItemKey> = {
function getLevelStr(level: ConfigLevel) { function getLevelStr(level: ConfigLevel) {
return level == LEVEL_POWER_USER return level == LEVEL_POWER_USER
? " (Power User)" ? $tf("obsidianLiveSyncSettingTab.levelPowerUser")
: level == LEVEL_ADVANCED : level == LEVEL_ADVANCED
? " (Advanced)" ? $tf("obsidianLiveSyncSettingTab.levelAdvanced")
: level == LEVEL_EDGE_CASE : level == LEVEL_EDGE_CASE
? " (Edge Case)" ? $tf("obsidianLiveSyncSettingTab.levelEdgeCase")
: ""; : "";
} }
@@ -384,7 +384,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const status = await replicator.getRemoteStatus(trialSetting); const status = await replicator.getRemoteStatus(trialSetting);
if (status) { if (status) {
if (status.estimatedSize) { if (status.estimatedSize) {
Logger(`Estimated size: ${sizeToHumanReadable(status.estimatedSize)}`, LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logEstimatedSize", { size: sizeToHumanReadable(status.estimatedSize) }), LOG_LEVEL_NOTICE);
} }
} }
} }
@@ -464,9 +464,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
// And modified. // And modified.
this.plugin.confirm.askInPopup( this.plugin.confirm.askInPopup(
`config-reloaded-${k}`, `config-reloaded-${k}`,
`The setting "${getConfName(k as AllSettingItemKey)}" was modified from another device. Click {HERE} to reload settings. Click elsewhere to ignore changes`, $tf("obsidianLiveSyncSettingTab.msgSettingModified", { setting: getConfName(k as AllSettingItemKey) }),
(anchor) => { (anchor) => {
anchor.text = "HERE"; anchor.text = $tf("obsidianLiveSyncSettingTab.optionHere");
anchor.addEventListener("click", () => { anchor.addEventListener("click", () => {
this.refreshSetting(k as AllSettingItemKey); this.refreshSetting(k as AllSettingItemKey);
this.display(); this.display();
@@ -611,35 +611,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE); Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE);
return; return;
} }
const OPTION_FETCH = `Fetch from Remote`; const OPTION_FETCH = $tf("obsidianLiveSyncSettingTab.optionFetchFromRemote");
const OPTION_REBUILD_BOTH = `Rebuild Both from This Device`; const OPTION_REBUILD_BOTH = $tf("obsidianLiveSyncSettingTab.optionRebuildBoth");
const OPTION_ONLY_SETTING = `(Danger) Save Only Settings`; const OPTION_ONLY_SETTING = $tf("obsidianLiveSyncSettingTab.optionSaveOnlySettings");
const OPTION_CANCEL = `Cancel`; const OPTION_CANCEL = $tf("obsidianLiveSyncSettingTab.optionCancel");
const title = `Rebuild Required`; const title = $tf("obsidianLiveSyncSettingTab.titleRebuildRequired");
const note = `Rebuilding Databases are required to apply the changes.. Please select the method to apply the changes. const note = $tf("obsidianLiveSyncSettingTab.msgRebuildRequired", {
OPTION_REBUILD_BOTH,
<details> OPTION_FETCH,
<summary>Legends</summary> OPTION_ONLY_SETTING,
});
| Symbol | Meaning |
|: ------ :| ------- |
| ⇔ | Up to Date |
| ⇄ | Synchronise to balance |
| ⇐,⇒ | Transfer to overwrite |
| ⇠,⇢ | Transfer to overwrite from other side |
</details>
## ${OPTION_REBUILD_BOTH}
At a glance: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄
Reconstruct both the local and remote databases using existing files from this device.
This causes a lockout other devices, and they need to perform fetching.
## ${OPTION_FETCH}
At a glance: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄
Initialise the local database and reconstruct it using data fetched from the remote database.
This case includes the case which you have rebuilt the remote database.
## ${OPTION_ONLY_SETTING}
Store only the settings. **Caution: This may lead to data corruption**; database reconstruction is generally necessary.`;
const buttons = [ const buttons = [
OPTION_FETCH, OPTION_FETCH,
OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE, OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE,
@@ -651,7 +632,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
if (result == OPTION_FETCH) { if (result == OPTION_FETCH) {
if (!(await checkWorkingPassphrase())) { if (!(await checkWorkingPassphrase())) {
if ( if (
(await this.plugin.confirm.askYesNoDialog("Are you sure to proceed?", { (await this.plugin.confirm.askYesNoDialog($tf("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
defaultOption: "No", defaultOption: "No",
})) != "yes" })) != "yes"
) )
@@ -682,8 +663,8 @@ Store only the settings. **Caution: This may lead to data corruption**; database
{ cls: "sls-setting-menu-buttons" }, { cls: "sls-setting-menu-buttons" },
(el) => { (el) => {
el.addClass("wizardHidden"); el.addClass("wizardHidden");
el.createEl("label", { text: "Changes need to be applied!" }); el.createEl("label", { text: $tf("obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied") });
void this.addEl(el, "button", { text: "Apply", cls: "mod-warning" }, (buttonEl) => { void this.addEl(el, "button", { text: $tf("obsidianLiveSyncSettingTab.optionApply"), cls: "mod-warning" }, (buttonEl) => {
buttonEl.addEventListener("click", () => fireAndForget(async () => await confirmRebuild())); buttonEl.addEventListener("click", () => fireAndForget(async () => await confirmRebuild()));
}); });
}, },
@@ -840,15 +821,15 @@ Store only the settings. **Caution: This may lead to data corruption**; database
true true
); );
if (typeof db === "string") { if (typeof db === "string") {
Logger(`ERROR: Failed to check passphrase with the remote server: \n${db}.`, LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE);
return false; return false;
} else { } else {
if (await checkSyncInfo(db.db)) { if (await checkSyncInfo(db.db)) {
// Logger("Database connected", LOG_LEVEL_NOTICE); // Logger($tf("obsidianLiveSyncSettingTab.logDatabaseConnected"), LOG_LEVEL_NOTICE);
return true; return true;
} else { } else {
Logger( Logger(
"ERROR: Passphrase is not compatible with the remote server! Please check it again!", $tf("obsidianLiveSyncSettingTab.logPassphraseNotCompatible"),
LOG_LEVEL_NOTICE LOG_LEVEL_NOTICE
); );
return false; return false;
@@ -857,11 +838,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database
}; };
const isPassphraseValid = async () => { const isPassphraseValid = async () => {
if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") {
Logger("You cannot enable encryption without a passphrase", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logEncryptionNoPassphrase"), LOG_LEVEL_NOTICE);
return false; return false;
} }
if (this.editingSettings.encrypt && !(await testCrypt())) { if (this.editingSettings.encrypt && !(await testCrypt())) {
Logger("Your device does not support encryption.", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logEncryptionNoSupport"), LOG_LEVEL_NOTICE);
return false; return false;
} }
return true; return true;
@@ -871,11 +852,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database
method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks" method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"
) => { ) => {
if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") { if (this.editingSettings.encrypt && this.editingSettings.passphrase == "") {
Logger("You cannot enable encryption without a passphrase", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logEncryptionNoPassphrase"), LOG_LEVEL_NOTICE);
return; return;
} }
if (this.editingSettings.encrypt && !(await testCrypt())) { if (this.editingSettings.encrypt && !(await testCrypt())) {
Logger("Your device does not support encryption.", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logEncryptionNoSupport"), LOG_LEVEL_NOTICE);
return; return;
} }
if (!this.editingSettings.encrypt) { if (!this.editingSettings.encrypt) {
@@ -886,7 +867,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
await this.plugin.$allSuspendExtraSync(); await this.plugin.$allSuspendExtraSync();
this.reloadAllSettings(); this.reloadAllSettings();
this.editingSettings.isConfigured = true; this.editingSettings.isConfigured = true;
Logger("Syncing has been disabled, fetch and re-enabled if desired.", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logRebuildNote"), LOG_LEVEL_NOTICE);
await this.saveAllDirtySettings(); await this.saveAllDirtySettings();
this.closeSetting(); this.closeSetting();
await delay(2000); await delay(2000);
@@ -894,14 +875,14 @@ Store only the settings. **Caution: This may lead to data corruption**; database
}; };
// Panes // Panes
void addPane(containerEl, "Change Log", "💬", 100, false).then((paneEl) => { void addPane(containerEl, $tf("obsidianLiveSyncSettingTab.panelChangeLog"), "💬", 100, false).then((paneEl) => {
const informationDivEl = this.createEl(paneEl, "div", { text: "" }); const informationDivEl = this.createEl(paneEl, "div", { text: "" });
const tmpDiv = createDiv(); const tmpDiv = createDiv();
// tmpDiv.addClass("sls-header-button"); // tmpDiv.addClass("sls-header-button");
tmpDiv.addClass("op-warn-info"); tmpDiv.addClass("op-warn-info");
tmpDiv.innerHTML = `<p>Here due to an upgrade notification? Please review the version history. If you're satisfied, click the button. A new update will prompt this again.</p><button> OK, I have read everything. </button>`; tmpDiv.innerHTML = `<p>${$tf("obsidianLiveSyncSettingTab.msgNewVersionNote")}</p><button>${$tf("obsidianLiveSyncSettingTab.optionOkReadEverything")}</button>`
if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) { if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) {
const informationButtonDiv = informationDivEl.appendChild(tmpDiv); const informationButtonDiv = informationDivEl.appendChild(tmpDiv);
informationButtonDiv.querySelector("button")?.addEventListener("click", () => { informationButtonDiv.querySelector("button")?.addEventListener("click", () => {
@@ -917,35 +898,33 @@ Store only the settings. **Caution: This may lead to data corruption**; database
); );
}); });
void addPane(containerEl, "Setup", "🧙‍♂️", 110, false).then((paneEl) => { void addPane(containerEl, $tf("obsidianLiveSyncSettingTab.panelSetup"), "🧙‍♂️", 110, false).then((paneEl) => {
void addPanel(paneEl, "Quick Setup").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleQuickSetup")).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Connect with Setup URI") .setName($tf("obsidianLiveSyncSettingTab.nameConnectSetupURI"))
.setDesc("This is the recommended method to set up Self-hosted LiveSync with a Setup URI.") .setDesc($tf("obsidianLiveSyncSettingTab.descConnectSetupURI"))
.addButton((text) => { .addButton((text) => {
text.setButtonText("Use").onClick(() => { text.setButtonText($tf("obsidianLiveSyncSettingTab.btnUse")).onClick(() => {
this.closeSetting(); this.closeSetting();
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI); eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
}); });
}); });
new Setting(paneEl) new Setting(paneEl)
.setName("Manual setup") .setName($tf("obsidianLiveSyncSettingTab.nameManualSetup"))
.setDesc("Not recommended, but useful if you don't have a Setup URI") .setDesc($tf("obsidianLiveSyncSettingTab.descManualSetup"))
.addButton((text) => { .addButton((text) => {
text.setButtonText("Start").onClick(async () => { text.setButtonText($tf("obsidianLiveSyncSettingTab.btnStart")).onClick(async () => {
await this.enableMinimalSetup(); await this.enableMinimalSetup();
}); });
}); });
new Setting(paneEl) new Setting(paneEl)
.setName("Enable LiveSync") .setName($tf("obsidianLiveSyncSettingTab.nameEnableLiveSync"))
.setDesc( .setDesc($tf("obsidianLiveSyncSettingTab.descEnableLiveSync"))
"Only enable this after configuring either of the above two options or completing all configuration manually."
)
.addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true))) .addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true)))
.addButton((text) => { .addButton((text) => {
text.setButtonText("Enable").onClick(async () => { text.setButtonText($tf("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
this.editingSettings.isConfigured = true; this.editingSettings.isConfigured = true;
await this.saveAllDirtySettings(); await this.saveAllDirtySettings();
this.plugin.$$askReload(); this.plugin.$$askReload();
@@ -955,29 +934,29 @@ Store only the settings. **Caution: This may lead to data corruption**; database
void addPanel( void addPanel(
paneEl, paneEl,
"To setup other devices", $tf("obsidianLiveSyncSettingTab.titleSetupOtherDevices"),
undefined, undefined,
visibleOnly(() => this.isConfiguredAs("isConfigured", true)) visibleOnly(() => this.isConfiguredAs("isConfigured", true))
).then((paneEl) => { ).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Copy the current settings to a Setup URI") .setName($tf("obsidianLiveSyncSettingTab.nameCopySetupURI"))
.setDesc("Perfect for setting up a new device!") .setDesc($tf("obsidianLiveSyncSettingTab.descCopySetupURI"))
.addButton((text) => { .addButton((text) => {
text.setButtonText("Copy").onClick(() => { text.setButtonText($tf("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => {
// await this.plugin.addOnSetup.command_copySetupURI(); // await this.plugin.addOnSetup.command_copySetupURI();
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
}); });
}); });
}); });
void addPanel(paneEl, "Reset").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Discard existing settings and databases") .setName($tf("obsidianLiveSyncSettingTab.nameDiscardSettings"))
.addButton((text) => { .addButton((text) => {
text.setButtonText("Discard") text.setButtonText($tf("obsidianLiveSyncSettingTab.btnDiscard"))
.onClick(async () => { .onClick(async () => {
if ( if (
(await this.plugin.confirm.askYesNoDialog( (await this.plugin.confirm.askYesNoDialog(
"Do you really want to discard existing settings and databases?", $tf("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
{ defaultOption: "No" } { defaultOption: "No" }
)) == "yes" )) == "yes"
) { ) {
@@ -993,9 +972,9 @@ Store only the settings. **Caution: This may lead to data corruption**; database
.setWarning(); .setWarning();
}) })
.addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true))); .addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true)));
// }
}); });
void addPanel(paneEl, "Enable extra and advanced features").then((paneEl) => {
void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleExtraFeatures")).then((paneEl) => {
new Setting(paneEl).autoWireToggle("useAdvancedMode"); new Setting(paneEl).autoWireToggle("useAdvancedMode");
new Setting(paneEl).autoWireToggle("usePowerUserMode"); new Setting(paneEl).autoWireToggle("usePowerUserMode");
@@ -1005,17 +984,18 @@ Store only the settings. **Caution: This may lead to data corruption**; database
this.addOnSaved("usePowerUserMode", () => this.display()); this.addOnSaved("usePowerUserMode", () => this.display());
this.addOnSaved("useEdgeCaseMode", () => this.display()); this.addOnSaved("useEdgeCaseMode", () => this.display());
}); });
void addPanel(paneEl, "Online Tips").then((paneEl) => {
// this.createEl(paneEl, "h3", { text: "Online Tips" }); void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleOnlineTips")).then((paneEl) => {
// this.createEl(paneEl, "h3", { text: $tf("obsidianLiveSyncSettingTab.titleOnlineTips") });
const repo = "vrtmrz/obsidian-livesync"; const repo = "vrtmrz/obsidian-livesync";
const topPath = "/docs/troubleshooting.md"; const topPath = $tf("obsidianLiveSyncSettingTab.linkTroubleshooting");
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`; const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
this.createEl( this.createEl(
paneEl, paneEl,
"div", "div",
"", "",
(el) => (el) =>
(el.innerHTML = `<a href='https://github.com/${repo}/blob/main${topPath}' target="_blank">Open in browser</a>`) (el.innerHTML = `<a href='https://github.com/${repo}/blob/main${topPath}' target="_blank">${$tf("obsidianLiveSyncSettingTab.linkOpenInBrowser")}</a>`)
); );
const troubleShootEl = this.createEl(paneEl, "div", { const troubleShootEl = this.createEl(paneEl, "div", {
text: "", text: "",
@@ -1035,7 +1015,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
try { try {
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`); remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
} catch (ex: any) { } catch (ex: any) {
remoteTroubleShootMDSrc = "An error occurred!!\n" + ex.toString(); remoteTroubleShootMDSrc = `${$tf("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${ex.toString()}`;
} }
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace( const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
/\((.*?(.png)|(.jpg))\)/g, /\((.*?(.png)|(.jpg))\)/g,
@@ -1044,7 +1024,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
// Render markdown // Render markdown
await MarkdownRenderer.render( await MarkdownRenderer.render(
this.plugin.app, this.plugin.app,
`<a class='sls-troubleshoot-anchor'></a> [Tips and Troubleshooting](${topPath}) [PageTop](${filename})\n\n${remoteTroubleShootMD}`, `<a class='sls-troubleshoot-anchor'></a> [${$tf("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$tf("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
troubleShootEl, troubleShootEl,
`${rawRepoURI}`, `${rawRepoURI}`,
this.plugin this.plugin
@@ -1097,10 +1077,10 @@ Store only the settings. **Caution: This may lead to data corruption**; database
void loadMarkdownPage(topPath); void loadMarkdownPage(topPath);
}); });
}); });
void addPane(containerEl, "General Settings", "⚙️", 20, false).then((paneEl) => { void addPane(containerEl, $tf("obsidianLiveSyncSettingTab.panelGeneralSettings"), "⚙️", 20, false).then((paneEl) => {
void addPanel(paneEl, "Appearance").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleAppearance")).then((paneEl) => {
const languages = Object.fromEntries([ const languages = Object.fromEntries([
["", "Default"], ["", $tf("obsidianLiveSyncSettingTab.defaultLanguage")],
...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]), ...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]),
]) as Record<I18N_LANGS, string>; ]) as Record<I18N_LANGS, string>;
new Setting(paneEl).autoWireDropDown("displayLanguage", { new Setting(paneEl).autoWireDropDown("displayLanguage", {
@@ -1113,7 +1093,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
}); });
new Setting(paneEl).autoWireToggle("showStatusOnStatusbar"); new Setting(paneEl).autoWireToggle("showStatusOnStatusbar");
}); });
void addPanel(paneEl, "Logging").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl).autoWireToggle("lessInformationInLog"); new Setting(paneEl).autoWireToggle("lessInformationInLog");
@@ -1124,7 +1104,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
}); });
new Setting(paneEl).setClass("wizardOnly").addButton((button) => new Setting(paneEl).setClass("wizardOnly").addButton((button) =>
button button
.setButtonText("Next") .setButtonText($tf("obsidianLiveSyncSettingTab.btnNext"))
.setCta() .setCta()
.onClick(() => { .onClick(() => {
this.changeDisplay("0"); this.changeDisplay("0");
@@ -1133,7 +1113,7 @@ Store only the settings. **Caution: This may lead to data corruption**; database
}); });
let checkResultDiv: HTMLDivElement; let checkResultDiv: HTMLDivElement;
const checkConfig = async (checkResultDiv: HTMLDivElement | undefined) => { const checkConfig = async (checkResultDiv: HTMLDivElement | undefined) => {
Logger(`Checking database configuration`, LOG_LEVEL_INFO); Logger($tf("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO);
let isSuccessful = true; let isSuccessful = true;
const emptyDiv = createDiv(); const emptyDiv = createDiv();
emptyDiv.innerHTML = "<span></span>"; emptyDiv.innerHTML = "<span></span>";
@@ -1149,9 +1129,10 @@ Store only the settings. **Caution: This may lead to data corruption**; database
}; };
try { try {
if (isCloudantURI(this.editingSettings.couchDB_URI)) { if (isCloudantURI(this.editingSettings.couchDB_URI)) {
Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logCannotUseCloudant"), LOG_LEVEL_NOTICE);
return; return;
} }
// Tip: Add log for cloudant as Logger($tf("obsidianLiveSyncSettingTab.logServerConfigurationCheck"));
const r = await requestToCouchDB( const r = await requestToCouchDB(
this.editingSettings.couchDB_URI, this.editingSettings.couchDB_URI,
this.editingSettings.couchDB_USER, this.editingSettings.couchDB_USER,
@@ -1164,11 +1145,11 @@ Store only the settings. **Caution: This may lead to data corruption**; database
if (!checkResultDiv) return; if (!checkResultDiv) return;
const tmpDiv = createDiv(); const tmpDiv = createDiv();
tmpDiv.addClass("ob-btn-config-fix"); tmpDiv.addClass("ob-btn-config-fix");
tmpDiv.innerHTML = `<label>${title}</label><button>Fix</button>`; tmpDiv.innerHTML = `<label>${title}</label><button>${$tf("obsidianLiveSyncSettingTab.btnFix")}</button>`;
const x = checkResultDiv.appendChild(tmpDiv); const x = checkResultDiv.appendChild(tmpDiv);
x.querySelector("button")?.addEventListener("click", () => { x.querySelector("button")?.addEventListener("click", () => {
fireAndForget(async () => { fireAndForget(async () => {
Logger(`CouchDB Configuration: ${title} -> Set ${key} to ${value}`); Logger($tf("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value }));
const res = await requestToCouchDB( const res = await requestToCouchDB(
this.editingSettings.couchDB_URI, this.editingSettings.couchDB_URI,
this.editingSettings.couchDB_USER, this.editingSettings.couchDB_USER,
@@ -1178,96 +1159,104 @@ Store only the settings. **Caution: This may lead to data corruption**; database
value value
); );
if (res.status == 200) { if (res.status == 200) {
Logger(`CouchDB Configuration: ${title} successfully updated`, LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logCouchDbConfigUpdated", { title }), LOG_LEVEL_NOTICE);
checkResultDiv.removeChild(x); checkResultDiv.removeChild(x);
await checkConfig(checkResultDiv); await checkConfig(checkResultDiv);
} else { } else {
Logger(`CouchDB Configuration: ${title} failed`, LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logCouchDbConfigFail", { title }), LOG_LEVEL_NOTICE);
Logger(res.text, LOG_LEVEL_VERBOSE); Logger(res.text, LOG_LEVEL_VERBOSE);
} }
}); });
}); });
}; };
addResult("---Notice---", ["ob-btn-config-head"]); addResult($tf("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]);
addResult( addResult($tf("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]);
"If the server configuration is not persistent (e.g., running on docker), the values here may change. Once you are able to connect, please update the settings in the server's local.ini.", addResult($tf("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]);
["ob-btn-config-info"]
);
addResult("--Config check--", ["ob-btn-config-head"]);
// Admin check // Admin check
// for database creation and deletion // for database creation and deletion
if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) { if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) {
addResult(`⚠ You do not have administrator privileges.`); addResult($tf("obsidianLiveSyncSettingTab.warnNoAdmin"));
} else { } else {
addResult("✔ You have administrator privileges."); addResult($tf("obsidianLiveSyncSettingTab.okAdminPrivileges"));
} }
// HTTP user-authorization check // HTTP user-authorization check
if (responseConfig?.chttpd?.require_valid_user != "true") { if (responseConfig?.chttpd?.require_valid_user != "true") {
isSuccessful = false; isSuccessful = false;
addResult("❗ chttpd.require_valid_user is wrong."); addResult($tf("obsidianLiveSyncSettingTab.errRequireValidUser"));
addConfigFixButton("Set chttpd.require_valid_user = true", "chttpd/require_valid_user", "true"); addConfigFixButton(
$tf("obsidianLiveSyncSettingTab.msgSetRequireValidUser"),
"chttpd/require_valid_user",
"true"
);
} else { } else {
addResult("✔ chttpd.require_valid_user is ok."); addResult($tf("obsidianLiveSyncSettingTab.okRequireValidUser"));
} }
if (responseConfig?.chttpd_auth?.require_valid_user != "true") { if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
isSuccessful = false; isSuccessful = false;
addResult("❗ chttpd_auth.require_valid_user is wrong."); addResult($tf("obsidianLiveSyncSettingTab.errRequireValidUserAuth"));
addConfigFixButton( addConfigFixButton(
"Set chttpd_auth.require_valid_user = true", $tf("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"),
"chttpd_auth/require_valid_user", "chttpd_auth/require_valid_user",
"true" "true"
); );
} else { } else {
addResult("✔ chttpd_auth.require_valid_user is ok."); addResult($tf("obsidianLiveSyncSettingTab.okRequireValidUserAuth"));
} }
// HTTPD check // HTTPD check
// Check Authentication header // Check Authentication header
if (!responseConfig?.httpd["WWW-Authenticate"]) { if (!responseConfig?.httpd["WWW-Authenticate"]) {
isSuccessful = false; isSuccessful = false;
addResult("❗ httpd.WWW-Authenticate is missing"); addResult($tf("obsidianLiveSyncSettingTab.errMissingWwwAuth"));
addConfigFixButton("Set httpd.WWW-Authenticate", "httpd/WWW-Authenticate", 'Basic realm="couchdb"'); addConfigFixButton(
$tf("obsidianLiveSyncSettingTab.msgSetWwwAuth"),
"httpd/WWW-Authenticate",
'Basic realm="couchdb"'
);
} else { } else {
addResult("✔ httpd.WWW-Authenticate is ok."); addResult($tf("obsidianLiveSyncSettingTab.okWwwAuth"));
} }
if (responseConfig?.httpd?.enable_cors != "true") { if (responseConfig?.httpd?.enable_cors != "true") {
isSuccessful = false; isSuccessful = false;
addResult("❗ httpd.enable_cors is wrong"); addResult($tf("obsidianLiveSyncSettingTab.errEnableCors"));
addConfigFixButton("Set httpd.enable_cors", "httpd/enable_cors", "true"); addConfigFixButton($tf("obsidianLiveSyncSettingTab.msgEnableCors"), "httpd/enable_cors", "true");
} else { } else {
addResult("✔ httpd.enable_cors is ok."); addResult($tf("obsidianLiveSyncSettingTab.okEnableCors"));
} }
// If the server is not cloudant, configure request size // If the server is not cloudant, configure request size
if (!isCloudantURI(this.editingSettings.couchDB_URI)) { if (!isCloudantURI(this.editingSettings.couchDB_URI)) {
// REQUEST SIZE // REQUEST SIZE
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) { if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
isSuccessful = false; isSuccessful = false;
addResult("❗ chttpd.max_http_request_size is low)"); addResult($tf("obsidianLiveSyncSettingTab.errMaxRequestSize"));
addConfigFixButton( addConfigFixButton(
"Set chttpd.max_http_request_size", $tf("obsidianLiveSyncSettingTab.msgSetMaxRequestSize"),
"chttpd/max_http_request_size", "chttpd/max_http_request_size",
"4294967296" "4294967296"
); );
} else { } else {
addResult("✔ chttpd.max_http_request_size is ok."); addResult($tf("obsidianLiveSyncSettingTab.okMaxRequestSize"));
} }
if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) { if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) {
isSuccessful = false; isSuccessful = false;
addResult("❗ couchdb.max_document_size is low)"); addResult($tf("obsidianLiveSyncSettingTab.errMaxDocumentSize"));
addConfigFixButton("Set couchdb.max_document_size", "couchdb/max_document_size", "50000000"); addConfigFixButton(
$tf("obsidianLiveSyncSettingTab.msgSetMaxDocSize"),
"couchdb/max_document_size",
"50000000"
);
} else { } else {
addResult("✔ couchdb.max_document_size is ok."); addResult($tf("obsidianLiveSyncSettingTab.okMaxDocumentSize"));
} }
} }
// CORS check // CORS check
// checking connectivity for mobile // checking connectivity for mobile
if (responseConfig?.cors?.credentials != "true") { if (responseConfig?.cors?.credentials != "true") {
isSuccessful = false; isSuccessful = false;
addResult("❗ cors.credentials is wrong"); addResult($tf("obsidianLiveSyncSettingTab.errCorsCredentials"));
addConfigFixButton("Set cors.credentials", "cors/credentials", "true"); addConfigFixButton($tf("obsidianLiveSyncSettingTab.msgSetCorsCredentials"), "cors/credentials", "true");
} else { } else {
addResult("✔ cors.credentials is ok."); addResult($tf("obsidianLiveSyncSettingTab.okCorsCredentials"));
} }
const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(","); const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(",");
if ( if (
@@ -1276,18 +1265,18 @@ Store only the settings. **Caution: This may lead to data corruption**; database
ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 && ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 &&
ConfiguredOrigins.indexOf("http://localhost") !== -1) ConfiguredOrigins.indexOf("http://localhost") !== -1)
) { ) {
addResult("✔ cors.origins is ok."); addResult($tf("obsidianLiveSyncSettingTab.okCorsOrigins"));
} else { } else {
addResult("❗ cors.origins is wrong"); addResult($tf("obsidianLiveSyncSettingTab.errCorsOrigins"));
addConfigFixButton( addConfigFixButton(
"Set cors.origins", $tf("obsidianLiveSyncSettingTab.msgSetCorsOrigins"),
"cors/origins", "cors/origins",
"app://obsidian.md,capacitor://localhost,http://localhost" "app://obsidian.md,capacitor://localhost,http://localhost"
); );
isSuccessful = false; isSuccessful = false;
} }
addResult("--Connection check--", ["ob-btn-config-head"]); addResult($tf("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]);
addResult(`Current origin:${window.location.origin}`); addResult($tf("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin }));
// Request header check // Request header check
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"]; const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];
@@ -1304,35 +1293,35 @@ Store only the settings. **Caution: This may lead to data corruption**; database
return e; return e;
}) })
); );
addResult(`Origin check:${org}`); addResult($tf("obsidianLiveSyncSettingTab.msgOriginCheck", { org }));
if (responseHeaders["access-control-allow-credentials"] != "true") { if (responseHeaders["access-control-allow-credentials"] != "true") {
addResult("❗ CORS is not allowing credentials"); addResult($tf("obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials"));
isSuccessful = false; isSuccessful = false;
} else { } else {
addResult("✔ CORS credentials OK"); addResult($tf("obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin"));
} }
if (responseHeaders["access-control-allow-origin"] != org) { if (responseHeaders["access-control-allow-origin"] != org) {
addResult( addResult(
`⚠ CORS Origin is unmatched:${origin}->${responseHeaders["access-control-allow-origin"]}` $tf("obsidianLiveSyncSettingTab.warnCorsOriginUnmatched", {
from: origin,
to: responseHeaders["access-control-allow-origin"],
})
); );
} else { } else {
addResult("✔ CORS origin OK"); addResult($tf("obsidianLiveSyncSettingTab.okCorsOriginMatched"));
} }
} }
addResult("--Done--", ["ob-btn-config-head"]); addResult($tf("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
addResult( addResult($tf("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
"If you're having trouble with the Connection-check (even after checking config), please check your reverse proxy configuration.", Logger($tf("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO);
["ob-btn-config-info"]
);
Logger(`Checking configuration done`, LOG_LEVEL_INFO);
} catch (ex: any) { } catch (ex: any) {
if (ex?.status == 401) { if (ex?.status == 401) {
isSuccessful = false; isSuccessful = false;
addResult(`Access forbidden.`); addResult($tf("obsidianLiveSyncSettingTab.errAccessForbidden"));
addResult(`We could not continue the test.`); addResult($tf("obsidianLiveSyncSettingTab.errCannotContinueTest"));
Logger(`Checking configuration done`, LOG_LEVEL_INFO); Logger($tf("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO);
} else { } else {
Logger(`Checking configuration failed`, LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logCheckingConfigFailed"), LOG_LEVEL_NOTICE);
Logger(ex); Logger(ex);
isSuccessful = false; isSuccessful = false;
} }
@@ -1340,31 +1329,23 @@ Store only the settings. **Caution: This may lead to data corruption**; database
return isSuccessful; return isSuccessful;
}; };
void addPane(containerEl, "Remote Configuration", "🛰️", 0, false).then((paneEl) => { void addPane(containerEl, $tf("obsidianLiveSyncSettingTab.panelRemoteConfiguration"), "🛰️", 0, false).then((paneEl) => {
void addPanel(paneEl, "Remote Server").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleRemoteServer")).then((paneEl) => {
// const containerRemoteDatabaseEl = containerEl.createDiv(); // const containerRemoteDatabaseEl = containerEl.createDiv();
new Setting(paneEl).autoWireDropDown("remoteType", { new Setting(paneEl).autoWireDropDown("remoteType", {
holdValue: true, holdValue: true,
options: { options: {
[REMOTE_COUCHDB]: "CouchDB", [REMOTE_COUCHDB]: $tf("obsidianLiveSyncSettingTab.optionCouchDB"),
[REMOTE_MINIO]: "Minio,S3,R2", [REMOTE_MINIO]: $tf("obsidianLiveSyncSettingTab.optionMinioS3R2"),
}, },
onUpdate: enableOnlySyncDisabled, onUpdate: enableOnlySyncDisabled,
}); });
void addPanel(paneEl, "Minio,S3,R2", undefined, onlyOnMinIO).then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleMinioS3R2"), undefined, onlyOnMinIO).then((paneEl) => {
const syncWarnMinio = this.createEl(paneEl, "div", { const syncWarnMinio = this.createEl(paneEl, "div", {
text: "", text: "",
}); });
const ObjectStorageMessage = `WARNING: This feature is a Work In Progress, so please keep in mind the following: const ObjectStorageMessage = $tf("obsidianLiveSyncSettingTab.msgObjectStorageWarning");
- Append only architecture. A rebuild is required to shrink the storage.
- A bit fragile.
- When first syncing, all history will be transferred from the remote. Be mindful of data caps and slow speeds.
- Only differences are synced live.
If you run into any issues, or have ideas about this feature, please create a issue on GitHub.
I appreciate you for your great dedication.
`;
void MarkdownRenderer.render( void MarkdownRenderer.render(
this.plugin.app, this.plugin.app,
@@ -1388,16 +1369,16 @@ I appreciate you for your great dedication.
new Setting(paneEl).autoWireText("bucket", { holdValue: true }); new Setting(paneEl).autoWireText("bucket", { holdValue: true });
new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true }); new Setting(paneEl).autoWireToggle("useCustomRequestHandler", { holdValue: true });
new Setting(paneEl).setName("Test Connection").addButton((button) => new Setting(paneEl).setName($tf("obsidianLiveSyncSettingTab.nameTestConnection")).addButton((button) =>
button button
.setButtonText("Test") .setButtonText($tf("obsidianLiveSyncSettingTab.btnTest"))
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await this.testConnection(this.editingSettings); await this.testConnection(this.editingSettings);
}) })
); );
new Setting(paneEl) new Setting(paneEl)
.setName("Apply Settings") .setName($tf("obsidianLiveSyncSettingTab.nameApplySettings"))
.setClass("wizardHidden") .setClass("wizardHidden")
.addApplyButton([ .addApplyButton([
"remoteType", "remoteType",
@@ -1411,13 +1392,13 @@ I appreciate you for your great dedication.
.addOnUpdate(onlyOnMinIO); .addOnUpdate(onlyOnMinIO);
}); });
void addPanel(paneEl, "CouchDB", undefined, onlyOnCouchDB).then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleCouchDB"), undefined, onlyOnCouchDB).then((paneEl) => {
if (this.plugin.$$isMobile()) { if (this.plugin.$$isMobile()) {
this.createEl( this.createEl(
paneEl, paneEl,
"div", "div",
{ {
text: `Cannot connect to non-HTTPS URI. Please update your config and try again.`, text:$tf("obsidianLiveSyncSettingTab.msgNonHTTPSWarning"),
}, },
undefined, undefined,
visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://"))
@@ -1427,7 +1408,7 @@ I appreciate you for your great dedication.
paneEl, paneEl,
"div", "div",
{ {
text: `Configured as non-HTTPS URI. Be warned that this may not work on mobile devices.`, text: $tf("obsidianLiveSyncSettingTab.msgNonHTTPSInfo"),
}, },
undefined, undefined,
visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://")) visibleOnly(() => !this.editingSettings.couchDB_URI.startsWith("https://"))
@@ -1438,7 +1419,7 @@ I appreciate you for your great dedication.
paneEl, paneEl,
"div", "div",
{ {
text: `These settings are unable to be changed during synchronization. Please disable all syncing in the "Sync Settings" to unlock.`, text: $tf("obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync"),
}, },
undefined, undefined,
visibleOnly(() => isAnySyncEnabled()) visibleOnly(() => isAnySyncEnabled())
@@ -1463,14 +1444,12 @@ I appreciate you for your great dedication.
}); });
new Setting(paneEl) new Setting(paneEl)
.setName("Test Database Connection") .setName($tf("obsidianLiveSyncSettingTab.nameTestDatabaseConnection"))
.setClass("wizardHidden") .setClass("wizardHidden")
.setDesc( .setDesc($tf("obsidianLiveSyncSettingTab.descTestDatabaseConnection"))
"Open database connection. If the remote database is not found and you have permission to create a database, the database will be created."
)
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Test") .setButtonText($tf("obsidianLiveSyncSettingTab.btnTest"))
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await this.testConnection(); await this.testConnection();
@@ -1478,11 +1457,11 @@ I appreciate you for your great dedication.
); );
new Setting(paneEl) new Setting(paneEl)
.setName("Validate Database Configuration") .setName($tf("obsidianLiveSyncSettingTab.nameValidateDatabaseConfig"))
.setDesc("Checks and fixes any potential issues with the database config.") .setDesc($tf("obsidianLiveSyncSettingTab.descValidateDatabaseConfig"))
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Check") .setButtonText($tf("obsidianLiveSyncSettingTab.btnCheck"))
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
await checkConfig(checkResultDiv); await checkConfig(checkResultDiv);
@@ -1493,7 +1472,7 @@ I appreciate you for your great dedication.
}); });
new Setting(paneEl) new Setting(paneEl)
.setName("Apply Settings") .setName($tf("obsidianLiveSyncSettingTab.nameApplySettings"))
.setClass("wizardHidden") .setClass("wizardHidden")
.addApplyButton([ .addApplyButton([
"remoteType", "remoteType",
@@ -1505,12 +1484,12 @@ I appreciate you for your great dedication.
.addOnUpdate(onlyOnCouchDB); .addOnUpdate(onlyOnCouchDB);
}); });
}); });
void addPanel(paneEl, "Notification").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleNotification")).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden"); new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden");
}); });
void addPanel(paneEl, "Privacy & Encryption").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.panelPrivacyEncryption")).then((paneEl) => {
new Setting(paneEl).autoWireToggle("encrypt", { holdValue: true }); new Setting(paneEl).autoWireToggle("encrypt", { holdValue: true });
const isEncryptEnabled = visibleOnly(() => this.isConfiguredAs("encrypt", true)); const isEncryptEnabled = visibleOnly(() => this.isConfiguredAs("encrypt", true));
@@ -1533,13 +1512,13 @@ I appreciate you for your great dedication.
.setClass("wizardHidden"); .setClass("wizardHidden");
}); });
void addPanel(paneEl, "Fetch settings").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleFetchSettings")).then((paneEl) => {
new Setting(paneEl) new Setting(paneEl)
.setName("Fetch config from remote server") .setName($tf("obsidianLiveSyncSettingTab.titleFetchConfigFromRemote"))
.setDesc("Fetch necessary settings from already configured remote server.") .setDesc($tf("obsidianLiveSyncSettingTab.descFetchConfigFromRemote"))
.addButton((button) => .addButton((button) =>
button button
.setButtonText("Fetch") .setButtonText($tf("obsidianLiveSyncSettingTab.buttonFetch"))
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
const trialSetting = { ...this.initialSettings, ...this.editingSettings }; const trialSetting = { ...this.initialSettings, ...this.editingSettings };
@@ -1553,15 +1532,15 @@ I appreciate you for your great dedication.
}); });
new Setting(paneEl).setClass("wizardOnly").addButton((button) => new Setting(paneEl).setClass("wizardOnly").addButton((button) =>
button button
.setButtonText("Next") .setButtonText($tf("obsidianLiveSyncSettingTab.buttonNext"))
.setCta() .setCta()
.setDisabled(false) .setDisabled(false)
.onClick(async () => { .onClick(async () => {
if (!(await checkConfig(checkResultDiv))) { if (!(await checkConfig(checkResultDiv))) {
if ( if (
(await this.plugin.confirm.askYesNoDialog( (await this.plugin.confirm.askYesNoDialog(
"The configuration check has failed. Do you want to continue anyway?", $tf("obsidianLiveSyncSettingTab.msgConfigCheckFailed"),
{ defaultOption: "No", title: "Remote Configuration Check Failed" } { defaultOption: "No", title: $tf("obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed") }
)) == "no" )) == "no"
) { ) {
return; return;
@@ -1572,8 +1551,8 @@ I appreciate you for your great dedication.
if (isEncryptionFullyEnabled) { if (isEncryptionFullyEnabled) {
if ( if (
(await this.plugin.confirm.askYesNoDialog( (await this.plugin.confirm.askYesNoDialog(
"We recommend enabling End-To-End Encryption, and Path Obfuscation. Are you sure you want to continue without encryption?", $tf("obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation"),
{ defaultOption: "No", title: "Encryption is not enabled" } { defaultOption: "No", title: $tf("obsidianLiveSyncSettingTab.titleEncryptionNotEnabled") }
)) == "no" )) == "no"
) { ) {
return; return;
@@ -1585,8 +1564,8 @@ I appreciate you for your great dedication.
if (!(await isPassphraseValid())) { if (!(await isPassphraseValid())) {
if ( if (
(await this.plugin.confirm.askYesNoDialog( (await this.plugin.confirm.askYesNoDialog(
"Your encryption passphrase might be invalid. Are you sure you want to continue?", $tf("obsidianLiveSyncSettingTab.msgInvalidPassphrase"),
{ defaultOption: "No", title: "Encryption Passphrase Invalid?" } { defaultOption: "No", title: $tf("obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid") }
)) == "no" )) == "no"
) { ) {
return; return;
@@ -1601,8 +1580,8 @@ I appreciate you for your great dedication.
} }
if ( if (
(await this.plugin.confirm.askYesNoDialog( (await this.plugin.confirm.askYesNoDialog(
"Do you want to fetch the config from the remote server?", $tf("obsidianLiveSyncSettingTab.msgFetchConfigFromRemote"),
{ defaultOption: "Yes", title: "Fetch config" } { defaultOption: "Yes", title: $tf("obsidianLiveSyncSettingTab.titleFetchConfig") }
)) == "yes" )) == "yes"
) { ) {
const trialSetting = { ...this.initialSettings, ...this.editingSettings }; const trialSetting = { ...this.initialSettings, ...this.editingSettings };
@@ -1618,7 +1597,7 @@ I appreciate you for your great dedication.
}) })
); );
}); });
void addPane(containerEl, "Sync Settings", "🔄", 30, false).then((paneEl) => { void addPane(containerEl, $tf("obsidianLiveSyncSettingTab.titleSyncSettings"), "🔄", 30, false).then((paneEl) => {
if (this.editingSettings.versionUpFlash != "") { if (this.editingSettings.versionUpFlash != "") {
const c = this.createEl( const c = this.createEl(
paneEl, paneEl,
@@ -1628,7 +1607,7 @@ I appreciate you for your great dedication.
cls: "op-warn sls-setting-hidden", cls: "op-warn sls-setting-hidden",
}, },
(el) => { (el) => {
this.createEl(el, "button", { text: "I got it and updated." }, (e) => { this.createEl(el, "button", { text: $tf("obsidianLiveSyncSettingTab.btnGotItAndUpdated") }, (e) => {
e.addClass("mod-cta"); e.addClass("mod-cta");
e.addEventListener("click", () => { e.addEventListener("click", () => {
fireAndForget(async () => { fireAndForget(async () => {
@@ -1644,23 +1623,23 @@ I appreciate you for your great dedication.
} }
this.createEl(paneEl, "div", { this.createEl(paneEl, "div", {
text: `Please select and apply any preset item to complete the wizard.`, text: $tf("obsidianLiveSyncSettingTab.msgSelectAndApplyPreset"),
cls: "wizardOnly", cls: "wizardOnly",
}).addClasses(["op-warn-info"]); }).addClasses(["op-warn-info"]);
void addPanel(paneEl, "Synchronization Preset").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleSynchronizationPreset")).then((paneEl) => {
const options: Record<string, string> = const options: Record<string, string> =
this.editingSettings.remoteType == REMOTE_COUCHDB this.editingSettings.remoteType == REMOTE_COUCHDB
? { ? {
NONE: "", NONE: "",
LIVESYNC: "LiveSync", LIVESYNC: $tf("obsidianLiveSyncSettingTab.optionLiveSync"),
PERIODIC: "Periodic w/ batch", PERIODIC: $tf("obsidianLiveSyncSettingTab.optionPeriodicWithBatch"),
DISABLE: "Disable all automatic", DISABLE: $tf("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"),
} }
: { : {
NONE: "", NONE: "",
PERIODIC: "Periodic w/ batch", PERIODIC: $tf("obsidianLiveSyncSettingTab.optionPeriodicWithBatch"),
DISABLE: "Disable all automatic", DISABLE: $tf("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"),
}; };
new Setting(paneEl) new Setting(paneEl)
@@ -1669,7 +1648,7 @@ I appreciate you for your great dedication.
holdValue: true, holdValue: true,
}) })
.addButton((button) => { .addButton((button) => {
button.setButtonText("Apply"); button.setButtonText($tf("obsidianLiveSyncSettingTab.btnApply"));
button.onClick(async () => { button.onClick(async () => {
// await this.saveSettings(["preset"]); // await this.saveSettings(["preset"]);
await this.saveAllDirtySettings(); await this.saveAllDirtySettings();
@@ -1678,7 +1657,7 @@ I appreciate you for your great dedication.
this.addOnSaved("preset", async (currentPreset) => { this.addOnSaved("preset", async (currentPreset) => {
if (currentPreset == "") { if (currentPreset == "") {
Logger("Select any preset.", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logSelectAnyPreset"), LOG_LEVEL_NOTICE);
return; return;
} }
const presetAllDisabled = { const presetAllDisabled = {
@@ -1711,15 +1690,15 @@ I appreciate you for your great dedication.
...this.editingSettings, ...this.editingSettings,
...presetLiveSync, ...presetLiveSync,
}; };
Logger("Configured synchronization mode: LiveSync", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logConfiguredLiveSync"), LOG_LEVEL_NOTICE);
} else if (currentPreset == "PERIODIC") { } else if (currentPreset == "PERIODIC") {
this.editingSettings = { this.editingSettings = {
...this.editingSettings, ...this.editingSettings,
...presetPeriodic, ...presetPeriodic,
}; };
Logger("Configured synchronization mode: Periodic", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logConfiguredPeriodic"), LOG_LEVEL_NOTICE);
} else { } else {
Logger("Configured synchronization mode: DISABLED", LOG_LEVEL_NOTICE); Logger($tf("obsidianLiveSyncSettingTab.logConfiguredDisabled"), LOG_LEVEL_NOTICE);
this.editingSettings = { this.editingSettings = {
...this.editingSettings, ...this.editingSettings,
...presetAllDisabled, ...presetAllDisabled,
@@ -1737,8 +1716,8 @@ I appreciate you for your great dedication.
// this.resetEditingSettings(); // this.resetEditingSettings();
if ( if (
(await this.plugin.confirm.askYesNoDialog( (await this.plugin.confirm.askYesNoDialog(
"All done! Do you want to generate a setup URI to set up other devices?", $tf("obsidianLiveSyncSettingTab.msgGenerateSetupURI"),
{ defaultOption: "Yes", title: "Congratulations!" } { defaultOption: "Yes", title: $tf("obsidianLiveSyncSettingTab.titleCongratulations") }
)) == "yes" )) == "yes"
) { ) {
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI); eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
@@ -1758,7 +1737,7 @@ I appreciate you for your great dedication.
} }
}); });
}); });
void addPanel(paneEl, "Synchronization Method").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleSynchronizationMethod")).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
// const onlyOnLiveSync = visibleOnly(() => this.isConfiguredAs("syncMode", "LIVESYNC")); // const onlyOnLiveSync = visibleOnly(() => this.isConfiguredAs("syncMode", "LIVESYNC"));
@@ -1768,11 +1747,14 @@ I appreciate you for your great dedication.
const optionsSyncMode = const optionsSyncMode =
this.editingSettings.remoteType == REMOTE_COUCHDB this.editingSettings.remoteType == REMOTE_COUCHDB
? { ? {
ONEVENTS: "On events", ONEVENTS: $tf("obsidianLiveSyncSettingTab.optionOnEvents"),
PERIODIC: "Periodic and on events", PERIODIC: $tf("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"),
LIVESYNC: "LiveSync", LIVESYNC: $tf("obsidianLiveSyncSettingTab.optionLiveSync"),
} }
: { ONEVENTS: "On events", PERIODIC: "Periodic and on events" }; : {
ONEVENTS: $tf("obsidianLiveSyncSettingTab.optionOnEvents"),
PERIODIC: $tf("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"),
};
new Setting(paneEl) new Setting(paneEl)
.autoWireDropDown("syncMode", { .autoWireDropDown("syncMode", {
@@ -1817,7 +1799,7 @@ I appreciate you for your great dedication.
.autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }); .autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync });
}); });
void addPanel(paneEl, "Update thinning").then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleUpdateThinning")).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave"); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave");
new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", { new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", {
@@ -1830,13 +1812,13 @@ I appreciate you for your great dedication.
}); });
}); });
void addPanel(paneEl, "Deletion Propagation", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleDeletionPropagation"), undefined, undefined, LEVEL_ADVANCED).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("trashInsteadDelete"); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("trashInsteadDelete");
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder");
}); });
void addPanel(paneEl, "Conflict resolution", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleConflictResolution"), undefined, undefined, LEVEL_ADVANCED).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile"); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile");
@@ -1846,7 +1828,7 @@ I appreciate you for your great dedication.
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive"); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive");
}); });
void addPanel(paneEl, "Sync settings via markdown", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown"), undefined, undefined, LEVEL_ADVANCED).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
new Setting(paneEl) new Setting(paneEl)
@@ -1858,14 +1840,14 @@ I appreciate you for your great dedication.
new Setting(paneEl).autoWireToggle("notifyAllSettingSyncFile"); new Setting(paneEl).autoWireToggle("notifyAllSettingSyncFile");
}); });
void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { void addPanel(paneEl, $tf("obsidianLiveSyncSettingTab.titleHiddenFiles"), undefined, undefined, LEVEL_ADVANCED).then((paneEl) => {
paneEl.addClass("wizardHidden"); paneEl.addClass("wizardHidden");
const LABEL_ENABLED = "🔁 : Enabled"; const LABEL_ENABLED = $tf("obsidianLiveSyncSettingTab.labelEnabled");
const LABEL_DISABLED = "⏹️ : Disabled"; const LABEL_DISABLED = $tf("obsidianLiveSyncSettingTab.labelDisabled");
const hiddenFileSyncSetting = new Setting(paneEl) const hiddenFileSyncSetting = new Setting(paneEl)
.setName("Hidden file synchronization") .setName($tf("obsidianLiveSyncSettingTab.nameHiddenFileSynchronization"))
.setClass("wizardHidden"); .setClass("wizardHidden");
const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl; const hiddenFileSyncSettingEl = hiddenFileSyncSetting.settingEl;
const hiddenFileSyncSettingDiv = hiddenFileSyncSettingEl.createDiv(""); const hiddenFileSyncSettingDiv = hiddenFileSyncSettingEl.createDiv("");
@@ -1874,10 +1856,10 @@ I appreciate you for your great dedication.
: LABEL_DISABLED; : LABEL_DISABLED;
if (this.editingSettings.syncInternalFiles) { if (this.editingSettings.syncInternalFiles) {
new Setting(paneEl) new Setting(paneEl)
.setName("Disable Hidden files sync") .setName($tf("obsidianLiveSyncSettingTab.nameDisableHiddenFileSync"))
.setClass("wizardHidden") .setClass("wizardHidden")
.addButton((button) => { .addButton((button) => {
button.setButtonText("Disable").onClick(async () => { button.setButtonText($tf("obsidianLiveSyncSettingTab.btnDisable")).onClick(async () => {
this.editingSettings.syncInternalFiles = false; this.editingSettings.syncInternalFiles = false;
await this.saveAllDirtySettings(); await this.saveAllDirtySettings();
this.display(); this.display();
@@ -1885,7 +1867,7 @@ I appreciate you for your great dedication.
}); });
} else { } else {
new Setting(paneEl) new Setting(paneEl)
.setName("Enable Hidden files sync") .setName($tf("obsidianLiveSyncSettingTab.nameEnableHiddenFileSync"))
.setClass("wizardHidden") .setClass("wizardHidden")
.addButton((button) => { .addButton((button) => {
button.setButtonText("Merge").onClick(async () => { button.setButtonText("Merge").onClick(async () => {
@@ -2881,7 +2863,6 @@ ${stringifyYaml(pluginConfig)}`;
}) })
) )
.addOnUpdate(onlyOnCouchDB); .addOnUpdate(onlyOnCouchDB);
new Setting(paneEl) new Setting(paneEl)
.setName("Reset journal received history") .setName("Reset journal received history")
.setDesc( .setDesc(

View File

@@ -8,7 +8,7 @@ import {
EVENT_SETTING_SAVED, EVENT_SETTING_SAVED,
eventHub, eventHub,
} from "../../common/events.ts"; } from "../../common/events.ts";
import { $f, setLang } from "../../lib/src/common/i18n.ts"; import { $tf, setLang } from "../../lib/src/common/i18n.ts";
import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert.ts"; import { versionNumberString2Number } from "../../lib/src/string_and_binary/convert.ts";
import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task"; import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor"; import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
@@ -20,25 +20,16 @@ export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
if (!(await this.core.$everyOnLayoutReady())) return; if (!(await this.core.$everyOnLayoutReady())) return;
eventHub.emitEvent(EVENT_LAYOUT_READY); eventHub.emitEvent(EVENT_LAYOUT_READY);
if (this.settings.suspendFileWatching || this.settings.suspendParseReplicationResult) { if (this.settings.suspendFileWatching || this.settings.suspendParseReplicationResult) {
const ANSWER_KEEP = "Keep LiveSync disabled"; const ANSWER_KEEP = $tf("moduleLiveSyncMain.optionKeepLiveSyncDisabled");
const ANSWER_RESUME = "Resume and restart Obsidian"; const ANSWER_RESUME = $tf("moduleLiveSyncMain.optionResumeAndRestart");
const message = `Self-hosted LiveSync has been configured to ignore some events. Is this correct? const message = $tf("moduleLiveSyncMain.msgScramEnabled", {
fileWatchingStatus: this.settings.suspendFileWatching ? "suspended" : "active",
| Type | Status | Note | parseReplicationStatus: this.settings.suspendParseReplicationResult ? "suspended" : "active"
|:---:|:---:|---| });
| Storage Events | ${this.settings.suspendFileWatching ? "suspended" : "active"} | Every modification will be ignored |
| Database Events | ${this.settings.suspendParseReplicationResult ? "suspended" : "active"} | Every synchronised change will be postponed |
Do you want to resume them and restart Obsidian?
> [!DETAILS]-
> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.
> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.
`;
if ( if (
(await this.core.confirm.askSelectStringDialogue(message, [ANSWER_KEEP, ANSWER_RESUME], { (await this.core.confirm.askSelectStringDialogue(message, [ANSWER_KEEP, ANSWER_RESUME], {
defaultAction: ANSWER_KEEP, defaultAction: ANSWER_KEEP,
title: "Scram Enabled", title: $tf("moduleLiveSyncMain.titleScramEnabled"),
})) == ANSWER_RESUME })) == ANSWER_RESUME
) { ) {
this.settings.suspendFileWatching = false; this.settings.suspendFileWatching = false;
@@ -56,11 +47,11 @@ Do you want to resume them and restart Obsidian?
if (!(await this.core.$everyOnFirstInitialize())) return; if (!(await this.core.$everyOnFirstInitialize())) return;
await this.core.$$realizeSettingSyncMode(); await this.core.$$realizeSettingSyncMode();
fireAndForget(async () => { fireAndForget(async () => {
this._log(`Additional safety scan..`, LOG_LEVEL_VERBOSE); this._log($tf("moduleLiveSyncMain.logAdditionalSafetyScan"), LOG_LEVEL_VERBOSE);
if (!(await this.core.$allScanStat())) { if (!(await this.core.$allScanStat())) {
this._log(`Additional safety scan has failed on a module`, LOG_LEVEL_NOTICE); this._log($tf("moduleLiveSyncMain.logSafetyScanFailed"), LOG_LEVEL_NOTICE);
} else { } else {
this._log(`Additional safety scan completed`, LOG_LEVEL_VERBOSE); this._log($tf("moduleLiveSyncMain.logSafetyScanCompleted"), LOG_LEVEL_VERBOSE);
} }
}); });
} }
@@ -80,9 +71,9 @@ Do you want to resume them and restart Obsidian?
this.$$wireUpEvents(); this.$$wireUpEvents();
// debugger; // debugger;
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core); eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core);
this._log("loading plugin"); this._log($tf("moduleLiveSyncMain.logLoadingPlugin"));
if (!(await this.core.$everyOnloadStart())) { if (!(await this.core.$everyOnloadStart())) {
this._log("Plugin initialisation was cancelled by a module", LOG_LEVEL_NOTICE); this._log($tf("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);
return; return;
} }
// this.addUIs(); // this.addUIs();
@@ -91,10 +82,10 @@ Do you want to resume them and restart Obsidian?
//@ts-ignore //@ts-ignore
const packageVersion: string = PACKAGE_VERSION || "0.0.0"; const packageVersion: string = PACKAGE_VERSION || "0.0.0";
this._log($f`Self-hosted LiveSync${" v"}${manifestVersion} ${packageVersion}`); this._log($tf("moduleLiveSyncMain.logPluginVersion", { manifestVersion, packageVersion }));
await this.core.$$loadSettings(); await this.core.$$loadSettings();
if (!(await this.core.$everyOnloadAfterLoadSettings())) { if (!(await this.core.$everyOnloadAfterLoadSettings())) {
this._log("Plugin initialisation was cancelled by a module", LOG_LEVEL_NOTICE); this._log($tf("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);
return; return;
} }
const lsKey = "obsidian-live-sync-ver" + this.core.$$getVaultName(); const lsKey = "obsidian-live-sync-ver" + this.core.$$getVaultName();
@@ -102,7 +93,7 @@ Do you want to resume them and restart Obsidian?
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000); const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) { if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) {
this._log($f`LiveSync has updated, please read the changelog!`, LOG_LEVEL_NOTICE); this._log($tf("moduleLiveSyncMain.logReadChangelog"), LOG_LEVEL_NOTICE);
} }
//@ts-ignore //@ts-ignore
@@ -117,7 +108,7 @@ Do you want to resume them and restart Obsidian?
this.settings.syncOnFileOpen = false; this.settings.syncOnFileOpen = false;
this.settings.syncAfterMerge = false; this.settings.syncAfterMerge = false;
this.settings.periodicReplication = false; this.settings.periodicReplication = false;
this.settings.versionUpFlash = $f`LiveSync has been updated, In case of breaking updates, all automatic synchronization has been temporarily disabled. Ensure that all devices are up to date before enabling.`; this.settings.versionUpFlash = $tf("moduleLiveSyncMain.logVersionUpdate");
await this.saveSettings(); await this.saveSettings();
} }
localStorage.setItem(lsKey, `${VER}`); localStorage.setItem(lsKey, `${VER}`);
@@ -148,7 +139,7 @@ Do you want to resume them and restart Obsidian?
} }
await this.localDatabase.close(); await this.localDatabase.close();
} }
this._log($f`unloading plugin`); this._log($tf("moduleLiveSyncMain.logUnloadingPlugin"));
} }
async $$realizeSettingSyncMode(): Promise<void> { async $$realizeSettingSyncMode(): Promise<void> {