mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-16 04:21:16 +00:00
Preparing v0.24.0
This commit is contained in:
325
src/modules/features/GlobalHistory/GlobalHistory.svelte
Normal file
325
src/modules/features/GlobalHistory/GlobalHistory.svelte
Normal file
@@ -0,0 +1,325 @@
|
||||
<script lang="ts">
|
||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { AnyEntry, FilePathWithPrefix } from "../../../lib/src/common/types.ts";
|
||||
import { getDocData, isAnyNote, isDocContentSame, readAsBlob } from "../../../lib/src/common/utils.ts";
|
||||
import { diff_match_patch } from "../../../deps.ts";
|
||||
import { DocumentHistoryModal } from "../DocumentHistory/DocumentHistoryModal.ts";
|
||||
import { isPlainText, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts";
|
||||
import { TFile } from "../../../deps.ts";
|
||||
import { getPath } from "../../../common/utils.ts";
|
||||
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 (!isAnyNote(docA)) continue;
|
||||
const path = getPath(docA as AnyEntry);
|
||||
const isPlain = isPlainText(docA.path);
|
||||
const revs = await db.getRaw(docA._id, { revs_info: true });
|
||||
let p: string | undefined = 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 isExist = await plugin.storageAccess.isExists(stripAllPrefixes(getPath(docA)));
|
||||
if (isExist) {
|
||||
const data = await plugin.storageAccess.readFileAuto(stripAllPrefixes(getPath(docA)));
|
||||
const d = readAsBlob(doc);
|
||||
const result = await isDocContentSame(data, d);
|
||||
if (result) {
|
||||
diffDetail += " ⚖️";
|
||||
} else {
|
||||
diffDetail += " ⚠️";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const docPath = 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, undefined, 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>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="rev">
|
||||
{#if entry.isPlain}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<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>
|
||||
49
src/modules/features/GlobalHistory/GlobalHistoryView.ts
Normal file
49
src/modules/features/GlobalHistory/GlobalHistoryView.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
ItemView,
|
||||
WorkspaceLeaf
|
||||
} from "../../../deps.ts";
|
||||
import GlobalHistoryComponent from "./GlobalHistory.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.component?.$destroy();
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user