diff --git a/manifest.json b/manifest.json index 1ef2a8a..c4ed3a2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "view-sync", "name": "View Sync", - "version": "0.1.5", + "version": "0.2.0", "minAppVersion": "1.3.5", "description": "Sync the state of the active view, not files, among devices.", "author": "Ryota Ushio", diff --git a/package-lock.json b/package-lock.json index 13482e2..19aeac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-view-sync", - "version": "0.1.5", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-view-sync", - "version": "0.1.5", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index df2810a..0e1b547 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-view-sync", - "version": "0.1.5", + "version": "0.2.0", "description": "An Obsidian.md plugin to sync the state of the active view, not files, among devices.", "scripts": { "dev": "node esbuild.config.mjs", diff --git a/src/main.ts b/src/main.ts index cd2750b..ee4a5fd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,8 +25,10 @@ declare module 'obsidian' { } } -export default class MyPlugin extends Plugin { +export default class ViewSyncPlugin extends Plugin { settings: ViewSyncSettings; + #lastViewStateSave: number = 0; + #lastWorkspaceLayoutSave: number = 0; onload() { this.loadSettings(); @@ -90,29 +92,36 @@ export default class MyPlugin extends Plugin { // Make sure that only the active leaf's state is recorded if (leaf !== this.app.workspace.activeLeaf) return; - const serialized = JSON.stringify(Object.assign( - leaf.getViewState(), - { eState: view.getEphemeralState() }, // Ephemeral state is not included in the result of getViewState() - override - )); + const timestamp = Date.now(); + this.#lastViewStateSave = timestamp; + const serialized = JSON.stringify({ + timestamp, + viewState: Object.assign( + leaf.getViewState(), + { eState: view.getEphemeralState() }, // Ephemeral state is not included in the result of getViewState() + override + ) + }); await this.writeFile(path, serialized); } } /** Record the workspace layout to the specified file. */ - onWorkspaceLayoutChange() { + async onWorkspaceLayoutChange() { const path = normalizePath(this.settings.ownWorkspacePath); // An empty string is normalized to `/` if (path === '/') return; const layout = this.app.workspace.getLayout(); - const serialized = JSON.stringify(layout); - this.writeFile(path, serialized); + const timestamp = Date.now(); + this.#lastWorkspaceLayoutSave = timestamp; + const serialized = JSON.stringify({ timestamp, layout }); + await this.writeFile(path, serialized); } registerViewSyncEventPublisher() { - this.registerEvent(this.app.workspace.on('active-leaf-change', async (leaf) => { - if (leaf) await this.onViewStateChange(leaf.view); + this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => { + if (leaf) this.onViewStateChange(leaf.view); })); this.registerEvent(this.app.workspace.on('view-sync:state-change', (view, override) => { @@ -130,7 +139,9 @@ export default class MyPlugin extends Plugin { if (!leaf) return; const data = await this.app.vault.read(file); - const viewState = JSON.parse(data); + const { timestamp, viewState } = JSON.parse(data); + + if (this.settings.syncOnlyIfNewer && this.#lastViewStateSave >= timestamp) return; await leaf.setViewState(viewState); if ('eState' in viewState) { @@ -171,7 +182,10 @@ export default class MyPlugin extends Plugin { this.registerEvent(this.app.vault.on('modify', async (file) => { if (this.settings.watchAnotherWorkspace && file instanceof TFile && normalizePath(this.settings.watchWorkspacePath) === file.path) { const data = await this.app.vault.read(file); - const layout = JSON.parse(data); + const { timestamp, layout } = JSON.parse(data); + + if (this.settings.syncWorkspaceOnlyIfNewer && this.#lastWorkspaceLayoutSave >= timestamp) return; + await this.app.workspace.changeLayout(layout); } })); diff --git a/src/settings.ts b/src/settings.ts index a3b1732..407ca61 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,5 @@ import { Component, IconName, MarkdownRenderer, Platform, PluginSettingTab, Setting, TFile, setIcon } from 'obsidian'; -import MyPlugin from 'main'; +import ViewSyncPlugin from 'main'; export interface ViewSyncSettings { @@ -7,10 +7,12 @@ export interface ViewSyncSettings { viewTypes: string[]; watchAnother: boolean; watchPath: string; + syncOnlyIfNewer: boolean; shareAfterSync: boolean; ownWorkspacePath: string; watchAnotherWorkspace: boolean; watchWorkspacePath: string; + syncWorkspaceOnlyIfNewer: boolean; } export const DEFAULT_SETTINGS: ViewSyncSettings = { @@ -18,10 +20,12 @@ export const DEFAULT_SETTINGS: ViewSyncSettings = { viewTypes: ['markdown', 'canvas', 'pdf'], watchAnother: false, watchPath: '', + syncOnlyIfNewer: false, shareAfterSync: false, ownWorkspacePath: '', watchAnotherWorkspace: false, watchWorkspacePath: '', + syncWorkspaceOnlyIfNewer: false, }; // Inspired by https://stackoverflow.com/a/50851710/13613783 @@ -32,7 +36,7 @@ export class ViewSyncSettingTab extends PluginSettingTab { items: Partial>; promises: Promise[]; - constructor(public plugin: MyPlugin) { + constructor(public plugin: ViewSyncPlugin) { super(plugin.app, plugin); this.component = new Component(); this.items = {}; @@ -208,7 +212,7 @@ export class ViewSyncSettingTab extends PluginSettingTab { .setDesc('Comma-separated list of view types to record. Other types of views will be ignored. Required if this device is the main device (= followed by other devices). Note that view types are case-sensitive. To get the type of the active view, you can run the "Copy active view type" command.'); this.addToggleSetting('watchAnother', () => this.redisplay()) .setName('Follow another device') - .setDesc('Note: It might be problematic if you let two devices follow each other. I recommend a one-way sync: one main device and one or more follower devices.'); + .setDesc('Note: It might be problematic if you let two devices follow each other. I recommend a one-way sync: one main device and one or more follower devices. If you want them to follow each other, make sure to turn on the "Sync only if newer ..." option on both devices to avoid conflicts.'); if (this.settings.watchAnother) { this.addPathSetting('watchPath', `view-sync-${exampleFollowedStr}.json`) @@ -218,6 +222,9 @@ export class ViewSyncSettingTab extends PluginSettingTab { .setName('Show "Share" menu after sync') .setDesc('Useful for drawing on PDF files on tablet, for example.') } + this.addToggleSetting('syncOnlyIfNewer') + .setName('Sync only if newer than this device\'s last view state update') + .setDesc('If this device has a newer view state than the followed device, the view state will not be updated.'); } this.addHeading('Workspace sync', 'lucide-layout'); @@ -231,7 +238,7 @@ export class ViewSyncSettingTab extends PluginSettingTab { this.renderMarkdown([ 'Note: ', '', - '- It might be problematic if you let two devices follow each other. I recommend a one-way sync: one main device and one or more follower devices.', + '- It might be problematic if you let two devices follow each other. I recommend a one-way sync: one main device and one or more follower devices. If you want them to follow each other, make sure to turn on the "Sync only if newer ..." option on both devices to avoid conflicts.', '- It is not recommended to make a mobile device follow a desktop device or vice versa.', ], setting.descEl); }); @@ -239,6 +246,9 @@ export class ViewSyncSettingTab extends PluginSettingTab { if (this.settings.watchAnotherWorkspace) { this.addPathSetting('watchWorkspacePath', `workspace-sync-${exampleFollowedStr}.json`) .setName('Path of the workspace layout file for the followed device'); + this.addToggleSetting('syncWorkspaceOnlyIfNewer') + .setName('Sync only if newer than this device\'s last workspace layout update') + .setDesc('If this device has a newer workspace layout than the followed device, the workspace layout will not be updated.'); } this.addFundingButton();