Improve settings

This commit is contained in:
Andras Schmelczer 2025-01-02 22:06:19 +00:00
parent 5870636210
commit a628b1f8ce
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
4 changed files with 437 additions and 104 deletions

View file

@ -16,12 +16,13 @@ import { Logger } from "./tracing/logger.js";
import { SyncHistory } from "./tracing/sync-history.js";
import { LogsView } from "./views/logs-view.js";
import { Syncer } from "./sync-operations/syncer.js";
import { StatusDescription } from "./views/status-description.js";
export default class SyncPlugin extends Plugin {
private remoteListenerIntervalId: number | null = null;
private readonly operations = new ObsidianFileOperations(this.app.vault);
private readonly history = new SyncHistory();
private settingsTab: SyncSettingsTab;
private remoteListenerIntervalId: number | null = null;
public async onload(): Promise<void> {
Logger.getInstance().info("Starting plugin");
@ -40,22 +41,30 @@ export default class SyncPlugin extends Plugin {
this.saveData.bind(this)
);
const syncServer = new SyncService(database);
const syncService = new SyncService(database);
const syncer = new Syncer({
database,
operations: this.operations,
syncServer,
syncService,
history: this.history,
});
this.settingsTab = new SyncSettingsTab(
this.app,
this,
const statusDescription = new StatusDescription(
database,
syncServer,
syncService,
this.history,
syncer
);
this.settingsTab = new SyncSettingsTab({
app: this.app,
plugin: this,
database,
syncService,
statusDescription,
syncer,
});
this.addSettingTab(this.settingsTab);
new StatusBar(database, this, this.history, syncer);
@ -86,14 +95,14 @@ export default class SyncPlugin extends Plugin {
this.registerEvent(event);
});
await syncer.scheduleSyncForOfflineChanges();
Logger.getInstance().info("Sync handlers initialised");
void syncer.scheduleSyncForOfflineChanges();
});
this.registerRemoteEventListener(
database,
syncServer,
syncService,
syncer,
database.getSettings().fetchChangesUpdateIntervalMs
);
@ -102,7 +111,7 @@ export default class SyncPlugin extends Plugin {
database.addOnSettingsChangeHandlers(async (settings, oldSettings) => {
this.registerRemoteEventListener(
database,
syncServer,
syncService,
syncer,
settings.fetchChangesUpdateIntervalMs
);
@ -130,6 +139,8 @@ export default class SyncPlugin extends Plugin {
);
Logger.getInstance().info("Plugin loaded");
this.openSettings();
}
public onunload(): void {
@ -138,14 +149,19 @@ export default class SyncPlugin extends Plugin {
}
}
public openSettings() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public openSettings(): void {
// eslint-disable-next-line
(this.app as any).setting.open(); // this is undocumented
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line
(this.app as any).setting.openTab(this.settingsTab); // this is undocumented
}
private async activateView(type: string): Promise<void> {
public closeSettings(): void {
// eslint-disable-next-line
(this.app as any).setting.close(); // this is undocumented
}
public async activateView(type: string): Promise<void> {
const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null;
@ -165,7 +181,7 @@ export default class SyncPlugin extends Plugin {
private registerRemoteEventListener(
database: Database,
syncServer: SyncService,
syncService: SyncService,
syncer: Syncer,
intervalMs: number
): void {
@ -178,7 +194,7 @@ export default class SyncPlugin extends Plugin {
async () =>
applyRemoteChangesLocally({
database,
syncServer,
syncService,
syncer,
}),
intervalMs

View file

@ -1,31 +1,106 @@
.sync-settings-access-token textarea {
width: 100%;
height: 100px;
.status-description {
margin: var(--p-spacing) 0;
.number {
padding: var(--size-2-1) var(--size-4-1);
border-radius: var(--radius-s);
background-color: var(--color-base-30);
font-family: var(--font-monospace-default);
font-weight: var(--bold-weight);
&.good {
background-color: rgba(var(--color-green-rgb), 0.35);
}
&.bad {
background-color: rgba(var(--color-red-rgb), 0.35);
}
}
.error {
color: rgb(var(--color-red-rgb));
}
.warning {
color: rgb(var(--color-yellow-rgb));
}
}
.vault-link-settings {
h2 {
display: flex;
align-items: center;
font-size: var(--h2-size);
.version {
display: block;
font-size: var(--font-ui-smaller);
margin-top: var(--size-2-2);
margin-left: var(--size-4-2);
padding: var(--size-2-1) var(--size-4-1);
background-color: var(--color-base-30);
color: var(--color-base-70);
border-radius: var(--radius-s);
}
}
.button-container {
display: flex;
gap: var(--size-4-2);
}
h3 {
font-size: var(--font-ui-large);
margin-top: var(--heading-spacing);
}
button,
input[type="range"],
.checkbox-container,
.slider::-webkit-slider-thumb {
cursor: pointer;
}
textarea {
resize: none;
width: 100%;
height: 60px;
}
}
.sync-status {
display: flex;
gap: var(--size-4-2);
* {
display: block;
}
.initialize-button {
padding: 0 var(--size-4-2);
background: rgba(var(--color-red-rgb), 0.4);
cursor: pointer;
}
}
.log-message {
font: var(--font-monospace-theme);
&.DEBUG {
color: var(--color-base-50);
}
&.INFO {
color: var(--color-base-70);
color: var(--color-green-rgb);
}
&.WARNING {
color: var(--color-yellow-70);
color: var(--color-yellow-rgb);
}
&.ERROR {
color: var(--color-red-70);
color: var(--color-red-rgb);
}
font: var(--font-monospace-theme);
}
.history-card * {
margin: 0;
padding: 0;
}
.history-card {
@ -42,6 +117,11 @@
background-color: rgba(var(--color-red-rgb), 0.2);
}
* {
margin: 0;
padding: 0;
}
.history-card-header {
display: flex;
justify-content: space-between;

View file

@ -4,19 +4,44 @@ import { Notice, PluginSettingTab, Setting } from "obsidian";
import type SyncPlugin from "src/plugin";
import type { Database } from "src/database/database";
import type { SyncService } from "src/services/sync-service";
import type { SyncHistory } from "src/tracing/sync-history";
import { Logger } from "src/tracing/logger";
import type { Syncer } from "src/sync-operations/syncer";
import type { StatusDescription } from "./status-description";
import { LogsView } from "./logs-view";
import { HistoryView } from "./history-view";
export class SyncSettingsTab extends PluginSettingTab {
private editedVaultName: string;
public constructor(
app: App,
plugin: SyncPlugin,
private readonly database: Database,
private readonly syncServer: SyncService,
private readonly history: SyncHistory
) {
private readonly plugin: SyncPlugin;
private readonly database: Database;
private readonly syncService: SyncService;
private readonly statusDescription: StatusDescription;
private readonly syncer: Syncer;
private statusDescriptionSubscription: (() => void) | undefined;
public constructor({
app,
plugin,
database,
syncService,
statusDescription,
syncer,
}: {
app: App;
plugin: SyncPlugin;
database: Database;
syncService: SyncService;
statusDescription: StatusDescription;
syncer: Syncer;
}) {
super(app, plugin);
this.plugin = plugin;
this.database = database;
this.syncService = syncService;
this.statusDescription = statusDescription;
this.syncer = syncer;
this.editedVaultName = this.database.getSettings().vaultName;
this.database.addOnSettingsChangeHandlers(
(newSettings, oldSettings) => {
@ -30,15 +55,65 @@ export class SyncSettingsTab extends PluginSettingTab {
public display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.addClass("vault-link-settings");
containerEl.createEl("h2", { text: "VaultLink" }).createSpan({
text: this.plugin.manifest.version,
cls: "version",
});
const descriptionContainer = containerEl.createDiv({
cls: "description",
});
this.statusDescriptionSubscription = (): void => {
this.statusDescription.renderStatusDescription(
descriptionContainer
);
};
this.statusDescription.addStatusChangeListener(
this.statusDescriptionSubscription
);
containerEl.createDiv(
{
cls: "button-container",
},
(buttonContainer) => {
buttonContainer.createEl(
"button",
{
text: "Show history",
},
(button) =>
(button.onclick = async (): Promise<void> => {
this.plugin.closeSettings();
await this.plugin.activateView(HistoryView.TYPE);
})
);
buttonContainer.createEl(
"button",
{
text: "Show logs",
},
(button) =>
(button.onclick = async (): Promise<void> => {
this.plugin.closeSettings();
await this.plugin.activateView(LogsView.TYPE);
})
);
}
);
containerEl.createEl("h3", { text: "Connection" });
new Setting(containerEl)
.setName("Remote URL")
.setDesc("Your server's URL")
.setTooltip(
"This is the URL of the server you want to sync with, todo, links to docs"
.setName("Server address")
.setDesc(
"Your VaultLink server's URL including the protocol and full path."
)
.setTooltip("This is the URL of the server you want to sync with.")
.addText((text) =>
text
.setPlaceholder("https://example.com:3030")
@ -48,44 +123,30 @@ export class SyncSettingsTab extends PluginSettingTab {
)
)
.addButton((button) =>
button.setButtonText("Test Connection").onClick(async () => {
try {
const result = await this.syncServer.ping();
if (result.isAuthenticated) {
new Notice(
`Successfully authenticated with the server (version: ${result.serverVersion})!`
);
} else {
new Notice(
`Successfully connected to server (version: ${result.serverVersion}) but failed to authenticate.`
);
}
} catch (e) {
new Notice(`Failed to connect to server: ${e}`);
}
})
)
.addSlider((text) =>
text
.setLimits(1, 3600, 1)
.setValue(5)
.setDynamicTooltip()
.setInstant(false)
.setValue(this.database.getSettings().uploadConcurrency)
.onChange(async (value) =>
this.database.setSetting("uploadConcurrency", value)
)
)
.addButton((button) =>
button.setButtonText("Reset sync state").onClick(async () => {
await this.database.resetSyncState();
this.history.reset();
button.setButtonText("Test connection").onClick(async () => {
new Notice(
"Sync state has been reset, you will need to resync"
(await this.syncService.checkConnection()).message
);
await this.statusDescription.updateConnectionState();
})
);
new Setting(containerEl)
.setName("Access token")
.setClass("sync-settings-access-token")
.setDesc(
"Set the access token for the server that you can get from the server"
)
.setTooltip("todo, links to dcocs")
.addTextArea((text) =>
text
.setPlaceholder("ey...")
.setValue(this.database.getSettings().token)
.onChange(async (value) =>
this.database.setSetting("token", value)
)
);
new Setting(containerEl)
.setName("Vault name")
.setDesc(
@ -110,8 +171,25 @@ export class SyncSettingsTab extends PluginSettingTab {
"vaultName",
this.editedVaultName
);
await this.database.resetSyncState();
this.history.reset();
await this.syncer.reset();
Logger.getInstance().reset();
new Notice(
"Sync state has been reset, you will need to resync"
);
})
);
containerEl.createEl("h3", { text: "Sync" });
new Setting(containerEl)
.setName("Danger zone")
.setDesc(
"How many concurrent sync operations to run. Setting this value higher may increase the overall performance, however, it will require more memory as well. If you notice frequent crashes, especially on mobile, set this to 1."
)
.addButton((button) =>
button.setButtonText("Reset sync state").onClick(async () => {
await this.syncer.reset();
Logger.getInstance().reset();
new Notice(
"Sync state has been reset, you will need to resync"
);
@ -119,48 +197,68 @@ export class SyncSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Access token")
.setClass("sync-settings-access-token")
.setName("Remote fetching frequency (seconds)")
.setDesc(
"Set the access token for the server that you can get from the server"
"Set how often should the plugin check for changes on the server. Lower values will increase the frequency of the checks making it easier to collaborate with others."
)
.setTooltip("todo, links to dcocs")
.addTextArea((text) =>
.setTooltip("todo, links to docs")
.addSlider((text) =>
text
.setPlaceholder("ey...")
.setValue(this.database.getSettings().token)
.setLimits(0.5, 60, 0.5)
.setDynamicTooltip()
.setInstant(false)
.setValue(
this.database.getSettings()
.fetchChangesUpdateIntervalMs / 1000
)
.onChange(async (value) =>
this.database.setSetting("token", value)
this.database.setSetting(
"fetchChangesUpdateIntervalMs",
value * 1000
)
)
);
new Setting(containerEl)
.setName("Full scan interval (seconds)")
.setName("Sync concurrency")
.setDesc(
"How often would you like to do a full scan of the local files"
"How many concurrent sync operations to run. Setting this value higher may increase the overall performance, however, it will require more memory as well. If you notice frequent crashes, especially on mobile, set this to 1."
)
.addSlider((text) =>
text
.setLimits(1, 16, 1)
.setDynamicTooltip()
.setInstant(false)
.setValue(this.database.getSettings().syncConcurrency)
.onChange(async (value) =>
this.database.setSetting("syncConcurrency", value)
)
);
new Setting(containerEl)
.setName("Enable sync")
.setDesc(
"Enable pulling and pushing changes to the remote server. The first time it's enabled, or after the sync state has been reset, all local files will be pushed to the server."
)
.setTooltip(
"Enable pulling and pushing changes to the remote server."
)
.setTooltip("todo, links to docs")
.addToggle((toggle) =>
toggle
.setValue(this.database.getSettings().isSyncEnabled)
.onChange(async (value) =>
this.database.setSetting("isSyncEnabled", value)
)
)
.addSlider((text) =>
text
.setLimits(1, 3600, 1)
.setDynamicTooltip()
.setInstant(false)
.setValue(
this.database.getSettings().fetchChangesUpdateIntervalMs
)
.onChange(async (value) =>
this.database.setSetting(
"fetchChangesUpdateIntervalMs",
value
)
)
);
}
public hide(): void {
super.hide();
if (this.statusDescriptionSubscription) {
this.statusDescription.removeStatusChangeListener(
this.statusDescriptionSubscription
);
}
}
}

View file

@ -0,0 +1,139 @@
import type { Database } from "src/database/database";
import type {
CheckConnectionResult,
SyncService,
} from "src/services/sync-service";
import type { Syncer } from "src/sync-operations/syncer";
import type { HistoryStats, SyncHistory } from "src/tracing/sync-history";
export class StatusDescription {
private lastHistoryStats: HistoryStats | undefined;
private lastRemaining: number | undefined;
private lastConnectionState: CheckConnectionResult | undefined;
private statusChangeListeners: (() => void)[] = [];
public constructor(
private readonly database: Database,
private readonly syncService: SyncService,
history: SyncHistory,
syncer: Syncer
) {
void this.updateConnectionState();
history.addSyncHistoryUpdateListener((status) => {
this.lastHistoryStats = status;
this.updateDescription();
});
syncer.addRemainingOperationsListener((remainingOperations) => {
this.lastRemaining = remainingOperations;
this.updateDescription();
});
database.addOnSettingsChangeHandlers(() => {
void this.updateConnectionState();
});
}
public async updateConnectionState(): Promise<void> {
this.lastConnectionState = await this.syncService.checkConnection();
this.updateDescription();
}
public addStatusChangeListener(listener: () => void): void {
this.statusChangeListeners.push(listener);
}
public removeStatusChangeListener(listener: () => void): void {
this.statusChangeListeners = this.statusChangeListeners.filter(
(l) => l !== listener
);
}
public renderStatusDescription(container: HTMLElement): void {
container.empty();
container.addClass("status-description");
if (this.lastConnectionState == undefined) {
container.createSpan({
text: "VaultLink is starting up…",
cls: "warning",
});
return;
}
if (!this.lastConnectionState.isSuccessful) {
container.createSpan({
text: `VaultLink failed to connect to the remote server with the error "${this.lastConnectionState.message}"`,
cls: "error",
});
return;
}
container.createSpan({ text: "VaultLink is connected to the server " });
container.createEl("a", {
text: this.database.getSettings().remoteUri,
href: this.database.getSettings().remoteUri,
});
container.createSpan({
text: ` and has indexed approximately `,
});
container.createSpan({
text: `${this.database.getDocuments().size}`,
cls: "number",
});
container.createSpan({
text: ` documents. `,
});
if (
(this.lastRemaining ?? 0) === 0 &&
(this.lastHistoryStats?.success ?? 0) === 0 &&
(this.lastHistoryStats?.error ?? 0) === 0
) {
if (this.database.getSettings().isSyncEnabled) {
container.createSpan({
text: "Syncing is enabled but VaultLink hasn't found anything to sync yet.",
});
} else {
container.createSpan({
text: "However, syncing is disabled right now.",
cls: "warning",
});
}
return;
}
container.createSpan({
text: "The plugin has ",
});
container.createSpan({
text: `${this.lastRemaining ?? 0}`,
cls: "number",
});
container.createSpan({
text: " outstanding operations while having succeeded ",
});
container.createSpan({
text: `${this.lastHistoryStats?.success ?? 0}`,
cls: ["number", "good"],
});
container.createSpan({
text: " times and failed ",
});
container.createSpan({
text: `${this.lastHistoryStats?.error ?? 0}`,
cls: ["number", "bad"],
});
container.createSpan({
text: " times.",
});
}
private updateDescription(): void {
this.statusChangeListeners.forEach((listener) => {
listener();
});
}
}