diff --git a/frontend/obsidian-plugin/src/views/settings/settings-tab.scss b/frontend/obsidian-plugin/src/views/settings/settings-tab.scss index dcc3e806..0aabbadc 100644 --- a/frontend/obsidian-plugin/src/views/settings/settings-tab.scss +++ b/frontend/obsidian-plugin/src/views/settings/settings-tab.scss @@ -13,45 +13,122 @@ } } -.vault-link-settings { - h2 { - display: flex; - align-items: center; - font-size: var(--h2-size); +.vault-link-settings-container { + position: relative; - .version { - @include number-card; - margin: var(--size-2-2) 0 0 var(--size-4-2); - background-color: var(--color-base-30); - color: var(--color-base-70); - font-size: var(--font-ui-smaller); + .vault-link-settings { + h2 { + display: flex; + align-items: center; + font-size: var(--h2-size); + + .version { + @include number-card; + margin: var(--size-2-2) 0 0 var(--size-4-2); + background-color: var(--color-base-30); + color: var(--color-base-70); + font-size: var(--font-ui-smaller); + } + } + + .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; + } + + input[type="text"], + textarea { + width: 250px; + } + + textarea { + resize: none; + height: 75px; + } + + .applying-changes-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translateY(-50%) translateX(-50%); + z-index: 10; + backdrop-filter: blur(10px); + + .spinner-container { + background-color: rgba(var(--background-primary), 0.5); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + padding: var(--size-4-8); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--size-4-3); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + min-width: 200px; + } + + .spinner { + width: 48px; + height: 48px; + border: 4px solid var(--background-modifier-border); + border-top-color: var(--interactive-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + + .spinner-text { + color: var(--text-normal); + font-size: var(--font-ui-medium); + font-weight: 500; + } + + .spinner-warning { + color: var(--text-muted); + font-size: var(--font-ui-small); + text-align: center; + margin-top: var(--size-2-2); + } + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } + + &.applying-changes { + .setting-item-control { + pointer-events: none; + opacity: 0.5; + } + + button:not(.applying-changes-overlay button) { + pointer-events: none; + opacity: 0.5; + } + + input, + textarea, + select { + pointer-events: none; + opacity: 0.5; + } } } - - .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; - } - - input[type="text"], - textarea { - width: 250px; - } - - textarea { - resize: none; - height: 75px; - } -} +} \ No newline at end of file diff --git a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts index 3c6ccd73..3c711a57 100644 --- a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts +++ b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts @@ -13,6 +13,9 @@ export class SyncSettingsTab extends PluginSettingTab { private editedToken: string; private editedVaultName: string; + private _isApplyingChanges = false; + private syncEnabledOverride: boolean | undefined = undefined; + private readonly plugin: VaultLinkPlugin; private readonly syncClient: SyncClient; private readonly statusDescription: StatusDescription; @@ -64,11 +67,28 @@ export class SyncSettingsTab extends PluginSettingTab { ); } + private get isApplyingChanges(): boolean { + return this._isApplyingChanges; + } + + private set isApplyingChanges(value: boolean) { + this._isApplyingChanges = value; + this.display() + } + public display(): void { const { containerEl } = this; containerEl.empty(); containerEl.addClass("vault-link-settings"); + containerEl.parentElement?.addClass("vault-link-settings-container"); + if (this.isApplyingChanges) { + containerEl.addClass("applying-changes"); + } else { + containerEl.removeClass("applying-changes"); + } + + this.renderApplyingChanges(containerEl); this.renderSettingsHeader(containerEl); this.renderConnectionSettings(containerEl); this.renderSyncSettings(containerEl); @@ -80,6 +100,32 @@ export class SyncSettingsTab extends PluginSettingTab { this.setStatusDescriptionSubscription(); } + private renderApplyingChanges(containerEl: HTMLElement): void { + if (this.isApplyingChanges) { + const overlay = containerEl.createDiv({ + cls: "applying-changes-overlay" + }); + + const spinnerContainer = overlay.createDiv({ + cls: "spinner-container" + }); + + spinnerContainer.createDiv({ + cls: "spinner" + }); + + spinnerContainer.createDiv({ + text: "Applying changes...", + cls: "spinner-text" + }); + + spinnerContainer.createDiv({ + text: "You can exit, but changes won't be saved", + cls: "spinner-warning" + }); + } + } + private renderSettingsHeader(containerEl: HTMLElement): void { containerEl.createEl("h2", { text: "VaultLink" }).createSpan({ text: this.plugin.manifest.version, @@ -111,10 +157,10 @@ export class SyncSettingsTab extends PluginSettingTab { text: "Show history" }, (button) => - (button.onclick = async (): Promise => { - this.plugin.closeSettings(); - await this.plugin.activateView(HistoryView.TYPE); - }) + (button.onclick = async (): Promise => { + this.plugin.closeSettings(); + await this.plugin.activateView(HistoryView.TYPE); + }) ); buttonContainer.createEl( @@ -123,10 +169,10 @@ export class SyncSettingsTab extends PluginSettingTab { text: "Show logs" }, (button) => - (button.onclick = async (): Promise => { - this.plugin.closeSettings(); - await this.plugin.activateView(LogsView.TYPE); - }) + (button.onclick = async (): Promise => { + this.plugin.closeSettings(); + await this.plugin.activateView(LogsView.TYPE); + }) ); } ); @@ -197,23 +243,40 @@ export class SyncSettingsTab extends PluginSettingTab { new Setting(containerEl).addButton((button) => button .setButtonText("Apply & test connection") - .onClick(async () => { - if (this.areThereUnsavedChanges()) { - await this.syncClient.setSettings({ - vaultName: this.editedVaultName, - remoteUri: this.editedServerUri, - token: this.editedToken - }); - new Notice("Checking connection to the server..."); - new Notice( - ( - await this.syncClient.checkConnection() - ).serverMessage - ); - await this.statusDescription.updateConnectionState(); - } else { - new Notice("No changes to apply"); - } + .setDisabled(this.isApplyingChanges) + .setTooltip( + this.isApplyingChanges + ? "Waiting for applying changes to finish..." + : "Apply the changes made to the connection settings and test the connection to the server." + ) + .onClick(() => { + // don't show loader within the button + void (async () => { + if (this.areThereUnsavedChanges()) { + new Notice("Applying changes to the server..."); + + this.isApplyingChanges = true; + try { + await this.syncClient.setSettings({ + vaultName: this.editedVaultName, + remoteUri: this.editedServerUri, + token: this.editedToken + }); + } finally { + this.isApplyingChanges = false; + } + + new Notice("Checking connection to the server..."); + new Notice( + ( + await this.syncClient.checkConnection() + ).serverMessage + ); + await this.statusDescription.updateConnectionState(); + } else { + new Notice("No changes to apply"); + } + })(); }) ); } @@ -239,9 +302,24 @@ export class SyncSettingsTab extends PluginSettingTab { ) .addToggle((toggle) => toggle - .setValue(this.syncClient.getSettings().isSyncEnabled) - .onChange(async (value) => - this.syncClient.setSetting("isSyncEnabled", value) + .setValue(this.syncEnabledOverride ?? this.syncClient.getSettings().isSyncEnabled) + .setDisabled(this.isApplyingChanges) + .setTooltip( + this.isApplyingChanges + ? "Waiting for applying changes to finish..." + : "Enable or disable syncing." + ) + .onChange((value) => void (async () => { + this.syncEnabledOverride = value; + this.isApplyingChanges = true; + try { + await this.syncClient.setSetting("isSyncEnabled", value); + } finally { + this.syncEnabledOverride = undefined; + this.isApplyingChanges = false; + } + } + )() ) ); @@ -321,12 +399,26 @@ export class SyncSettingsTab extends PluginSettingTab { "Delete the local metadata database while leaving the local and remote files intact." ) .addButton((button) => - button.setButtonText("Reset sync state").onClick(async () => { - await this.syncClient.applyChangedConnectionSettings(); - new Notice( - "Sync state has been reset, you will need to resync" - ); - }) + button + .setDisabled(this.isApplyingChanges) + .setTooltip( + this.isApplyingChanges + ? "Waiting for applying changes to finish..." + : "Reset sync state" + ) + .setButtonText("Reset sync state") + .onClick(() => void (async () => { + this.isApplyingChanges = true; + try { + await this.syncClient.reset(); + } finally { + this.isApplyingChanges = false; + } + + new Notice( + "Sync state has been reset, you will need to resync" + ); + })()) ); } @@ -441,9 +533,9 @@ export class SyncSettingsTab extends PluginSettingTab { name: string, settingName: keyof SyncSettings ): [ - DocumentFragment, - (newValue: SyncSettings[keyof SyncSettings]) => unknown - ] { + DocumentFragment, + (newValue: SyncSettings[keyof SyncSettings]) => unknown + ] { const titleContainer = document.createDocumentFragment(); const title = titleContainer.createEl("div", { text: name, @@ -453,11 +545,10 @@ export class SyncSettingsTab extends PluginSettingTab { const updateTitle = ( currentValue: SyncSettings[keyof SyncSettings] ): void => { - title.innerText = `${name}${ - currentValue !== this.syncClient.getSettings()[settingName] - ? " (unsaved)" - : "" - }`; + title.innerText = `${name}${currentValue !== this.syncClient.getSettings()[settingName] + ? " (unsaved)" + : "" + }`; }; return [titleContainer, updateTitle];