mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-20 13:11:29 +00:00
- Customisation Sync now checks the difference while storing or applying the configuration. - Time difference in the dialogue has been fixed.
435 lines
16 KiB
Svelte
435 lines
16 KiB
Svelte
<script lang="ts">
|
|
import { PluginDataExDisplayV2, type IPluginDataExDisplay } from "../../features/CmdConfigSync";
|
|
import { Logger } from "../../lib/src/common/logger";
|
|
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
|
|
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
|
|
import type ObsidianLiveSyncPlugin from "../../main";
|
|
import { askString } from "../../common/utils";
|
|
import { Menu } from "obsidian";
|
|
|
|
export let list: IPluginDataExDisplay[] = [];
|
|
export let thisTerm = "";
|
|
export let hideNotApplicable = false;
|
|
export let selectNewest = 0;
|
|
export let selectNewestStyle = 0;
|
|
export let applyAllPluse = 0;
|
|
|
|
export let applyData: (data: IPluginDataExDisplay) => Promise<boolean>;
|
|
export let compareData: (dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean) => Promise<boolean>;
|
|
export let deleteData: (data: IPluginDataExDisplay) => Promise<boolean>;
|
|
export let hidden: boolean;
|
|
export let plugin: ObsidianLiveSyncPlugin;
|
|
export let isMaintenanceMode: boolean = false;
|
|
export let isFlagged: boolean = false;
|
|
const addOn = plugin.addOnConfigSync;
|
|
|
|
export let selected = "";
|
|
let freshness = "";
|
|
let equivalency = "";
|
|
let version = "";
|
|
let canApply: boolean = false;
|
|
let canCompare: boolean = false;
|
|
let pickToCompare: boolean = false;
|
|
let currentSelectNewest = 0;
|
|
let currentApplyAll = 0;
|
|
|
|
// Selectable terminals
|
|
let terms = [] as string[];
|
|
|
|
async function comparePlugin(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined) {
|
|
let freshness = "";
|
|
let equivalency = "";
|
|
let version = "";
|
|
let contentCheck = false;
|
|
let canApply: boolean = false;
|
|
let canCompare = false;
|
|
if (!local && !remote) {
|
|
// NO OP. what's happened?
|
|
freshness = "";
|
|
} else if (local && !remote) {
|
|
freshness = "Local only";
|
|
} else if (remote && !local) {
|
|
freshness = "Remote only";
|
|
canApply = true;
|
|
} else {
|
|
const dtDiff = (local?.mtime ?? 0) - (remote?.mtime ?? 0);
|
|
const diff = timeDeltaToHumanReadable(Math.abs(dtDiff));
|
|
if (dtDiff / 1000 < -10) {
|
|
// freshness = "✓ Newer";
|
|
freshness = `Newer (${diff})`;
|
|
canApply = true;
|
|
contentCheck = true;
|
|
} else if (dtDiff / 1000 > 10) {
|
|
// freshness = "⚠ Older";
|
|
freshness = `Older (${diff})`;
|
|
canApply = true;
|
|
contentCheck = true;
|
|
} else {
|
|
freshness = "Same";
|
|
canApply = false;
|
|
contentCheck = true;
|
|
}
|
|
}
|
|
const localVersionStr = local?.version || "0.0.0";
|
|
const remoteVersionStr = remote?.version || "0.0.0";
|
|
if (local?.version || remote?.version) {
|
|
const compare = `${localVersionStr}`.localeCompare(remoteVersionStr, undefined, { numeric: true });
|
|
if (compare == 0) {
|
|
version = "Same";
|
|
} else if (compare < 0) {
|
|
version = `Lower (${localVersionStr} < ${remoteVersionStr})`;
|
|
} else if (compare > 0) {
|
|
version = `Higher (${localVersionStr} > ${remoteVersionStr})`;
|
|
}
|
|
}
|
|
|
|
if (contentCheck) {
|
|
if (local && remote) {
|
|
const { canApply, equivalency, canCompare } = await checkEquivalency(local, remote);
|
|
return { canApply, freshness, equivalency, version, canCompare };
|
|
}
|
|
}
|
|
return { canApply, freshness, equivalency, version, canCompare };
|
|
}
|
|
|
|
async function checkEquivalency(local: IPluginDataExDisplay, remote: IPluginDataExDisplay) {
|
|
let equivalency = "";
|
|
let canApply = false;
|
|
let canCompare = false;
|
|
const filenames = [...new Set([...local.files.map((e) => e.filename), ...remote.files.map((e) => e.filename)])];
|
|
const matchingStatus = filenames
|
|
.map((filename) => {
|
|
const localFile = local.files.find((e) => e.filename == filename);
|
|
const remoteFile = remote.files.find((e) => e.filename == filename);
|
|
if (!localFile && !remoteFile) {
|
|
return 0b0000000;
|
|
} else if (localFile && !remoteFile) {
|
|
return 0b0000010; //"LOCAL_ONLY";
|
|
} else if (!localFile && remoteFile) {
|
|
return 0b0001000; //"REMOTE ONLY"
|
|
} else if (localFile && remoteFile) {
|
|
const localDoc = getDocData(localFile.data);
|
|
const remoteDoc = getDocData(remoteFile.data);
|
|
if (localDoc == remoteDoc) {
|
|
return 0b0000100; //"EVEN"
|
|
} else {
|
|
return 0b0010000; //"DIFFERENT";
|
|
}
|
|
} else {
|
|
return 0b0010000; //"DIFFERENT";
|
|
}
|
|
})
|
|
.reduce((p, c) => p | (c as number), 0 as number);
|
|
if (matchingStatus == 0b0000100) {
|
|
equivalency = "Same";
|
|
canApply = false;
|
|
} else if (matchingStatus <= 0b0000100) {
|
|
equivalency = "Same or local only";
|
|
canApply = false;
|
|
} else if (matchingStatus == 0b0010000) {
|
|
canApply = true;
|
|
canCompare = true;
|
|
equivalency = "Different";
|
|
} else {
|
|
canApply = true;
|
|
canCompare = true;
|
|
equivalency = "Mixed";
|
|
}
|
|
return { equivalency, canApply, canCompare };
|
|
}
|
|
|
|
async function performCompare(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined) {
|
|
const result = await comparePlugin(local, remote);
|
|
canApply = result.canApply;
|
|
freshness = result.freshness;
|
|
equivalency = result.equivalency;
|
|
version = result.version;
|
|
canCompare = result.canCompare;
|
|
pickToCompare = false;
|
|
if (canCompare) {
|
|
if (local?.files.length == remote?.files.length && local?.files.length == 1 && local?.files[0].filename == remote?.files[0].filename) {
|
|
pickToCompare = false;
|
|
} else {
|
|
pickToCompare = true;
|
|
// pickToCompare = false;
|
|
// canCompare = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function updateTerms(list: IPluginDataExDisplay[], selectNewest: boolean, isMaintenanceMode: boolean) {
|
|
const local = list.find((e) => e.term == thisTerm);
|
|
// selected = "";
|
|
if (isMaintenanceMode) {
|
|
terms = [...new Set(list.map((e) => e.term))];
|
|
} else if (hideNotApplicable) {
|
|
const termsTmp = [];
|
|
const wk = [...new Set(list.map((e) => e.term))];
|
|
for (const termName of wk) {
|
|
const remote = list.find((e) => e.term == termName);
|
|
if ((await comparePlugin(local, remote)).canApply) {
|
|
termsTmp.push(termName);
|
|
}
|
|
}
|
|
terms = [...termsTmp];
|
|
} else {
|
|
terms = [...new Set(list.map((e) => e.term))].filter((e) => e != thisTerm);
|
|
}
|
|
let newest: IPluginDataExDisplay | undefined = local;
|
|
if (selectNewest) {
|
|
for (const term of terms) {
|
|
const remote = list.find((e) => e.term == term);
|
|
if (remote && remote.mtime && (newest?.mtime || 0) < remote.mtime) {
|
|
newest = remote;
|
|
}
|
|
}
|
|
if (newest && newest.term != thisTerm) {
|
|
selected = newest.term;
|
|
}
|
|
// selectNewest = false;
|
|
}
|
|
if (terms.indexOf(selected) < 0) {
|
|
selected = "";
|
|
}
|
|
}
|
|
$: {
|
|
// React pulse and select
|
|
let doSelectNewest = false;
|
|
if (selectNewest != currentSelectNewest) {
|
|
if (selectNewestStyle == 1) {
|
|
doSelectNewest = true;
|
|
} else if (selectNewestStyle == 2) {
|
|
doSelectNewest = isFlagged;
|
|
} else if (selectNewestStyle == 3) {
|
|
selected = "";
|
|
}
|
|
// currentSelectNewest = selectNewest;
|
|
}
|
|
updateTerms(list, doSelectNewest, isMaintenanceMode);
|
|
currentSelectNewest = selectNewest;
|
|
}
|
|
$: {
|
|
// React pulse and apply
|
|
const doApply = applyAllPluse != currentApplyAll;
|
|
currentApplyAll = applyAllPluse;
|
|
if (doApply && selected) {
|
|
if (!hidden) {
|
|
applySelected();
|
|
}
|
|
}
|
|
}
|
|
$: {
|
|
freshness = "";
|
|
equivalency = "";
|
|
version = "";
|
|
canApply = false;
|
|
if (selected == "") {
|
|
// NO OP.
|
|
} else if (selected == thisTerm) {
|
|
freshness = "This device";
|
|
canApply = false;
|
|
} else {
|
|
const local = list.find((e) => e.term == thisTerm);
|
|
const remote = list.find((e) => e.term == selected);
|
|
performCompare(local, remote);
|
|
}
|
|
}
|
|
async function applySelected() {
|
|
const local = list.find((e) => e.term == thisTerm);
|
|
const selectedItem = list.find((e) => e.term == selected);
|
|
if (selectedItem && (await applyData(selectedItem))) {
|
|
addOn.updatePluginList(true, local?.documentPath);
|
|
}
|
|
}
|
|
async function compareSelected() {
|
|
const local = list.find((e) => e.term == thisTerm);
|
|
const selectedItem = list.find((e) => e.term == selected);
|
|
await compareItems(local, selectedItem);
|
|
}
|
|
async function compareItems(local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined, filename?: string) {
|
|
if (local && remote) {
|
|
if (!filename) {
|
|
if (await compareData(local, remote)) {
|
|
addOn.updatePluginList(true, local.documentPath);
|
|
}
|
|
return;
|
|
} else {
|
|
const localCopy = local instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(local) : { ...local };
|
|
const remoteCopy = remote instanceof PluginDataExDisplayV2 ? new PluginDataExDisplayV2(remote) : { ...remote };
|
|
localCopy.files = localCopy.files.filter((e) => e.filename == filename);
|
|
remoteCopy.files = remoteCopy.files.filter((e) => e.filename == filename);
|
|
if (await compareData(localCopy, remoteCopy, true)) {
|
|
addOn.updatePluginList(true, local.documentPath);
|
|
}
|
|
}
|
|
return;
|
|
} else {
|
|
if (!remote && !local) {
|
|
Logger(`Could not find both remote and local item`, LOG_LEVEL_INFO);
|
|
} else if (!remote) {
|
|
Logger(`Could not find remote item`, LOG_LEVEL_INFO);
|
|
} else if (!local) {
|
|
Logger(`Could not locally item`, LOG_LEVEL_INFO);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function pickCompareItem(evt: MouseEvent) {
|
|
const local = list.find((e) => e.term == thisTerm);
|
|
const selectedItem = list.find((e) => e.term == selected);
|
|
if (!local) return;
|
|
if (!selectedItem) return;
|
|
const menu = new Menu();
|
|
menu.addItem((item) => item.setTitle("Compare file").setIsLabel(true));
|
|
menu.addSeparator();
|
|
const files = unique(local.files.map((e) => e.filename).concat(selectedItem.files.map((e) => e.filename)));
|
|
for (const filename of files) {
|
|
menu.addItem((item) => {
|
|
item.setTitle(filename).onClick((e) => compareItems(local, selectedItem, filename));
|
|
});
|
|
}
|
|
menu.showAtMouseEvent(evt);
|
|
}
|
|
async function deleteSelected() {
|
|
const selectedItem = list.find((e) => e.term == selected);
|
|
// const deletedPath = selectedItem.documentPath;
|
|
if (selectedItem && (await deleteData(selectedItem))) {
|
|
addOn.reloadPluginList(true);
|
|
}
|
|
}
|
|
async function duplicateItem() {
|
|
const local = list.find((e) => e.term == thisTerm);
|
|
if (!local) {
|
|
Logger(`Could not find local item`, LOG_LEVEL_VERBOSE);
|
|
return;
|
|
}
|
|
const duplicateTermName = await askString(plugin.app, "Duplicate", "device name", "");
|
|
if (duplicateTermName) {
|
|
if (duplicateTermName.contains("/")) {
|
|
Logger(`We can not use "/" to the device name`, LOG_LEVEL_NOTICE);
|
|
return;
|
|
}
|
|
const key = `${plugin.app.vault.configDir}/${local.files[0].filename}`;
|
|
await addOn.storeCustomizationFiles(key as FilePath, duplicateTermName);
|
|
await addOn.updatePluginList(false, addOn.filenameToUnifiedKey(key, duplicateTermName));
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if terms.length > 0}
|
|
<span class="spacer" />
|
|
{#if !hidden}
|
|
<span class="chip-wrap">
|
|
<span class="chip modified">{freshness}</span>
|
|
<span class="chip content">{equivalency}</span>
|
|
<span class="chip version">{version}</span>
|
|
</span>
|
|
<select bind:value={selected}>
|
|
<option value={""}>-</option>
|
|
{#each terms as term}
|
|
<option value={term}>{term}</option>
|
|
{/each}
|
|
</select>
|
|
{#if canApply || (isMaintenanceMode && selected != "")}
|
|
{#if canCompare}
|
|
{#if pickToCompare}
|
|
<button on:click={pickCompareItem}>🗃️</button>
|
|
{:else}
|
|
<!--🔍 -->
|
|
<button on:click={compareSelected}>⮂</button>
|
|
{/if}
|
|
{:else}
|
|
<button disabled />
|
|
{/if}
|
|
<button on:click={applySelected}>✓</button>
|
|
{:else}
|
|
<button disabled />
|
|
<button disabled />
|
|
{/if}
|
|
{#if isMaintenanceMode}
|
|
{#if selected != ""}
|
|
<button on:click={deleteSelected}>🗑️</button>
|
|
{:else}
|
|
<button on:click={duplicateItem}>📑</button>
|
|
{/if}
|
|
{/if}
|
|
{/if}
|
|
{:else}
|
|
<span class="spacer" />
|
|
<span class="message even">All the same or non-existent</span>
|
|
<button disabled />
|
|
<button disabled />
|
|
{/if}
|
|
|
|
<style>
|
|
.spacer {
|
|
min-width: 1px;
|
|
flex-grow: 1;
|
|
}
|
|
button {
|
|
margin: 2px 4px;
|
|
min-width: 3em;
|
|
max-width: 4em;
|
|
}
|
|
button:disabled {
|
|
border: none;
|
|
box-shadow: none;
|
|
background-color: transparent;
|
|
visibility: collapse;
|
|
}
|
|
button:disabled:hover {
|
|
border: none;
|
|
box-shadow: none;
|
|
background-color: transparent;
|
|
visibility: collapse;
|
|
}
|
|
span.message {
|
|
color: var(--text-muted);
|
|
font-size: var(--font-ui-smaller);
|
|
padding: 0 1em;
|
|
line-height: var(--line-height-tight);
|
|
}
|
|
/* span.messages {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
} */
|
|
:global(.is-mobile) .spacer {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.chip-wrap {
|
|
display: flex;
|
|
gap: 2px;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
}
|
|
.chip {
|
|
display: inline-block;
|
|
border-radius: 2px;
|
|
font-size: 0.8em;
|
|
padding: 0 4px;
|
|
margin: 0 2px;
|
|
border-color: var(--tag-border-color);
|
|
background-color: var(--tag-background);
|
|
color: var(--tag-color);
|
|
}
|
|
.chip:empty {
|
|
display: none;
|
|
}
|
|
.chip:not(:empty)::before {
|
|
min-width: 1.8em;
|
|
display: inline-block;
|
|
}
|
|
.chip.content:not(:empty)::before {
|
|
content: "📄: ";
|
|
}
|
|
.chip.version:not(:empty)::before {
|
|
content: "🏷️: ";
|
|
}
|
|
.chip.modified:not(:empty)::before {
|
|
content: "📅: ";
|
|
}
|
|
</style>
|