mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-26 21:07:23 +03:00
Compare commits
23 Commits
cli_test_d
...
fix_warns
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45fe0b3682 | ||
|
|
8d3825abc9 | ||
|
|
c5f9841b85 | ||
|
|
d36d176d99 | ||
|
|
38b2cf73ed | ||
|
|
40b15a6950 | ||
|
|
e312bb7640 | ||
|
|
852c0e6c13 | ||
|
|
7c1bcf9e9b | ||
|
|
2b79bed085 | ||
|
|
6b1e0c4aa8 | ||
|
|
3c3645eba4 | ||
|
|
009cc3c87a | ||
|
|
fc5fd4be94 | ||
|
|
8ed1acf79d | ||
|
|
c518223d21 | ||
|
|
caaff618e9 | ||
|
|
148aa8505e | ||
|
|
f9a626a858 | ||
|
|
5454e1106f | ||
|
|
0d9397c8b9 | ||
|
|
429a3ff1fd | ||
|
|
bfff6ea7b8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,4 +30,5 @@ cov_profile/**
|
||||
coverage
|
||||
src/apps/cli/dist/*
|
||||
_testdata/**
|
||||
utils/bench/splitResults.csv
|
||||
utils/bench/splitResults.csv
|
||||
.eslintcache
|
||||
@@ -43,7 +43,7 @@ export default defineConfig([
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser },
|
||||
globals: { ...globals.browser, "PouchDB": "readonly" },
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
@@ -79,5 +79,5 @@ export default defineConfig([
|
||||
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||
"obsidianmd/no-plugin-as-component": "off", // Temporary
|
||||
},
|
||||
},
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.68",
|
||||
"version": "0.25.70",
|
||||
"minAppVersion": "1.7.2",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
"authorUrl": "https://github.com/vrtmrz",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.68",
|
||||
"version": "0.25.70",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.68",
|
||||
"version": "0.25.70",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.68",
|
||||
"version": "0.25.70",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
@@ -19,7 +19,7 @@
|
||||
"buildVite": "npx dotenv-cli -e .env -- vite build --mode production",
|
||||
"buildViteOriginal": "npx dotenv-cli -e .env -- vite build --mode original",
|
||||
"buildDev": "node esbuild.config.mjs dev",
|
||||
"lint": "eslint src",
|
||||
"lint": "eslint --cache --concurrency auto src",
|
||||
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"tsc-check": "tsc --noEmit",
|
||||
"pretty": "npm run prettyNoWrite -- --write --log-level error",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
|
||||
let replicatorStatus = $state<P2PReplicatorStatus | undefined>(undefined);
|
||||
let roomSuffix = $state<string>(extractP2PRoomSuffix(core?.services.setting.currentSettings()?.P2P_roomID ?? ""));
|
||||
let useDiagRTC = $state<boolean>(core?.services.setting.currentSettings()?.P2P_useDiagRTC ?? false);
|
||||
|
||||
async function requestServerStatus() {
|
||||
await Promise.resolve(liveSyncReplicator.requestStatus());
|
||||
@@ -48,6 +49,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDiagRTC() {
|
||||
if (!core) {
|
||||
return;
|
||||
}
|
||||
const next = !useDiagRTC;
|
||||
await core.services.setting.updateSettings((settings) => {
|
||||
settings.P2P_useDiagRTC = next;
|
||||
return settings;
|
||||
}, true);
|
||||
useDiagRTC = next;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
|
||||
serverInfo = status;
|
||||
@@ -58,6 +71,7 @@
|
||||
});
|
||||
const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => {
|
||||
roomSuffix = extractP2PRoomSuffix(settings?.P2P_roomID ?? "");
|
||||
useDiagRTC = settings?.P2P_useDiagRTC ?? false;
|
||||
});
|
||||
|
||||
fireAndForget(async () => {
|
||||
@@ -131,6 +145,48 @@
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if core}
|
||||
<div class="status-item status-action diag-toggle-row">
|
||||
<label class="broadcast-label" for="diag-toggle">
|
||||
🕵️ Diag
|
||||
</label>
|
||||
<button
|
||||
id="diag-toggle"
|
||||
class="broadcast-button {useDiagRTC ? 'is-on' : 'is-off'}"
|
||||
onclick={toggleDiagRTC}
|
||||
title={useDiagRTC
|
||||
? 'Diagnostic RTCPeerConnection is enabled'
|
||||
: 'Use Diagnostic RTCPeerConnection for statistics'}
|
||||
>
|
||||
{useDiagRTC ? 'On' : 'Off'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if serverInfo}
|
||||
<div class="diag-section">
|
||||
<h4>Stats</h4>
|
||||
<div class="diag-grid">
|
||||
<div class="diag-item">
|
||||
<span>Incoming:</span>
|
||||
<span>{serverInfo.diag.totalNewConnections}</span>
|
||||
</div>
|
||||
<div class="diag-item">
|
||||
<span>Connected:</span>
|
||||
<span>{serverInfo.diag.totalSuccessfulConnections}</span>
|
||||
</div>
|
||||
<div class="diag-item">
|
||||
<span>Failed:</span>
|
||||
<span>{serverInfo.diag.totalFailedConnections}</span>
|
||||
</div>
|
||||
<div class="diag-item">
|
||||
<span>Closed:</span>
|
||||
<span>{serverInfo.diag.totalClosedConnections}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -190,6 +246,11 @@
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.diag-toggle-row {
|
||||
align-items: center;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.broadcast-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-normal);
|
||||
@@ -221,4 +282,29 @@
|
||||
background-color: var(--interactive-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.diag-section {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.diag-section h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.diag-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 0.35rem 0.75rem;
|
||||
}
|
||||
|
||||
.diag-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: b9aaf3c03a...61741c1748
@@ -56,6 +56,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
info!: HTMLDivElement;
|
||||
fileInfo!: HTMLDivElement;
|
||||
showDiff = false;
|
||||
diffOnly = false;
|
||||
id?: DocumentID;
|
||||
|
||||
file: FilePathWithPrefix;
|
||||
@@ -70,6 +71,15 @@ export class DocumentHistoryModal extends Modal {
|
||||
currentDiffIndex = -1;
|
||||
diffNavContainer!: HTMLDivElement;
|
||||
diffNavIndicator!: HTMLSpanElement;
|
||||
diffOnlyLabel!: HTMLLabelElement;
|
||||
|
||||
// Search state
|
||||
searchKeyword = "";
|
||||
searchResults: { rev: string; index: number; matchType: "Content" | "Diff" }[] = [];
|
||||
currentSearchIndex = -1;
|
||||
searchResultIndicator!: HTMLSpanElement;
|
||||
searchProgressIndicator!: HTMLSpanElement;
|
||||
searchTimeout: number | null = null;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
@@ -88,9 +98,12 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (!file && id) {
|
||||
this.file = this.services.path.id2path(id);
|
||||
}
|
||||
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
|
||||
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
|
||||
this.showDiff = true;
|
||||
}
|
||||
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
|
||||
this.diffOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
async loadFile(initialRev?: string) {
|
||||
@@ -151,17 +164,48 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
|
||||
appendTextDiff(diff: [number, string][]) {
|
||||
let hasOmitted = false;
|
||||
for (const [operation, text] of diff) {
|
||||
if (operation == DIFF_DELETE) {
|
||||
this.contentView.createSpan({ text, cls: "history-deleted" });
|
||||
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-deleted" }), text);
|
||||
hasOmitted = false;
|
||||
} else if (operation == DIFF_EQUAL) {
|
||||
this.contentView.createSpan({ text, cls: "history-normal" });
|
||||
if (this.diffOnly) {
|
||||
if (!hasOmitted) {
|
||||
this.contentView.appendText("\n...\n");
|
||||
hasOmitted = true;
|
||||
}
|
||||
} else {
|
||||
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-normal" }), text);
|
||||
}
|
||||
} else if (operation == DIFF_INSERT) {
|
||||
this.contentView.createSpan({ text, cls: "history-added" });
|
||||
this.appendSearchHighlightedText(this.contentView.createSpan({ cls: "history-added" }), text);
|
||||
hasOmitted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendSearchHighlightedText(container: HTMLElement, text: string) {
|
||||
if (!this.searchKeyword) {
|
||||
container.appendText(text);
|
||||
return;
|
||||
}
|
||||
const escapedKeyword = this.searchKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(escapedKeyword, "gi");
|
||||
let lastIndex = 0;
|
||||
for (const match of text.matchAll(regex)) {
|
||||
const index = match.index ?? 0;
|
||||
if (index > lastIndex) {
|
||||
container.appendText(text.slice(lastIndex, index));
|
||||
}
|
||||
container.createEl("mark", { text: match[0] });
|
||||
lastIndex = index + match[0].length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
container.appendText(text.slice(lastIndex));
|
||||
}
|
||||
}
|
||||
|
||||
appendImageDiff(baseSrc: string, overlaySrc?: string) {
|
||||
const wrap = this.contentView.createDiv({ cls: "ls-imgdiff-wrap" });
|
||||
const overlay = wrap.createDiv({ cls: "overlay" });
|
||||
@@ -258,7 +302,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.currentDeleted) {
|
||||
this.appendDeletedNotice();
|
||||
}
|
||||
this.contentView.appendText(w1data);
|
||||
this.appendSearchHighlightedText(this.contentView, w1data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +310,11 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.resetDiffNavigation();
|
||||
if (this.showDiff) {
|
||||
this.navigateDiff("next");
|
||||
} else if (this.searchKeyword) {
|
||||
const firstMark = this.contentView.querySelector("mark");
|
||||
if (firstMark) {
|
||||
firstMark.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +342,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
target.classList.add("diff-focused");
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
this.diffNavIndicator.textContent = `${this.currentDiffIndex + 1}/${diffElements.length}`;
|
||||
this.diffNavIndicator.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,9 +353,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.diffNavIndicator) {
|
||||
if (this.showDiff) {
|
||||
const diffElements = this.contentView.querySelectorAll(".history-added, .history-deleted");
|
||||
this.diffNavIndicator.textContent = diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014";
|
||||
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
|
||||
} else {
|
||||
this.diffNavIndicator.textContent = "\u2014";
|
||||
this.diffNavIndicator.setText("\u2014");
|
||||
}
|
||||
}
|
||||
this.updateDiffNavVisibility();
|
||||
@@ -319,6 +368,117 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.diffNavContainer) {
|
||||
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
|
||||
}
|
||||
if (this.diffOnlyLabel) {
|
||||
this.diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through the last 100 revisions for the given keyword.
|
||||
*/
|
||||
async performSearch(keyword: string) {
|
||||
this.searchKeyword = keyword;
|
||||
this.searchResults = [];
|
||||
this.currentSearchIndex = -1;
|
||||
|
||||
if (!keyword) {
|
||||
this.searchResultIndicator.setText("");
|
||||
this.searchProgressIndicator.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
const db = this.core.localDatabase;
|
||||
const limit = 100;
|
||||
const totalRevs = this.revs_info.length;
|
||||
const end = Math.min(totalRevs, limit);
|
||||
|
||||
this.searchProgressIndicator.setText("Searching...");
|
||||
|
||||
const dmp = new diff_match_patch();
|
||||
|
||||
// 0 is the newest, higher index is older.
|
||||
for (let i = 0; i < end; i++) {
|
||||
const revInfo = this.revs_info[i];
|
||||
const rev = revInfo.rev;
|
||||
|
||||
this.searchProgressIndicator.setText(`Searching ${i + 1}/${end}...`);
|
||||
|
||||
const doc = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||
if (doc === false) continue;
|
||||
|
||||
const content = readDocument(doc);
|
||||
if (typeof content !== "string") continue;
|
||||
|
||||
const keywordLower = keyword.toLocaleLowerCase();
|
||||
|
||||
// Search in content
|
||||
if (content.toLocaleLowerCase().includes(keywordLower)) {
|
||||
this.searchResults.push({ rev, index: i, matchType: "Content" });
|
||||
this.updateSearchUI();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Search in diff (from older version to this version)
|
||||
// Older version is at i + 1
|
||||
if (i < totalRevs - 1) {
|
||||
const olderRev = this.revs_info[i + 1].rev;
|
||||
const olderDoc = await db.getDBEntry(this.file, { rev: olderRev }, false, false, true);
|
||||
if (olderDoc !== false) {
|
||||
const olderContent = readDocument(olderDoc);
|
||||
if (typeof olderContent === "string") {
|
||||
const diffs = dmp.diff_main(olderContent, content);
|
||||
let foundInDiff = false;
|
||||
for (const d of diffs) {
|
||||
if (
|
||||
(d[0] === DIFF_INSERT || d[0] === DIFF_DELETE) &&
|
||||
d[1].toLocaleLowerCase().includes(keywordLower)
|
||||
) {
|
||||
foundInDiff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundInDiff) {
|
||||
this.searchResults.push({ rev, index: i, matchType: "Diff" });
|
||||
this.updateSearchUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.searchProgressIndicator.setText("Done");
|
||||
this.updateSearchUI();
|
||||
}
|
||||
|
||||
updateSearchUI() {
|
||||
if (this.searchResults.length === 0) {
|
||||
this.searchResultIndicator.setText(this.searchKeyword ? "No matches found" : "");
|
||||
} else {
|
||||
const current = this.currentSearchIndex >= 0 ? this.currentSearchIndex + 1 : 0;
|
||||
this.searchResultIndicator.setText(`${current}/${this.searchResults.length} matches`);
|
||||
}
|
||||
}
|
||||
|
||||
navigateSearch(direction: "prev" | "next") {
|
||||
if (this.searchResults.length === 0) return;
|
||||
|
||||
if (direction === "next") {
|
||||
this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length;
|
||||
} else {
|
||||
this.currentSearchIndex =
|
||||
this.currentSearchIndex <= 0 ? this.searchResults.length - 1 : this.currentSearchIndex - 1;
|
||||
}
|
||||
|
||||
const match = this.searchResults[this.currentSearchIndex];
|
||||
this.range.value = `${this.revs_info.length - 1 - match.index}`;
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
this.updateSearchUI();
|
||||
|
||||
// If it's a diff match, make sure Highlight diff is on
|
||||
if (match.matchType === "Diff" && !this.showDiff) {
|
||||
// We could auto-enable it, but maybe just notify the user?
|
||||
// For now, let's just let the user toggle it if they want to see the diff.
|
||||
}
|
||||
}
|
||||
|
||||
override onOpen() {
|
||||
@@ -327,6 +487,42 @@ export class DocumentHistoryModal extends Modal {
|
||||
contentEl.empty();
|
||||
this.fileInfo = contentEl.createDiv("");
|
||||
this.fileInfo.addClass("op-info");
|
||||
|
||||
// Search Row
|
||||
const searchRow = contentEl.createDiv("");
|
||||
searchRow.addClass("op-info");
|
||||
searchRow.addClass("search-row");
|
||||
searchRow.addClass("history-search-row");
|
||||
|
||||
const searchInput = searchRow.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: "Search in history (last 100)...",
|
||||
});
|
||||
searchInput.addClass("history-search-input");
|
||||
searchInput.addEventListener("input", () => {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
this.searchTimeout = window.setTimeout(() => {
|
||||
void this.performSearch(searchInput.value);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
searchRow.createEl("button", { text: "\u25B2" }, (e) => {
|
||||
e.title = "Previous match";
|
||||
e.addEventListener("click", () => this.navigateSearch("prev"));
|
||||
});
|
||||
searchRow.createEl("button", { text: "\u25BC" }, (e) => {
|
||||
e.title = "Next match";
|
||||
e.addEventListener("click", () => this.navigateSearch("next"));
|
||||
});
|
||||
|
||||
this.searchResultIndicator = searchRow.createEl("span", { text: "" });
|
||||
this.searchResultIndicator.addClass("history-search-result-indicator");
|
||||
|
||||
this.searchProgressIndicator = searchRow.createEl("span", { text: "" });
|
||||
this.searchProgressIndicator.addClass("history-search-progress-indicator");
|
||||
|
||||
const divView = contentEl.createDiv("");
|
||||
divView.addClass("op-flex");
|
||||
|
||||
@@ -342,24 +538,43 @@ export class DocumentHistoryModal extends Modal {
|
||||
const diffOptionsRow = contentEl.createDiv("");
|
||||
diffOptionsRow.addClass("op-info");
|
||||
diffOptionsRow.addClass("diff-options-row");
|
||||
diffOptionsRow.addClass("history-diff-options-row");
|
||||
|
||||
diffOptionsRow.createEl("label", {}, (label) => {
|
||||
label.appendChild(
|
||||
createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
})
|
||||
);
|
||||
const highlightDiffContainer = diffOptionsRow.createDiv("");
|
||||
highlightDiffContainer.addClass("history-highlight-diff-container");
|
||||
|
||||
highlightDiffContainer.createEl("label", {}, (label) => {
|
||||
label.addClass("history-highlight-diff-label");
|
||||
label.createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
});
|
||||
label.appendText("Highlight diff");
|
||||
});
|
||||
|
||||
const diffOnlyLabel = diffOptionsRow.createEl("label", {});
|
||||
diffOnlyLabel.createEl("input", { type: "checkbox" }, (checkbox) => {
|
||||
if (this.diffOnly) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
this.diffOnly = checkbox.checked;
|
||||
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
});
|
||||
diffOnlyLabel.appendText("Diff only");
|
||||
diffOnlyLabel.addClass("diff-only-label");
|
||||
diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
|
||||
this.diffOnlyLabel = diffOnlyLabel;
|
||||
|
||||
// Diff navigation buttons
|
||||
this.diffNavContainer = diffOptionsRow.createDiv("");
|
||||
this.diffNavContainer.addClass("diff-nav");
|
||||
|
||||
@@ -27,6 +27,9 @@ export class ConflictResolveModal extends Modal {
|
||||
localName: string = "Base";
|
||||
remoteName: string = "Conflicted";
|
||||
offEvent?: ReturnType<typeof eventHub.onEvent>;
|
||||
currentDiffIndex = -1;
|
||||
diffView!: HTMLDivElement;
|
||||
diffNavIndicator!: HTMLSpanElement;
|
||||
|
||||
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
|
||||
super(app);
|
||||
@@ -47,7 +50,7 @@ export class ConflictResolveModal extends Modal {
|
||||
const lines = text.split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
const span = container.createSpan({ cls });
|
||||
span.textContent = line;
|
||||
span.setText(line);
|
||||
if (index < lines.length - 1) {
|
||||
container.createSpan({ cls: "ls-mark-cr" });
|
||||
container.createEl("br");
|
||||
@@ -62,6 +65,33 @@ export class ConflictResolveModal extends Modal {
|
||||
container.createEl("br");
|
||||
}
|
||||
|
||||
navigateDiff(direction: "prev" | "next") {
|
||||
const diffElements = this.diffView.querySelectorAll(".added, .deleted");
|
||||
if (diffElements.length === 0) return;
|
||||
|
||||
const prevFocused = this.diffView.querySelector(".diff-focused");
|
||||
if (prevFocused) {
|
||||
prevFocused.classList.remove("diff-focused");
|
||||
}
|
||||
|
||||
if (direction === "next") {
|
||||
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
||||
} else {
|
||||
this.currentDiffIndex = this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
||||
}
|
||||
|
||||
const target = diffElements[this.currentDiffIndex];
|
||||
target.classList.add("diff-focused");
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
this.diffNavIndicator.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
|
||||
}
|
||||
|
||||
resetDiffNavigation() {
|
||||
this.currentDiffIndex = -1;
|
||||
const diffElements = this.diffView.querySelectorAll(".added, .deleted");
|
||||
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
|
||||
}
|
||||
|
||||
override onOpen() {
|
||||
const { contentEl } = this;
|
||||
// Send cancel signal for the previous merge dialogue
|
||||
@@ -78,10 +108,26 @@ export class ConflictResolveModal extends Modal {
|
||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||
this.titleEl.setText(this.title);
|
||||
contentEl.empty();
|
||||
contentEl.createEl("span", { text: this.filename });
|
||||
const div = contentEl.createDiv("");
|
||||
div.addClass("op-scrollable");
|
||||
div.addClass("ls-dialog");
|
||||
const diffOptionsRow = contentEl.createDiv("");
|
||||
diffOptionsRow.addClass("diff-options-row");
|
||||
diffOptionsRow.createEl("span", { text: this.filename });
|
||||
|
||||
const diffNavContainer = diffOptionsRow.createDiv("");
|
||||
diffNavContainer.addClass("diff-nav");
|
||||
diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
|
||||
e.addClass("diff-nav-btn");
|
||||
e.addEventListener("click", () => this.navigateDiff("prev"));
|
||||
});
|
||||
diffNavContainer.createEl("button", { text: "\u25BC Next" }, (e) => {
|
||||
e.addClass("diff-nav-btn");
|
||||
e.addEventListener("click", () => this.navigateDiff("next"));
|
||||
});
|
||||
this.diffNavIndicator = diffNavContainer.createEl("span", { text: "\u2014" });
|
||||
this.diffNavIndicator.addClass("diff-nav-indicator");
|
||||
|
||||
this.diffView = contentEl.createDiv("");
|
||||
this.diffView.addClass("op-scrollable");
|
||||
this.diffView.addClass("ls-dialog");
|
||||
let diffLength = 0;
|
||||
for (const v of this.result.diff) {
|
||||
const x1 = v[0];
|
||||
@@ -91,12 +137,11 @@ export class ConflictResolveModal extends Modal {
|
||||
continue;
|
||||
}
|
||||
if (x1 == DIFF_DELETE) {
|
||||
this.appendDiffFragment(div, x2, "deleted");
|
||||
div.createEl("span", { text: x2, cls: "deleted normal conflict-dev-name" });
|
||||
this.appendDiffFragment(this.diffView, x2, "deleted");
|
||||
} else if (x1 == DIFF_EQUAL) {
|
||||
this.appendDiffFragment(div, x2, "normal");
|
||||
this.appendDiffFragment(this.diffView, x2, "normal");
|
||||
} else if (x1 == DIFF_INSERT) {
|
||||
this.appendDiffFragment(div, x2, "added");
|
||||
this.appendDiffFragment(this.diffView, x2, "added");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,24 +153,30 @@ export class ConflictResolveModal extends Modal {
|
||||
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
||||
this.appendVersionInfo(div2, "deleted", this.localName, date1);
|
||||
this.appendVersionInfo(div2, "added", this.remoteName, date2);
|
||||
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
||||
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
||||
).style.marginRight = "4px";
|
||||
contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) =>
|
||||
e.addEventListener("click", () => this.sendResponse(this.result.left.rev))
|
||||
).style.marginRight = "4px";
|
||||
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) => {
|
||||
e.addClass("conflict-action-button");
|
||||
e.addEventListener("click", () => this.sendResponse(this.result.right.rev));
|
||||
});
|
||||
contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) => {
|
||||
e.addClass("conflict-action-button");
|
||||
e.addEventListener("click", () => this.sendResponse(this.result.left.rev));
|
||||
});
|
||||
if (!this.pluginPickMode) {
|
||||
contentEl.createEl("button", { text: "Concat both" }, (e) =>
|
||||
e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT))
|
||||
).style.marginRight = "4px";
|
||||
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
||||
e.addClass("conflict-action-button");
|
||||
e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT));
|
||||
});
|
||||
}
|
||||
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
||||
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
||||
).style.marginRight = "4px";
|
||||
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => {
|
||||
e.addClass("conflict-action-button");
|
||||
e.addEventListener("click", () => this.sendResponse(CANCELLED));
|
||||
});
|
||||
if (diffLength > 100 * 1024) {
|
||||
div.empty();
|
||||
div.innerText = "(Too large diff to display)";
|
||||
this.diffView.empty();
|
||||
this.diffView.setText("(Too large diff to display)");
|
||||
}
|
||||
this.resetDiffNavigation();
|
||||
this.navigateDiff("next");
|
||||
}
|
||||
|
||||
sendResponse(result: MergeDialogResult) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
REMOTE_MINIO,
|
||||
REMOTE_P2P,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts";
|
||||
import { isObjectDifferent } from "@lib/common/utils.ts";
|
||||
import Intro from "./SetupWizard/dialogs/Intro.svelte";
|
||||
import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte";
|
||||
import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte";
|
||||
@@ -23,6 +23,7 @@ import SetupRemoteP2P from "./SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
||||
import SetupRemoteE2EE from "./SetupWizard/dialogs/SetupRemoteE2EE.svelte";
|
||||
import { decodeSettingsFromQRCodeData } from "../../lib/src/API/processSetting.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { ConnectionStringParser } from "@lib/common/ConnectionString.ts";
|
||||
|
||||
/**
|
||||
* User modes for onboarding and setup
|
||||
@@ -194,8 +195,24 @@ export class SetupManager extends AbstractModule {
|
||||
return await this.onOnboard(userMode);
|
||||
}
|
||||
const newSetting = { ...currentSetting, ...p2pConf } as ObsidianLiveSyncSettings;
|
||||
// Apply remoteConfigurations
|
||||
if (newSetting.P2P_ActiveRemoteConfigurationId) {
|
||||
const id = newSetting.P2P_ActiveRemoteConfigurationId;
|
||||
const merged = {
|
||||
...newSetting,
|
||||
...p2pConf,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
const uri = ConnectionStringParser.serialize({ type: "p2p", settings: merged });
|
||||
newSetting.remoteConfigurations[id] = {
|
||||
...newSetting.remoteConfigurations[id],
|
||||
uri,
|
||||
isEncrypted: false,
|
||||
};
|
||||
newSetting.P2P_ActiveRemoteConfigurationId = id;
|
||||
}
|
||||
if (activate) {
|
||||
newSetting.remoteType = REMOTE_P2P;
|
||||
newSetting.activeConfigurationId = newSetting.P2P_ActiveRemoteConfigurationId;
|
||||
}
|
||||
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||
}
|
||||
@@ -285,9 +302,9 @@ export class SetupManager extends AbstractModule {
|
||||
this._log("No changes in settings detected. Skipping applying settings from wizard.", LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
}
|
||||
const patch = generatePatchObj(this.settings, newConf);
|
||||
console.log(`Changes:`);
|
||||
console.dir(patch);
|
||||
// const patch = generatePatchObj(this.settings, newConf);
|
||||
// console.log(`Changes:`);
|
||||
// console.dir(patch);
|
||||
if (!activate) {
|
||||
extra();
|
||||
await this.applySetting(newConf, UserMode.ExistingUser);
|
||||
|
||||
46
styles.css
46
styles.css
@@ -17,6 +17,10 @@
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
.conflict-action-button {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.op-scrollable {
|
||||
overflow-y: scroll;
|
||||
/* min-height: 280px; */
|
||||
@@ -521,8 +525,48 @@ div.workspace-leaf-content[data-type=bases] .livesync-status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.diff-only-label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.history-search-row {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.history-search-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.history-search-result-indicator {
|
||||
font-size: 0.8em;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.history-search-progress-indicator {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.history-diff-options-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.history-highlight-diff-container,
|
||||
.history-highlight-diff-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.history-highlight-diff-label {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.diff-focused {
|
||||
outline: 2px solid var(--interactive-accent);
|
||||
outline-offset: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
updates.md
29
updates.md
@@ -3,6 +3,35 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## 0.25.70
|
||||
|
||||
25th May, 2026
|
||||
|
||||
### New features
|
||||
- Diff dialogue now has great tools to navigate and understand the differences, including:
|
||||
- A checkbox to toggle the visibility of collapsed identical sections, making it easier to focus on the actual differences (PR #889).
|
||||
- A search feature to find specific text in past revisions, and navigate revisions with search results highlighted in the dialogue (PR #890).
|
||||
|
||||
- Conflict resolution dialogue now has a navigation feature to jump between conflicts (PR #891).
|
||||
|
||||
Thank you so much to @SeleiXi for implementing these features!
|
||||
|
||||
### Improved
|
||||
|
||||
- More diagnostic information for P2P connections is now shown, including why a connection failure occurred and the current connection status.
|
||||
|
||||
## 0.25.69
|
||||
|
||||
22nd May, 2026
|
||||
|
||||
### Fixed
|
||||
- No longer does the P2P passphrase mismatch cause a server shutdown.
|
||||
- Settings related to P2P synchronisation are now correctly applied on start-up and no longer reverted.
|
||||
|
||||
### New features
|
||||
- Diagnostic P2P connection stats are now available.
|
||||
- These stats indicate the number of connection trials, successes, and failures.
|
||||
|
||||
## 0.25.68
|
||||
|
||||
22nd May, 2026
|
||||
|
||||
Reference in New Issue
Block a user