Extract library from plugin
This commit is contained in:
parent
8374c971ee
commit
ae3acb9e1e
37 changed files with 61 additions and 77 deletions
68
obsidian-plugin/src/obisidan-event-handler.ts
Normal file
68
obsidian-plugin/src/obisidan-event-handler.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import type { Syncer } from "sync-client";
|
||||
import { Logger } from "sync-client";
|
||||
import type { TAbstractFile } from "obsidian";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
export class ObsidianFileEventHandler {
|
||||
public constructor(private readonly syncer: Syncer) {}
|
||||
|
||||
public async onCreate(file: TAbstractFile): Promise<void> {
|
||||
if (file instanceof TFile) {
|
||||
Logger.getInstance().info(`File created: ${file.path}`);
|
||||
|
||||
await this.syncer.syncLocallyCreatedFile(
|
||||
file.path,
|
||||
new Date(file.stat.ctime)
|
||||
);
|
||||
} else {
|
||||
Logger.getInstance().debug(`Folder created: ${file.path}, ignored`);
|
||||
}
|
||||
}
|
||||
|
||||
public async onDelete(file: TAbstractFile): Promise<void> {
|
||||
if (file instanceof TFile) {
|
||||
Logger.getInstance().info(`File deleted: ${file.path}`);
|
||||
|
||||
await this.syncer.syncLocallyDeletedFile(file.path);
|
||||
} else {
|
||||
Logger.getInstance().debug(`Folder deleted: ${file.path}, ignored`);
|
||||
}
|
||||
}
|
||||
|
||||
public async onRename(file: TAbstractFile, oldPath: string): Promise<void> {
|
||||
if (file instanceof TFile) {
|
||||
Logger.getInstance().info(
|
||||
`File renamed: ${oldPath} -> ${file.path}`
|
||||
);
|
||||
|
||||
await this.syncer.syncLocallyUpdatedFile({
|
||||
oldPath,
|
||||
relativePath: file.path,
|
||||
updateTime: new Date(file.stat.ctime)
|
||||
});
|
||||
} else {
|
||||
Logger.getInstance().debug(
|
||||
`Folder renamed: ${oldPath} -> ${file.path}, ignored`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async onModify(file: TAbstractFile): Promise<void> {
|
||||
if (file instanceof TFile) {
|
||||
if (file.basename.startsWith("console-log.iPhone")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.getInstance().info(`File modified: ${file.path}`);
|
||||
|
||||
await this.syncer.syncLocallyUpdatedFile({
|
||||
relativePath: file.path,
|
||||
updateTime: new Date(file.stat.ctime)
|
||||
});
|
||||
} else {
|
||||
Logger.getInstance().debug(
|
||||
`Folder modified: ${file.path}, ignored`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
165
obsidian-plugin/src/obsidian-file-operations.ts
Normal file
165
obsidian-plugin/src/obsidian-file-operations.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import type { Stat, Vault } from "obsidian";
|
||||
import { normalizePath } from "obsidian";
|
||||
import { Platform } from "obsidian";
|
||||
import type { FileOperations, RelativePath } from "sync-client";
|
||||
import { Logger, isFileTypeMergable, mergeText } from "sync-client";
|
||||
|
||||
export class ObsidianFileOperations implements FileOperations {
|
||||
public constructor(private readonly vault: Vault) {}
|
||||
|
||||
public async listAllFiles(): Promise<RelativePath[]> {
|
||||
const files = this.vault.getFiles();
|
||||
Logger.getInstance().debug(`Listing all files, found ${files.length}`);
|
||||
return files.map((file) => file.path);
|
||||
}
|
||||
|
||||
public async read(path: RelativePath): Promise<Uint8Array> {
|
||||
Logger.getInstance().debug(`Reading file: ${path}`);
|
||||
if (isFileTypeMergable(path)) {
|
||||
let text = await this.vault.adapter.read(normalizePath(path));
|
||||
|
||||
text = text.replace(/\r\n/g, "\n");
|
||||
|
||||
return new TextEncoder().encode(text);
|
||||
}
|
||||
return new Uint8Array(
|
||||
await this.vault.adapter.readBinary(normalizePath(path))
|
||||
);
|
||||
}
|
||||
|
||||
public async getFileSize(path: RelativePath): Promise<number> {
|
||||
Logger.getInstance().debug(`Getting file size: ${path}`);
|
||||
return (await this.statFile(path)).size;
|
||||
}
|
||||
|
||||
public async getModificationTime(path: RelativePath): Promise<Date> {
|
||||
Logger.getInstance().debug(`Getting modification time: ${path}`);
|
||||
return new Date((await this.statFile(path)).mtime);
|
||||
}
|
||||
|
||||
public async exists(path: RelativePath): Promise<boolean> {
|
||||
Logger.getInstance().debug(`Checking existance of ${path}`);
|
||||
return this.vault.adapter.exists(normalizePath(path));
|
||||
}
|
||||
|
||||
public async create(
|
||||
path: RelativePath,
|
||||
newContent: Uint8Array
|
||||
): Promise<void> {
|
||||
Logger.getInstance().debug(`Creating file: ${path}`);
|
||||
if (await this.vault.adapter.exists(normalizePath(path))) {
|
||||
Logger.getInstance().debug(
|
||||
`Didn't expect ${path} to exist, when trying to create it, merging instead`
|
||||
);
|
||||
await this.write(path, new Uint8Array(0), newContent);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.createParentDirectories(normalizePath(path));
|
||||
await this.vault.adapter.writeBinary(normalizePath(path), newContent);
|
||||
}
|
||||
|
||||
public async write(
|
||||
path: RelativePath,
|
||||
expectedContent: Uint8Array,
|
||||
newContent: Uint8Array
|
||||
): Promise<Uint8Array> {
|
||||
Logger.getInstance().debug(`Writing file: ${path}`);
|
||||
if (!(await this.vault.adapter.exists(normalizePath(path)))) {
|
||||
Logger.getInstance().debug(
|
||||
`The caller assumed ${path} exists, but it no longer, so we wont recreate it`
|
||||
);
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
if (!isFileTypeMergable(path)) {
|
||||
Logger.getInstance().debug(
|
||||
`The expected content is not mergable, so we won't perform a 3-way merge, just overwrite it`
|
||||
);
|
||||
await this.vault.adapter.writeBinary(
|
||||
normalizePath(path),
|
||||
newContent
|
||||
);
|
||||
return newContent;
|
||||
}
|
||||
|
||||
const expetedText = new TextDecoder().decode(expectedContent);
|
||||
const newText = new TextDecoder().decode(newContent);
|
||||
|
||||
const resultText = await this.vault.adapter.process(
|
||||
normalizePath(path),
|
||||
(currentText) => {
|
||||
currentText = currentText.replace(/\r\n/g, "\n");
|
||||
if (currentText !== expetedText) {
|
||||
Logger.getInstance().debug(
|
||||
`Performing a 3-way merge for ${path} with the expected content`
|
||||
);
|
||||
|
||||
return mergeText(expetedText, currentText, newText);
|
||||
}
|
||||
|
||||
Logger.getInstance().debug(
|
||||
`The current content of ${path} is the same as the expected content, so we will just write the new content`
|
||||
);
|
||||
|
||||
return newText;
|
||||
}
|
||||
);
|
||||
return new TextEncoder().encode(resultText);
|
||||
}
|
||||
|
||||
public async remove(path: RelativePath): Promise<void> {
|
||||
Logger.getInstance().debug(`Removing file: ${path}`);
|
||||
if (await this.vault.adapter.exists(normalizePath(path))) {
|
||||
await this.vault.adapter.trashSystem(normalizePath(path));
|
||||
}
|
||||
}
|
||||
|
||||
public async move(
|
||||
oldPath: RelativePath,
|
||||
newPath: RelativePath
|
||||
): Promise<void> {
|
||||
oldPath = normalizePath(oldPath);
|
||||
newPath = normalizePath(newPath);
|
||||
|
||||
Logger.getInstance().debug(`Moving file: ${oldPath} -> ${newPath}`);
|
||||
|
||||
if (oldPath === newPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.createParentDirectories(newPath);
|
||||
await this.vault.adapter.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
public isFileEligibleForSync(path: RelativePath): boolean {
|
||||
if (Platform.isDesktopApp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isFileTypeMergable(path);
|
||||
}
|
||||
|
||||
private async statFile(path: string): Promise<Stat> {
|
||||
const file = await this.vault.adapter.stat(normalizePath(path));
|
||||
|
||||
if (!file) {
|
||||
throw new Error(`File not found: ${path}`);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private async createParentDirectories(path: string): Promise<void> {
|
||||
const components = path.split("/");
|
||||
if (components.length === 1) {
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i < components.length; i++) {
|
||||
const parentDir = components.slice(0, i).join("/");
|
||||
if (!(await this.vault.adapter.exists(parentDir))) {
|
||||
await this.vault.adapter.mkdir(parentDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
179
obsidian-plugin/src/styles.scss
Normal file
179
obsidian-plugin/src/styles.scss
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
@mixin number-card {
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--color-base-30);
|
||||
font-size: var(--font-ui-small);
|
||||
|
||||
&.good {
|
||||
background-color: rgba(var(--color-green-rgb), 0.35);
|
||||
}
|
||||
|
||||
&.bad {
|
||||
background-color: rgba(var(--color-red-rgb), 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.status-description {
|
||||
margin: var(--p-spacing) 0;
|
||||
|
||||
.number {
|
||||
@include number-card;
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: var(--bold-weight);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.logs-container {
|
||||
max-width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.log-message {
|
||||
font: var(--font-monospace);
|
||||
margin-bottom: var(--size-2-1);
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
.timestamp {
|
||||
@include number-card;
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: var(--bold-weight);
|
||||
margin-right: var(--size-4-1);
|
||||
}
|
||||
|
||||
&.DEBUG {
|
||||
color: var(--color-base-50);
|
||||
}
|
||||
|
||||
&.INFO {
|
||||
color: var(--color-green-rgb);
|
||||
}
|
||||
|
||||
&.WARNING {
|
||||
color: var(--color-yellow-rgb);
|
||||
}
|
||||
|
||||
&.ERROR {
|
||||
color: var(--color-red-rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-card {
|
||||
padding: var(--size-4-4);
|
||||
margin: var(--size-4-2);
|
||||
background-color: var(--color-base-00);
|
||||
border-radius: var(--radius-l);
|
||||
container-type: inline-size;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: rgba(var(--color-green-rgb), 0.2);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: rgba(var(--color-red-rgb), 0.2);
|
||||
}
|
||||
|
||||
.history-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--size-4-2);
|
||||
gap: var(--size-4-2);
|
||||
|
||||
@container (max-width: 300px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.history-card-title {
|
||||
font: var(--font-monospace);
|
||||
display: flex;
|
||||
gap: var(--size-4-2);
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-card-timestamp {
|
||||
font-size: var(--font-ui-small);
|
||||
font-style: italic;
|
||||
color: var(--italic-color);
|
||||
}
|
||||
}
|
||||
|
||||
.history-card-message {
|
||||
font-size: var(--font-ui-medium);
|
||||
color: var(--color-base-70);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
200
obsidian-plugin/src/vault-link-plugin.ts
Normal file
200
obsidian-plugin/src/vault-link-plugin.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import type { WorkspaceLeaf } from "obsidian";
|
||||
import { Plugin } from "obsidian";
|
||||
import "./styles.scss";
|
||||
import "../manifest.json";
|
||||
|
||||
import { SyncSettingsTab } from "./views/settings-tab";
|
||||
import { HistoryView } from "./views/history-view";
|
||||
import { ObsidianFileEventHandler } from "./obisidan-event-handler";
|
||||
import { ObsidianFileOperations } from "./obsidian-file-operations";
|
||||
import { StatusBar } from "./views/status-bar";
|
||||
|
||||
import { LogsView } from "./views/logs-view";
|
||||
import { StatusDescription } from "./views/status-description";
|
||||
import {
|
||||
applyRemoteChangesLocally,
|
||||
Database,
|
||||
Logger,
|
||||
Syncer,
|
||||
SyncHistory,
|
||||
SyncService,
|
||||
initialize
|
||||
} from "sync-client";
|
||||
|
||||
export default class VaultLinkPlugin extends Plugin {
|
||||
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");
|
||||
|
||||
await initialize();
|
||||
|
||||
const database = new Database(
|
||||
await this.loadData(),
|
||||
this.saveData.bind(this)
|
||||
);
|
||||
|
||||
const syncService = new SyncService(database);
|
||||
|
||||
const syncer = new Syncer(
|
||||
database,
|
||||
syncService,
|
||||
this.operations,
|
||||
this.history
|
||||
);
|
||||
|
||||
const statusDescription = new StatusDescription(
|
||||
database,
|
||||
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);
|
||||
|
||||
this.registerView(
|
||||
HistoryView.TYPE,
|
||||
(leaf) => new HistoryView(leaf, database, this.history)
|
||||
);
|
||||
this.registerView(
|
||||
LogsView.TYPE,
|
||||
(leaf) => new LogsView(this, database, leaf)
|
||||
);
|
||||
|
||||
this.addRibbonIcon(
|
||||
HistoryView.ICON,
|
||||
"Open VaultLink events",
|
||||
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
|
||||
);
|
||||
this.addRibbonIcon(
|
||||
LogsView.ICON,
|
||||
"Open VaultLink logs",
|
||||
async (_: MouseEvent) => this.activateView(LogsView.TYPE)
|
||||
);
|
||||
|
||||
const eventHandler = new ObsidianFileEventHandler(syncer);
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
Logger.getInstance().info("Initialising sync handlers");
|
||||
|
||||
[
|
||||
this.app.vault.on(
|
||||
"create",
|
||||
eventHandler.onCreate.bind(eventHandler)
|
||||
),
|
||||
this.app.vault.on(
|
||||
"modify",
|
||||
eventHandler.onModify.bind(eventHandler)
|
||||
),
|
||||
this.app.vault.on(
|
||||
"delete",
|
||||
eventHandler.onDelete.bind(eventHandler)
|
||||
),
|
||||
this.app.vault.on(
|
||||
"rename",
|
||||
eventHandler.onRename.bind(eventHandler)
|
||||
)
|
||||
].forEach((event) => {
|
||||
this.registerEvent(event);
|
||||
});
|
||||
|
||||
Logger.getInstance().info("Sync handlers initialised");
|
||||
|
||||
void syncer.scheduleSyncForOfflineChanges();
|
||||
});
|
||||
|
||||
this.registerRemoteEventListener(
|
||||
database,
|
||||
syncService,
|
||||
syncer,
|
||||
database.getSettings().fetchChangesUpdateIntervalMs
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
database.addOnSettingsChangeHandlers(async (settings, oldSettings) => {
|
||||
this.registerRemoteEventListener(
|
||||
database,
|
||||
syncService,
|
||||
syncer,
|
||||
settings.fetchChangesUpdateIntervalMs
|
||||
);
|
||||
|
||||
if (!oldSettings.isSyncEnabled && settings.isSyncEnabled) {
|
||||
await syncer.scheduleSyncForOfflineChanges();
|
||||
}
|
||||
});
|
||||
|
||||
Logger.getInstance().info("Plugin loaded");
|
||||
}
|
||||
|
||||
public onunload(): void {
|
||||
if (this.remoteListenerIntervalId !== null) {
|
||||
window.clearInterval(this.remoteListenerIntervalId);
|
||||
}
|
||||
}
|
||||
|
||||
public openSettings(): void {
|
||||
// eslint-disable-next-line
|
||||
(this.app as any).setting.open(); // this is undocumented
|
||||
// eslint-disable-next-line
|
||||
(this.app as any).setting.openTab(this.settingsTab); // this is undocumented
|
||||
}
|
||||
|
||||
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;
|
||||
const leaves = workspace.getLeavesOfType(type);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
[leaf] = leaves;
|
||||
} else {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: type, active: true });
|
||||
}
|
||||
|
||||
if (leaf) {
|
||||
await workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
private registerRemoteEventListener(
|
||||
database: Database,
|
||||
syncService: SyncService,
|
||||
syncer: Syncer,
|
||||
intervalMs: number
|
||||
): void {
|
||||
if (this.remoteListenerIntervalId !== null) {
|
||||
window.clearInterval(this.remoteListenerIntervalId);
|
||||
}
|
||||
|
||||
this.remoteListenerIntervalId = window.setInterval(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
async () =>
|
||||
applyRemoteChangesLocally({
|
||||
database,
|
||||
syncService,
|
||||
syncer
|
||||
}),
|
||||
intervalMs
|
||||
);
|
||||
}
|
||||
}
|
||||
168
obsidian-plugin/src/views/history-view.ts
Normal file
168
obsidian-plugin/src/views/history-view.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import type { IconName, WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView, setIcon } from "obsidian";
|
||||
|
||||
import { intlFormatDistance } from "date-fns";
|
||||
import type {
|
||||
SyncHistory,
|
||||
HistoryEntry,
|
||||
Database,
|
||||
RelativePath
|
||||
} from "sync-client";
|
||||
import { SyncType, SyncSource, SyncStatus } from "sync-client";
|
||||
|
||||
export class HistoryView extends ItemView {
|
||||
public static readonly TYPE = "history-view";
|
||||
public static readonly ICON = "square-stack";
|
||||
private timer: NodeJS.Timer | null = null;
|
||||
|
||||
public constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private readonly database: Database,
|
||||
private readonly history: SyncHistory
|
||||
) {
|
||||
super(leaf);
|
||||
this.icon = HistoryView.ICON;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
history.addSyncHistoryUpdateListener(async () => {
|
||||
await this.updateView();
|
||||
});
|
||||
}
|
||||
|
||||
private static getSyncTypeIcon(type: SyncType | undefined): IconName {
|
||||
switch (type) {
|
||||
case SyncType.CREATE:
|
||||
return "file-plus";
|
||||
case SyncType.DELETE:
|
||||
return "trash-2";
|
||||
case SyncType.UPDATE:
|
||||
return "file-pen-line";
|
||||
case undefined:
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static getSyncSourceIcon(source: SyncSource | undefined): IconName {
|
||||
switch (source) {
|
||||
case SyncSource.PUSH:
|
||||
return "upload";
|
||||
case SyncSource.PULL:
|
||||
return "download";
|
||||
case undefined:
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static renderSyncItemTitle(
|
||||
element: HTMLElement,
|
||||
entry: HistoryEntry
|
||||
): void {
|
||||
const syncTypeIcon = HistoryView.getSyncTypeIcon(entry.type);
|
||||
if (syncTypeIcon) {
|
||||
setIcon(element.createDiv(), syncTypeIcon);
|
||||
}
|
||||
|
||||
element.createEl("span", {
|
||||
text: entry.relativePath
|
||||
});
|
||||
|
||||
const syncSourceIcon = HistoryView.getSyncSourceIcon(entry.source);
|
||||
if (syncSourceIcon) {
|
||||
setIcon(element.createDiv(), syncSourceIcon);
|
||||
}
|
||||
}
|
||||
|
||||
public getViewType(): string {
|
||||
return HistoryView.TYPE;
|
||||
}
|
||||
|
||||
public getDisplayText(): string {
|
||||
return "VaultLink history";
|
||||
}
|
||||
|
||||
public async onOpen(): Promise<void> {
|
||||
await this.updateView();
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.timer = setInterval(async () => this.updateView(), 1000);
|
||||
}
|
||||
|
||||
public async onClose(): Promise<void> {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateView(): Promise<void> {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.createEl("h4", { text: "VaultLink History" });
|
||||
|
||||
const entries = this.history
|
||||
.getEntries()
|
||||
.reverse()
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.status !== SyncStatus.NO_OP ||
|
||||
this.database.getSettings().displayNoopSyncEvents
|
||||
);
|
||||
|
||||
entries.forEach((entry) => {
|
||||
container.createDiv(
|
||||
{
|
||||
cls: ["history-card", entry.status.toLocaleLowerCase()]
|
||||
},
|
||||
(card) => {
|
||||
if (
|
||||
this.app.vault.getFileByPath(entry.relativePath) !==
|
||||
null
|
||||
) {
|
||||
card.addEventListener("click", () => {
|
||||
void this.app.workspace.openLinkText(
|
||||
entry.relativePath,
|
||||
entry.relativePath,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
card.addClass("clickable");
|
||||
}
|
||||
|
||||
card.createDiv(
|
||||
{
|
||||
cls: "history-card-header"
|
||||
},
|
||||
(header) => {
|
||||
header.createEl(
|
||||
"h5",
|
||||
{
|
||||
cls: "history-card-title"
|
||||
},
|
||||
(title) => {
|
||||
HistoryView.renderSyncItemTitle(
|
||||
title,
|
||||
entry
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
header.createSpan({
|
||||
text: intlFormatDistance(
|
||||
entry.timestamp,
|
||||
new Date()
|
||||
),
|
||||
cls: "history-card-timestamp"
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
card.createEl("p", {
|
||||
text: `${entry.message}.`,
|
||||
cls: "history-card-message"
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
120
obsidian-plugin/src/views/logs-view.ts
Normal file
120
obsidian-plugin/src/views/logs-view.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import type { WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView } from "obsidian";
|
||||
import type VaultLinkPlugin from "src/vault-link-plugin";
|
||||
import type { Database } from "sync-client";
|
||||
import { Logger } from "sync-client";
|
||||
|
||||
export class LogsView extends ItemView {
|
||||
public static readonly TYPE = "logs-view";
|
||||
public static readonly ICON = "logs";
|
||||
|
||||
public constructor(
|
||||
private readonly plugin: VaultLinkPlugin,
|
||||
private readonly database: Database,
|
||||
leaf: WorkspaceLeaf
|
||||
) {
|
||||
super(leaf);
|
||||
this.icon = LogsView.ICON;
|
||||
Logger.getInstance().addOnMessageListener(() => {
|
||||
this.updateView();
|
||||
});
|
||||
|
||||
database.addOnSettingsChangeHandlers((newSettings, oldSettings) => {
|
||||
if (newSettings.minimumLogLevel !== oldSettings.minimumLogLevel) {
|
||||
this.updateView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static formatTimestamp(timestamp: Date): string {
|
||||
return timestamp.toTimeString().split(" ")[0];
|
||||
}
|
||||
|
||||
public getViewType(): string {
|
||||
return LogsView.TYPE;
|
||||
}
|
||||
|
||||
public getDisplayText(): string {
|
||||
return "VaultLink logs";
|
||||
}
|
||||
|
||||
public async onOpen(): Promise<void> {
|
||||
this.updateView();
|
||||
|
||||
const container = this.containerEl.children[1];
|
||||
container.addClass("logs-view");
|
||||
}
|
||||
|
||||
private updateView(): void {
|
||||
const container = this.containerEl.children[1];
|
||||
|
||||
let logsContainer = container
|
||||
.getElementsByClassName("logs-container")
|
||||
.item(0);
|
||||
const scrollPosition = logsContainer?.scrollTop;
|
||||
|
||||
container.empty();
|
||||
|
||||
container.createEl("h4", { text: "VaultLink logs" });
|
||||
container.createEl(
|
||||
"p",
|
||||
{
|
||||
text: "This view displays logs generated by VaultLink. You can set the log level in the "
|
||||
},
|
||||
(p) => {
|
||||
p.createEl(
|
||||
"a",
|
||||
{
|
||||
text: "settings"
|
||||
},
|
||||
(button) => {
|
||||
button.addEventListener("click", () => {
|
||||
this.plugin.openSettings();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
p.createSpan({ text: "." });
|
||||
}
|
||||
);
|
||||
|
||||
const logs = Logger.getInstance().getMessages(
|
||||
this.database.getSettings().minimumLogLevel
|
||||
);
|
||||
|
||||
if (logs.length === 0) {
|
||||
container.createEl("p", { text: "No logs available yet." });
|
||||
return;
|
||||
}
|
||||
|
||||
logsContainer = container.createDiv(
|
||||
{ cls: "logs-container" },
|
||||
(element) => {
|
||||
logs.forEach((message) =>
|
||||
element.createDiv(
|
||||
{
|
||||
cls: ["log-message", message.level]
|
||||
},
|
||||
(messageContainer) => {
|
||||
messageContainer.createEl("span", {
|
||||
text: LogsView.formatTimestamp(
|
||||
message.timestamp
|
||||
),
|
||||
cls: "timestamp"
|
||||
});
|
||||
messageContainer.createEl("span", {
|
||||
text: message.message
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (scrollPosition !== undefined) {
|
||||
logsContainer.scrollTop = scrollPosition;
|
||||
} else {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
342
obsidian-plugin/src/views/settings-tab.ts
Normal file
342
obsidian-plugin/src/views/settings-tab.ts
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
import type { App } from "obsidian";
|
||||
import { Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
|
||||
import type VaultLinkPlugin from "src/vault-link-plugin";
|
||||
import type { StatusDescription } from "./status-description";
|
||||
import { LogsView } from "./logs-view";
|
||||
import { HistoryView } from "./history-view";
|
||||
import type { SyncService, Syncer, Database } from "sync-client";
|
||||
import { Logger, LogLevel } from "sync-client";
|
||||
|
||||
export class SyncSettingsTab extends PluginSettingTab {
|
||||
private editedVaultName: string;
|
||||
|
||||
private readonly plugin: VaultLinkPlugin;
|
||||
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: VaultLinkPlugin;
|
||||
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) => {
|
||||
if (newSettings.vaultName !== oldSettings.vaultName) {
|
||||
this.editedVaultName = newSettings.vaultName;
|
||||
this.display();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.addClass("vault-link-settings");
|
||||
|
||||
this.renderSettingsHeader(containerEl);
|
||||
this.renderConnectionSettings(containerEl);
|
||||
this.renderSyncSettings(containerEl);
|
||||
this.renderViewSettings(containerEl);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
super.hide();
|
||||
this.setStatusDescriptionSubscription();
|
||||
}
|
||||
|
||||
private renderSettingsHeader(containerEl: HTMLElement): void {
|
||||
containerEl.createEl("h2", { text: "VaultLink" }).createSpan({
|
||||
text: this.plugin.manifest.version,
|
||||
cls: "version"
|
||||
});
|
||||
|
||||
containerEl.createDiv(
|
||||
{
|
||||
cls: "description"
|
||||
},
|
||||
(descriptionContainer) => {
|
||||
this.setStatusDescriptionSubscription((): void => {
|
||||
this.statusDescription.renderStatusDescription(
|
||||
descriptionContainer
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private renderConnectionSettings(containerEl: HTMLElement): void {
|
||||
containerEl.createEl("h3", { text: "Connection" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.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")
|
||||
.setValue(this.database.getSettings().remoteUri)
|
||||
.onChange(async (value) =>
|
||||
this.database.setSetting("remoteUri", value)
|
||||
)
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Test connection").onClick(async () => {
|
||||
new Notice(
|
||||
(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(
|
||||
"Set the name of the remote vault that you want to sync with"
|
||||
)
|
||||
.setTooltip("todo, links to dcocs")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("My Obsidian Vault")
|
||||
.setValue(this.database.getSettings().vaultName)
|
||||
.onChange((value) => (this.editedVaultName = value))
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Apply").onClick(async () => {
|
||||
if (
|
||||
this.editedVaultName ===
|
||||
this.database.getSettings().vaultName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await this.database.setSetting(
|
||||
"vaultName",
|
||||
this.editedVaultName
|
||||
);
|
||||
await this.syncer.reset();
|
||||
Logger.getInstance().reset();
|
||||
new Notice(
|
||||
"Sync state has been reset, you will need to resync"
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private renderSyncSettings(containerEl: HTMLElement): void {
|
||||
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"
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Remote fetching frequency (seconds)")
|
||||
.setDesc(
|
||||
"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 docs")
|
||||
.addSlider((text) =>
|
||||
text
|
||||
.setLimits(0.5, 60, 0.5)
|
||||
.setDynamicTooltip()
|
||||
.setInstant(false)
|
||||
.setValue(
|
||||
this.database.getSettings()
|
||||
.fetchChangesUpdateIntervalMs / 1000
|
||||
)
|
||||
.onChange(async (value) =>
|
||||
this.database.setSetting(
|
||||
"fetchChangesUpdateIntervalMs",
|
||||
value * 1000
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Sync concurrency")
|
||||
.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."
|
||||
)
|
||||
.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("Maximum file size to be uploaded (MB)")
|
||||
.setDesc(
|
||||
"Set the maximum file size that can be uploaded to the server. Files larger than this size will be ignored."
|
||||
)
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 32, 1)
|
||||
.setDynamicTooltip()
|
||||
.setInstant(false)
|
||||
.setValue(this.database.getSettings().maxFileSizeMB)
|
||||
.onChange(async (value) =>
|
||||
this.database.setSetting("maxFileSizeMB", 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."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.database.getSettings().isSyncEnabled)
|
||||
.onChange(async (value) =>
|
||||
this.database.setSetting("isSyncEnabled", value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private renderViewSettings(containerEl: HTMLElement): void {
|
||||
containerEl.createEl("h3", { text: "View" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Show no-op sync operations in history")
|
||||
.setDesc(
|
||||
"Enabling this will make the history view more verbose while also providing more explanation for the scyning choices made."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.database.getSettings().displayNoopSyncEvents)
|
||||
.onChange(async (value) =>
|
||||
this.database.setSetting("displayNoopSyncEvents", value)
|
||||
)
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Minimum log level")
|
||||
.setDesc(
|
||||
"Set the log level for the plugin. Lower levels will show more logs."
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOptions({
|
||||
[LogLevel.DEBUG]: LogLevel.DEBUG,
|
||||
[LogLevel.INFO]: LogLevel.INFO,
|
||||
[LogLevel.WARNING]: LogLevel.WARNING,
|
||||
[LogLevel.ERROR]: LogLevel.ERROR
|
||||
})
|
||||
.setValue(this.database.getSettings().minimumLogLevel)
|
||||
.onChange(async (value) =>
|
||||
this.database.setSetting(
|
||||
"minimumLogLevel",
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
value as LogLevel
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private setStatusDescriptionSubscription(
|
||||
newSubscription?: () => void
|
||||
): void {
|
||||
if (this.statusDescriptionSubscription) {
|
||||
this.statusDescription.removeStatusChangeListener(
|
||||
this.statusDescriptionSubscription
|
||||
);
|
||||
}
|
||||
this.statusDescriptionSubscription = newSubscription;
|
||||
if (this.statusDescriptionSubscription) {
|
||||
this.statusDescriptionSubscription();
|
||||
this.statusDescription.addStatusChangeListener(
|
||||
this.statusDescriptionSubscription
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
obsidian-plugin/src/views/status-bar.ts
Normal file
73
obsidian-plugin/src/views/status-bar.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { Database, HistoryStats, SyncHistory, Syncer } from "sync-client";
|
||||
import type VaultLinkPlugin from "src/vault-link-plugin";
|
||||
|
||||
export class StatusBar {
|
||||
private readonly statusBarItem: HTMLElement;
|
||||
|
||||
private lastHistoryStats: HistoryStats | undefined;
|
||||
private lastRemaining: number | undefined;
|
||||
|
||||
public constructor(
|
||||
private readonly database: Database,
|
||||
private readonly plugin: VaultLinkPlugin,
|
||||
history: SyncHistory,
|
||||
syncer: Syncer
|
||||
) {
|
||||
this.statusBarItem = plugin.addStatusBarItem();
|
||||
history.addSyncHistoryUpdateListener((status) => {
|
||||
this.lastHistoryStats = status;
|
||||
this.updateStatus();
|
||||
});
|
||||
|
||||
syncer.addRemainingOperationsListener((remainingOperations) => {
|
||||
this.lastRemaining = remainingOperations;
|
||||
this.updateStatus();
|
||||
});
|
||||
|
||||
database.addOnSettingsChangeHandlers(() => {
|
||||
this.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
this.statusBarItem.empty();
|
||||
const container = this.statusBarItem.createDiv({
|
||||
cls: ["sync-status"]
|
||||
});
|
||||
|
||||
let hasShownMessage = false;
|
||||
|
||||
if ((this.lastRemaining ?? 0) > 0) {
|
||||
hasShownMessage = true;
|
||||
container.createSpan({ text: `${this.lastRemaining} ⏳` });
|
||||
}
|
||||
|
||||
if ((this.lastHistoryStats?.success ?? 0) > 0) {
|
||||
hasShownMessage = true;
|
||||
container.createSpan({
|
||||
text: `${this.lastHistoryStats?.success ?? 0} ✅`
|
||||
});
|
||||
}
|
||||
|
||||
if ((this.lastHistoryStats?.error ?? 0) > 0) {
|
||||
hasShownMessage = true;
|
||||
container.createSpan({
|
||||
text: `${this.lastHistoryStats?.error ?? 0} ❌`
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasShownMessage) {
|
||||
if (this.database.getSettings().isSyncEnabled) {
|
||||
container.createSpan({ text: "VaultLink is idle" });
|
||||
} else {
|
||||
const button = container.createEl("button", {
|
||||
text: "VaultLink is disabled, click to configure",
|
||||
cls: "initialize-button"
|
||||
});
|
||||
button.onclick = (): void => {
|
||||
this.plugin.openSettings();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
obsidian-plugin/src/views/status-description.ts
Normal file
140
obsidian-plugin/src/views/status-description.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import type {
|
||||
HistoryStats,
|
||||
CheckConnectionResult,
|
||||
SyncService,
|
||||
SyncHistory,
|
||||
Syncer,
|
||||
Database
|
||||
} from "sync-client";
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue