mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-03-03 15:32:25 +02:00
- New feature:
- Vault history: A tab has been implemented to give a birds-eye view of the changes that have occurred in the vault. - Improved: - Log dialogue is now shown as one of tabs. - Fixed: - Some minor issues has been fixed.
This commit is contained in:
parent
432a211f80
commit
cda90259c5
@ -482,6 +482,10 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
||||||
const term = termOverRide || this.plugin.deviceAndVaultName;
|
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||||
|
if (term == "") {
|
||||||
|
Logger("We have to configure the device name", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const vf = this.filenameToUnifiedKey(path, term);
|
const vf = this.filenameToUnifiedKey(path, term);
|
||||||
return await runWithLock(`plugin-${vf}`, false, async () => {
|
return await runWithLock(`plugin-${vf}`, false, async () => {
|
||||||
const category = this.getFileCategory(path);
|
const category = this.getFileCategory(path);
|
||||||
|
@ -3,7 +3,7 @@ import { getPathFromTFile, isValidPath } from "./utils";
|
|||||||
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
||||||
import { DocumentID, FilePathWithPrefix, LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||||
import { getDocData } from "./lib/src/utils";
|
import { getDocData } from "./lib/src/utils";
|
||||||
@ -24,12 +24,14 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
currentDoc: LoadedEntry;
|
currentDoc: LoadedEntry;
|
||||||
currentText = "";
|
currentText = "";
|
||||||
currentDeleted = false;
|
currentDeleted = false;
|
||||||
|
initialRev: string;
|
||||||
|
|
||||||
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID) {
|
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id: DocumentID, revision?: string) {
|
||||||
super(app);
|
super(app);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
|
this.file = (file instanceof TFile) ? getPathFromTFile(file) : file;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.initialRev = revision;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
this.file = this.plugin.id2path(id, null);
|
this.file = this.plugin.id2path(id, null);
|
||||||
}
|
}
|
||||||
@ -38,7 +40,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadFile() {
|
async loadFile(initialRev: string) {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
this.id = await this.plugin.path2id(this.file);
|
this.id = await this.plugin.path2id(this.file);
|
||||||
}
|
}
|
||||||
@ -49,7 +51,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
this.range.max = `${this.revs_info.length - 1}`;
|
this.range.max = `${this.revs_info.length - 1}`;
|
||||||
this.range.value = this.range.max;
|
this.range.value = this.range.max;
|
||||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||||
await this.loadRevs();
|
await this.loadRevs(initialRev);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (isErrorOfMissingDoc(ex)) {
|
if (isErrorOfMissingDoc(ex)) {
|
||||||
this.range.max = "0";
|
this.range.max = "0";
|
||||||
@ -63,18 +65,27 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async loadRevs() {
|
async loadRevs(initialRev?: string) {
|
||||||
if (this.revs_info.length == 0) return;
|
if (this.revs_info.length == 0) return;
|
||||||
const db = this.plugin.localDatabase;
|
if (initialRev) {
|
||||||
|
const rIndex = this.revs_info.findIndex(e => e.rev == initialRev);
|
||||||
|
if (rIndex >= 0) {
|
||||||
|
this.range.value = `${this.revs_info.length - 1 - rIndex}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||||
const rev = this.revs_info[index];
|
const rev = this.revs_info[index];
|
||||||
const w = await db.getDBEntry(this.file, { rev: rev.rev }, false, false, true);
|
await this.showExactRev(rev.rev);
|
||||||
|
}
|
||||||
|
async showExactRev(rev: string) {
|
||||||
|
const db = this.plugin.localDatabase;
|
||||||
|
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||||
this.currentText = "";
|
this.currentText = "";
|
||||||
this.currentDeleted = false;
|
this.currentDeleted = false;
|
||||||
if (w === false) {
|
if (w === false) {
|
||||||
this.currentDeleted = true;
|
this.currentDeleted = true;
|
||||||
this.info.innerHTML = "";
|
this.info.innerHTML = "";
|
||||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`;
|
this.contentView.innerHTML = `Could not read this revision<br>(${rev})`;
|
||||||
} else {
|
} else {
|
||||||
this.currentDoc = w;
|
this.currentDoc = w;
|
||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||||
@ -158,7 +169,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
.addClass("op-info");
|
.addClass("op-info");
|
||||||
this.info = contentEl.createDiv("");
|
this.info = contentEl.createDiv("");
|
||||||
this.info.addClass("op-info");
|
this.info.addClass("op-info");
|
||||||
this.loadFile();
|
this.loadFile(this.initialRev);
|
||||||
const div = contentEl.createDiv({ text: "Loading old revisions..." });
|
const div = contentEl.createDiv({ text: "Loading old revisions..." });
|
||||||
this.contentView = div;
|
this.contentView = div;
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
|
328
src/GlobalHistory.svelte
Normal file
328
src/GlobalHistory.svelte
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import type { AnyEntry, FilePathWithPrefix } from "./lib/src/types";
|
||||||
|
import { getDocData, isDocContentSame } from "./lib/src/utils";
|
||||||
|
import { diff_match_patch } from "diff-match-patch";
|
||||||
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
|
import { isPlainText, stripAllPrefixes } from "./lib/src/path";
|
||||||
|
import { TFile } from "./deps";
|
||||||
|
import { arrayBufferToBase64 } from "./lib/src/strbin";
|
||||||
|
export let plugin: ObsidianLiveSyncPlugin;
|
||||||
|
|
||||||
|
let showDiffInfo = false;
|
||||||
|
let showChunkCorrected = false;
|
||||||
|
let checkStorageDiff = false;
|
||||||
|
|
||||||
|
let range_from_epoch = Date.now() - 3600000 * 24 * 7;
|
||||||
|
let range_to_epoch = Date.now() + 3600000 * 24 * 2;
|
||||||
|
const timezoneOffset = new Date().getTimezoneOffset();
|
||||||
|
let dispDateFrom = new Date(range_from_epoch - timezoneOffset).toISOString().split("T")[0];
|
||||||
|
let dispDateTo = new Date(range_to_epoch - timezoneOffset).toISOString().split("T")[0];
|
||||||
|
$: {
|
||||||
|
range_from_epoch = new Date(dispDateFrom).getTime() + timezoneOffset;
|
||||||
|
range_to_epoch = new Date(dispDateTo).getTime() + timezoneOffset;
|
||||||
|
|
||||||
|
getHistory(showDiffInfo, showChunkCorrected, checkStorageDiff);
|
||||||
|
}
|
||||||
|
function mtimeToDate(mtime: number) {
|
||||||
|
return new Date(mtime).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
type HistoryData = {
|
||||||
|
id: string;
|
||||||
|
rev: string;
|
||||||
|
path: string;
|
||||||
|
dirname: string;
|
||||||
|
filename: string;
|
||||||
|
mtime: number;
|
||||||
|
mtimeDisp: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
size: number;
|
||||||
|
changes: string;
|
||||||
|
chunks: string;
|
||||||
|
isPlain: boolean;
|
||||||
|
};
|
||||||
|
let history = [] as HistoryData[];
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
async function fetchChanges(): Promise<HistoryData[]> {
|
||||||
|
try {
|
||||||
|
const db = plugin.localDatabase;
|
||||||
|
let result = [] as typeof history;
|
||||||
|
for await (const docA of db.findAllNormalDocs()) {
|
||||||
|
if (docA.mtime < range_from_epoch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (docA.type != "newnote" && docA.type != "plain") continue;
|
||||||
|
const path = plugin.getPath(docA as AnyEntry);
|
||||||
|
const isPlain = isPlainText(docA.path);
|
||||||
|
const revs = await db.getRaw(docA._id, { revs_info: true });
|
||||||
|
let p: string = undefined;
|
||||||
|
const reversedRevs = revs._revs_info.reverse();
|
||||||
|
const DIFF_DELETE = -1;
|
||||||
|
|
||||||
|
const DIFF_EQUAL = 0;
|
||||||
|
const DIFF_INSERT = 1;
|
||||||
|
|
||||||
|
for (const revInfo of reversedRevs) {
|
||||||
|
if (revInfo.status == "available") {
|
||||||
|
const doc =
|
||||||
|
(!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev)
|
||||||
|
? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true)
|
||||||
|
: await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
|
||||||
|
if (doc === false) continue;
|
||||||
|
const rev = revInfo.rev;
|
||||||
|
|
||||||
|
const mtime = "mtime" in doc ? doc.mtime : 0;
|
||||||
|
if (range_from_epoch > mtime) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (range_to_epoch < mtime) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffDetail = "";
|
||||||
|
if (showDiffInfo && !isPlain) {
|
||||||
|
const data = getDocData(doc.data);
|
||||||
|
if (p === undefined) {
|
||||||
|
p = data;
|
||||||
|
}
|
||||||
|
if (p != data) {
|
||||||
|
const dmp = new diff_match_patch();
|
||||||
|
const diff = dmp.diff_main(p, data);
|
||||||
|
dmp.diff_cleanupSemantic(diff);
|
||||||
|
p = data;
|
||||||
|
const pxinit = {
|
||||||
|
[DIFF_DELETE]: 0,
|
||||||
|
[DIFF_EQUAL]: 0,
|
||||||
|
[DIFF_INSERT]: 0,
|
||||||
|
} as { [key: number]: number };
|
||||||
|
const px = diff.reduce((p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }), pxinit);
|
||||||
|
diffDetail = `-${px[DIFF_DELETE]}, +${px[DIFF_INSERT]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isDeleted = doc._deleted || (doc as any)?.deleted || false;
|
||||||
|
if (isDeleted) {
|
||||||
|
diffDetail += " 🗑️";
|
||||||
|
}
|
||||||
|
if (rev == docA._rev) {
|
||||||
|
if (checkStorageDiff) {
|
||||||
|
const abs = plugin.app.vault.getAbstractFileByPath(stripAllPrefixes(plugin.getPath(docA)));
|
||||||
|
if (abs instanceof TFile) {
|
||||||
|
let result = false;
|
||||||
|
if (isPlainText(docA.path)) {
|
||||||
|
const data = await plugin.app.vault.read(abs);
|
||||||
|
result = isDocContentSame(data, doc.data);
|
||||||
|
} else {
|
||||||
|
const data = await plugin.app.vault.readBinary(abs);
|
||||||
|
const dataEEncoded = await arrayBufferToBase64(data);
|
||||||
|
result = isDocContentSame(dataEEncoded, doc.data);
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
diffDetail += " ⚖️";
|
||||||
|
} else {
|
||||||
|
diffDetail += " ⚠️";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const docPath = plugin.getPath(doc as AnyEntry);
|
||||||
|
const [filename, ...pathItems] = docPath.split("/").reverse();
|
||||||
|
|
||||||
|
let chunksStatus = "";
|
||||||
|
if (showChunkCorrected) {
|
||||||
|
const chunks = (doc as any)?.children ?? [];
|
||||||
|
const loadedChunks = await db.allDocsRaw({ keys: [...chunks] });
|
||||||
|
const totalCount = loadedChunks.rows.length;
|
||||||
|
const errorCount = loadedChunks.rows.filter((e) => "error" in e).length;
|
||||||
|
if (errorCount == 0) {
|
||||||
|
chunksStatus = `✅ ${totalCount}`;
|
||||||
|
} else {
|
||||||
|
chunksStatus = `🔎 ${errorCount} ✅ ${totalCount}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: doc._id,
|
||||||
|
rev: doc._rev,
|
||||||
|
path: docPath,
|
||||||
|
dirname: pathItems.reverse().join("/"),
|
||||||
|
filename: filename,
|
||||||
|
mtime: mtime,
|
||||||
|
mtimeDisp: mtimeToDate(mtime),
|
||||||
|
size: (doc as any)?.size ?? 0,
|
||||||
|
isDeleted: isDeleted,
|
||||||
|
changes: diffDetail,
|
||||||
|
chunks: chunksStatus,
|
||||||
|
isPlain: isPlain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...result].sort((a, b) => b.mtime - a.mtime);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getHistory(showDiffInfo: boolean, showChunkCorrected: boolean, checkStorageDiff: boolean) {
|
||||||
|
loading = true;
|
||||||
|
const newDisplay = [];
|
||||||
|
const page = await fetchChanges();
|
||||||
|
newDisplay.push(...page);
|
||||||
|
history = [...newDisplay];
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextWeek() {
|
||||||
|
dispDateTo = new Date(range_to_epoch - timezoneOffset + 3600 * 1000 * 24 * 7).toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
function prevWeek() {
|
||||||
|
dispDateFrom = new Date(range_from_epoch - timezoneOffset - 3600 * 1000 * 24 * 7).toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await getHistory(showDiffInfo, showChunkCorrected, checkStorageDiff);
|
||||||
|
});
|
||||||
|
onDestroy(() => {});
|
||||||
|
|
||||||
|
function showHistory(file: string, rev: string) {
|
||||||
|
new DocumentHistoryModal(plugin.app, plugin, file as unknown as FilePathWithPrefix, null, rev).open();
|
||||||
|
}
|
||||||
|
function openFile(file: string) {
|
||||||
|
plugin.app.workspace.openLinkText(file, file);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="globalhistory">
|
||||||
|
<h1>Vault history</h1>
|
||||||
|
<div class="control">
|
||||||
|
<div class="row"><label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} /></div>
|
||||||
|
<div class="row"><label for="">To:</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="">Info:</label>
|
||||||
|
<label><input type="checkbox" bind:checked={showDiffInfo} disabled={loading} /><span>Diff</span></label>
|
||||||
|
<label><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span></label>
|
||||||
|
<label><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if loading}
|
||||||
|
<div class="">Gathering information...</div>
|
||||||
|
{/if}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th> Date </th>
|
||||||
|
<th> Path </th>
|
||||||
|
<th> Rev </th>
|
||||||
|
<th> Stat </th>
|
||||||
|
{#if showChunkCorrected}
|
||||||
|
<th> Chunks </th>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="more">
|
||||||
|
{#if loading}
|
||||||
|
<div class="" />
|
||||||
|
{:else}
|
||||||
|
<div><button on:click={() => nextWeek()}>+1 week</button></div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{#each history as entry}
|
||||||
|
<tr>
|
||||||
|
<td class="mtime">
|
||||||
|
{entry.mtimeDisp}
|
||||||
|
</td>
|
||||||
|
<td class="path">
|
||||||
|
<div class="filenames">
|
||||||
|
<span class="path">/{entry.dirname.split("/").join(`/`)}</span>
|
||||||
|
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="rev">
|
||||||
|
{#if entry.isPlain}
|
||||||
|
<a on:click={() => showHistory(entry.path, entry.rev)}>{entry.rev}</a>
|
||||||
|
{:else}
|
||||||
|
{entry.rev}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{entry.changes}
|
||||||
|
</td>
|
||||||
|
{#if showChunkCorrected}
|
||||||
|
<td>
|
||||||
|
{entry.chunks}
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="more">
|
||||||
|
{#if loading}
|
||||||
|
<div class="" />
|
||||||
|
{:else}
|
||||||
|
<div><button on:click={() => prevWeek()}>+1 week</button></div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.globalhistory {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.more > div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.more > div > button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
td.mtime {
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
td.path {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.row > label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 5em;
|
||||||
|
}
|
||||||
|
.row > input {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filenames {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.filenames > .path {
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
.rev {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 3em;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
47
src/GlobalHistoryView.ts
Normal file
47
src/GlobalHistoryView.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
ItemView,
|
||||||
|
WorkspaceLeaf
|
||||||
|
} from "./deps";
|
||||||
|
import GlobalHistoryComponent from "./GlobalHistory.svelte";
|
||||||
|
import type ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
|
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
|
||||||
|
export class GlobalHistoryView extends ItemView {
|
||||||
|
|
||||||
|
component: GlobalHistoryComponent;
|
||||||
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
icon: "clock";
|
||||||
|
title: string;
|
||||||
|
navigation: true;
|
||||||
|
|
||||||
|
getIcon(): string {
|
||||||
|
return "clock";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getViewType() {
|
||||||
|
return VIEW_TYPE_GLOBAL_HISTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayText() {
|
||||||
|
return "Vault history";
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen() {
|
||||||
|
this.component = new GlobalHistoryComponent({
|
||||||
|
target: this.contentEl,
|
||||||
|
props: {
|
||||||
|
plugin: this.plugin,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClose() {
|
||||||
|
this.component.$destroy();
|
||||||
|
}
|
||||||
|
}
|
81
src/LogPane.svelte
Normal file
81
src/LogPane.svelte
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import { logMessageStore } from "./lib/src/stores";
|
||||||
|
|
||||||
|
let unsubscribe: () => void;
|
||||||
|
let messages = [] as string[];
|
||||||
|
let wrapRight = false;
|
||||||
|
let autoScroll = true;
|
||||||
|
let suspended = false;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
unsubscribe = logMessageStore.observe((e) => {
|
||||||
|
if (!suspended) {
|
||||||
|
messages = [...e];
|
||||||
|
if (autoScroll) {
|
||||||
|
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logMessageStore.invalidate();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
onDestroy(() => {
|
||||||
|
if (unsubscribe) unsubscribe();
|
||||||
|
});
|
||||||
|
let scroll: HTMLDivElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="logpane">
|
||||||
|
<!-- <h1>Self-hosted LiveSync Log</h1> -->
|
||||||
|
<div class="control">
|
||||||
|
<div class="row">
|
||||||
|
<label><input type="checkbox" bind:checked={wrapRight} /><span>Wrap</span></label>
|
||||||
|
<label><input type="checkbox" bind:checked={autoScroll} /><span>Auto scroll</span></label>
|
||||||
|
<label><input type="checkbox" bind:checked={suspended} /><span>Pause</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="log" bind:this={scroll}>
|
||||||
|
{#each messages as line}
|
||||||
|
<pre class:wrap-right={wrapRight}>{line}</pre>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.logpane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.log {
|
||||||
|
overflow-y: scroll;
|
||||||
|
user-select: text;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
.log > pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.log > pre.wrap-right {
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.row > label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 5em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
46
src/LogPaneView.ts
Normal file
46
src/LogPaneView.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
ItemView,
|
||||||
|
WorkspaceLeaf
|
||||||
|
} from "obsidian";
|
||||||
|
import LogPaneComponent from "./LogPane.svelte";
|
||||||
|
import type ObsidianLiveSyncPlugin from "./main";
|
||||||
|
export const VIEW_TYPE_LOG = "log-log";
|
||||||
|
// Show notes as like scroll.
|
||||||
|
export class LogPaneView extends ItemView {
|
||||||
|
|
||||||
|
component: LogPaneComponent;
|
||||||
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
|
icon: "view-log";
|
||||||
|
title: string;
|
||||||
|
navigation: true;
|
||||||
|
|
||||||
|
getIcon(): string {
|
||||||
|
return "view-log";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getViewType() {
|
||||||
|
return VIEW_TYPE_LOG;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayText() {
|
||||||
|
return "Self-hosted LiveSync Log";
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen() {
|
||||||
|
this.component = new LogPaneComponent({
|
||||||
|
target: this.contentEl,
|
||||||
|
props: {
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClose() {
|
||||||
|
this.component.$destroy();
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import { FilePath } from "./lib/src/types";
|
import { type FilePath } from "./lib/src/types";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
addIcon, App, DataWriteOptions, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginManifest,
|
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, Plugin_2, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||||
PluginSettingTab, Plugin_2, requestUrl, RequestUrlParam, RequestUrlResponse, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
parseYaml, ItemView, WorkspaceLeaf
|
||||||
parseYaml
|
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
|
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||||
import {
|
import {
|
||||||
normalizePath as normalizePath_
|
normalizePath as normalizePath_
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
|
@ -43,7 +43,7 @@ export class InputStringDialog extends Modal {
|
|||||||
key: string;
|
key: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
isManuallyClosed = false;
|
isManuallyClosed = false;
|
||||||
isPassword: boolean = false;
|
isPassword = false;
|
||||||
|
|
||||||
constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void) {
|
constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
|
49
src/main.ts
49
src/main.ts
@ -7,7 +7,6 @@ import { type InternalFileInfo, type queueItem, type CacheData, type FileEventIt
|
|||||||
import { getDocData, isDocContentSame, Parallels } from "./lib/src/utils";
|
import { getDocData, isDocContentSame, Parallels } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { LogDisplayModal } from "./LogDisplayModal";
|
|
||||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
@ -30,6 +29,8 @@ import { HiddenFileSync } from "./CmdHiddenFileSync";
|
|||||||
import { SetupLiveSync } from "./CmdSetupLiveSync";
|
import { SetupLiveSync } from "./CmdSetupLiveSync";
|
||||||
import { ConfigSync } from "./CmdConfigSync";
|
import { ConfigSync } from "./CmdConfigSync";
|
||||||
import { confirmWithMessage } from "./dialogs";
|
import { confirmWithMessage } from "./dialogs";
|
||||||
|
import { GlobalHistoryView, VIEW_TYPE_GLOBAL_HISTORY } from "./GlobalHistoryView";
|
||||||
|
import { LogPaneView, VIEW_TYPE_LOG } from "./LogPaneView";
|
||||||
|
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
|
|
||||||
@ -539,7 +540,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.addRibbonIcon("view-log", "Show log", () => {
|
this.addRibbonIcon("view-log", "Show log", () => {
|
||||||
new LogDisplayModal(this.app, this).open();
|
this.showView(VIEW_TYPE_LOG);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||||
@ -650,8 +651,44 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.registerView(
|
||||||
|
VIEW_TYPE_GLOBAL_HISTORY,
|
||||||
|
(leaf) => new GlobalHistoryView(leaf, this)
|
||||||
|
);
|
||||||
|
this.registerView(
|
||||||
|
VIEW_TYPE_LOG,
|
||||||
|
(leaf) => new LogPaneView(leaf, this)
|
||||||
|
);
|
||||||
|
this.addCommand({
|
||||||
|
id: "livesync-global-history",
|
||||||
|
name: "Show vault history",
|
||||||
|
callback: () => {
|
||||||
|
this.showGlobalHistory()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async showView(viewType: string) {
|
||||||
|
const leaves = this.app.workspace.getLeavesOfType(viewType);
|
||||||
|
if (leaves.length == 0) {
|
||||||
|
await this.app.workspace.getLeaf(true).setViewState({
|
||||||
|
type: viewType,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
leaves[0].setViewState({
|
||||||
|
type: viewType,
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (leaves.length > 0) {
|
||||||
|
this.app.workspace.revealLeaf(
|
||||||
|
leaves[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showGlobalHistory() {
|
||||||
|
this.showView(VIEW_TYPE_GLOBAL_HISTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
for (const addOn of this.addOns) {
|
for (const addOn of this.addOns) {
|
||||||
@ -1003,7 +1040,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
//--> Basic document Functions
|
//--> Basic document Functions
|
||||||
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
|
notifies: { [key: string]: { notice: Notice; timer: ReturnType<typeof setTimeout>; count: number } } = {};
|
||||||
|
|
||||||
lastLog = "";
|
lastLog = "";
|
||||||
// eslint-disable-next-line require-await
|
// eslint-disable-next-line require-await
|
||||||
@ -1382,7 +1419,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
|
|
||||||
//---> Sync
|
//---> Sync
|
||||||
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<void> {
|
||||||
const docsSorted = docs.sort((a, b) => b.mtime - a.mtime);
|
const docsSorted = docs.sort((a: any, b: any) => b?.mtime ?? 0 - a?.mtime ?? 0);
|
||||||
L1:
|
L1:
|
||||||
for (const change of docsSorted) {
|
for (const change of docsSorted) {
|
||||||
if (isChunk(change._id)) {
|
if (isChunk(change._id)) {
|
||||||
@ -1471,7 +1508,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
|||||||
this.statusBar.title = e.syncStatus;
|
this.statusBar.title = e.syncStatus;
|
||||||
let waiting = "";
|
let waiting = "";
|
||||||
if (this.settings.batchSave && !this.settings.liveSync) {
|
if (this.settings.batchSave && !this.settings.liveSync) {
|
||||||
const len = this.vaultManager.getQueueLength();
|
const len = this.vaultManager?.getQueueLength();
|
||||||
if (len != 0) {
|
if (len != 0) {
|
||||||
waiting = ` 🛫${len}`;
|
waiting = ` 🛫${len}`;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user