Skip to content

Commit

Permalink
feat(ui): ✨ conflicts list modal (#84)
Browse files Browse the repository at this point in the history
* refactor(ui): ♻️ list of conflicts files

complete refactor of the UI to show the list of conflicts files.
Need to check the sorting methods before merging
Simpler, cleaner, more efficient UI.

* fix(controllers): 🐛 sort by conflict date

utility function for sorting by conflict date

* refactor(ui): 💄 update conflict details
  • Loading branch information
LBF38 authored Jul 26, 2023
1 parent 9147dab commit 401337e
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 105 deletions.
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions src/components/conflict_file_details.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!-- This file is for providing a reusable component to display information about a sync conflict file. -->
<script lang="ts">
import { TFile } from "obsidian";
import { SyncthingController } from "src/controllers/main_controller";
import { Failure } from "src/models/failures";
import Error from "./error.svelte";
import { formatBytes } from "src/controllers/utils";
export let counter: number;
export let file: TFile;
export let syncthingController: SyncthingController;
const filenameProps = syncthingController.parseConflictFilename(file.name);
</script>

{#if filenameProps instanceof Failure}
<Error failure={filenameProps} />
{:else}
<div>
<strong>Conflict #{counter}</strong>
<em>
Occured on: {filenameProps.dateTime.toLocaleString()}
</em>
<ul>
<li>
Conflict date: {filenameProps.dateTime.toLocaleString()}
</li>
<li>
Modified by: {filenameProps.modifiedBy}
</li>
<li>
Extension: <code>{file.extension}</code>
</li>
<li>Size: {formatBytes(file.stat.size, 1)}</li>
<li>
Last modified at: {new Date(file.stat.mtime).toLocaleString()}
</li>
<li>Created at: {new Date(file.stat.ctime).toLocaleString()}</li>
<li>Path: {file.path}</li>
</ul>
</div>
{/if}

<style>
/* TODO */
</style>
83 changes: 83 additions & 0 deletions src/components/conflict_item.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
import { TFile } from "obsidian";
import { SyncthingController } from "src/controllers/main_controller";
import { Failure } from "src/models/failures";
import { ConflictsModal } from "src/views/conflicts_modal";
import { DiffModal } from "src/views/diff_modal";
import ConflictFileDetails from "./conflict_file_details.svelte";
export let conflicts: TFile[];
export let syncthingController: SyncthingController;
export let parentModal: ConflictsModal;
let filenameProps = syncthingController.parseConflictFilename(
conflicts[0].name
);
if (filenameProps instanceof Failure) {
console.error(filenameProps);
}
</script>

<details>
<summary>
<p>
<span>
{#if filenameProps instanceof Failure}
{filenameProps.message}
{:else}
{filenameProps.filename}
{/if}
</span>
<span>
({conflicts.length} conflict{conflicts.length > 1 ? "s" : ""})
</span>
</p>

<button
class="mod-cta"
on:click={() =>
new DiffModal(
parentModal.app,
conflicts[0],
syncthingController
).open()}
>
View conflict{conflicts.length > 1 ? "s" : ""}
</button>
</summary>
{#each conflicts as conflict, i}
{#if i !== 0}
<div class="divider" />
{/if}
<ConflictFileDetails
file={conflict}
{syncthingController}
counter={i}
/>
{/each}
</details>

<style>
.divider {
margin-top: 10px;
margin-bottom: 10px;
border-bottom: 2px solid var(--background-modifier-border);
}
details {
padding: 1% 2%;
border-radius: 10px;
}
details:hover {
background-color: var(--background-modifier-hover);
}
summary {
list-style: none;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
summary > p {
display: flex;
flex-direction: column;
}
</style>
63 changes: 63 additions & 0 deletions src/components/conflicts_list.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
import { Setting, TFile } from "obsidian";
import { ConflictsModal } from "src/views/conflicts_modal";
import { onMount } from "svelte";
import ConflictItem from "./conflict_item.svelte";
import { sortFilesBy } from "src/controllers/utils";
export let parentModal: ConflictsModal;
export let conflicts: Map<string, TFile[]>;
let sortSettingContainer: HTMLDivElement;
let sortOptions = {
recent: "Most recent",
old: "Least recent",
"a-to-z": "A to Z",
"z-to-a": "Z to A",
};
onMount(() => {
new Setting(sortSettingContainer)
.setName("Sort by date")
.addDropdown((dropdown) => {
dropdown.addOptions(sortOptions);
dropdown.onChange((value) => {
conflicts = sortFilesBy(
conflicts,
value as keyof typeof sortOptions,
parentModal.syncthingController
);
console.log("dropdown", conflicts);
});
});
conflicts = sortFilesBy(
conflicts,
"recent",
parentModal.syncthingController
);
});
parentModal.titleEl.setText("Syncthing Conflicts");
$: if (conflicts) console.log("conflicts", conflicts);
</script>

<div bind:this={sortSettingContainer} />
{#key conflicts}
{#each conflicts.keys() as conflictNames, i}
{#if i !== 0}
<div class="divider" />
{/if}
<ConflictItem
conflicts={conflicts.get(conflictNames) ?? []}
syncthingController={parentModal.syncthingController}
{parentModal}
/>
{/each}
{/key}

<style>
.divider {
margin-top: 10px;
margin-bottom: 10px;
border-bottom: 2px solid var(--background-modifier-border);
}
</style>
20 changes: 20 additions & 0 deletions src/components/error.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { Failure } from "src/models/failures";
export let failure: Failure | { message: string }; // For the error message
</script>

<p>
A failure occured :
{failure.message}
</p>

<style>
p {
background-color: darkred;
opacity: 0.6;
border-radius: 10px;
box-shadow: black 0px 0px 10px;
padding: 1%;
}
</style>
8 changes: 8 additions & 0 deletions src/controllers/main_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CliFailure, Failure, RestFailure } from "src/models/failures";
import { type SyncThingFromCLI } from "src/data/syncthing_local_datasource";
import { type SyncThingFromREST } from "src/data/syncthing_remote_datasource";
import { type SyncthingFromAndroid } from "src/data/syncthing_android_datasource";
import { sortByConflictDate } from "./utils";

export interface SyncthingController {
/**
Expand Down Expand Up @@ -173,6 +174,13 @@ export class SyncthingControllerImpl implements SyncthingController {
if (conflictsFilesMap.has(filename)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
conflictsFilesMap.get(filename)!.push(file);
conflictsFilesMap.set(
filename,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
conflictsFilesMap
.get(filename)!
.sort((a, b) => sortByConflictDate(a, b, this))
);
continue;
}
conflictsFilesMap.set(filename, [file]);
Expand Down
125 changes: 125 additions & 0 deletions src/controllers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// --- My utility functions --- //

import { TFile } from "obsidian";
import { Failure } from "src/models/failures";
import { SyncthingController } from "./main_controller";

/**
* This utility function is used to sort the files in conflict by date, name, etc.
* @param files the file map to sort
* @param type the sort type
* @param syncthingController the Syncthing controller instance
* @returns the sorted file map
*/
export function sortFilesBy(
files: Map<string, TFile[]>,
type: "recent" | "old" | "a-to-z" | "z-to-a",
syncthingController: SyncthingController
): Map<string, TFile[]> {
switch (type) {
case "recent":
return new Map(
[...files.entries()].sort((a, b) =>
sortByConflictDate(a[1], b[1], syncthingController)
)
);
case "old":
return new Map(
[...files.entries()]
.sort((a, b) =>
sortByConflictDate(a[1], b[1], syncthingController)
)
.reverse()
);
case "a-to-z":
return new Map([...files.entries()].sort());
case "z-to-a":
return new Map([...files.entries()].sort().reverse());
}
}

/**
* Compares two files by conflict date.
* It uses {@link SyncthingController.parseConflictFilename} from the Syncthing controller.
*
* TODO: refactor the `parseConflictFilename` method to a utility function.
* @param a file A to compare
* @param b file B to compare
* @param syncthingController the Syncthing controller instance
*/
export function sortByConflictDate(
a: TFile,
b: TFile,
syncthingController: SyncthingController
): number;

/**
* Compares two lists of files by conflict date.
* It uses {@link SyncthingController.parseConflictFilename} from the Syncthing controller.
*
* TODO: refactor the `parseConflictFilename` method to a utility function.
* @param a list of files to compare
* @param b list of files to compare
* @param syncthingController the Syncthing controller instance
*/
export function sortByConflictDate(
a: TFile[],
b: TFile[],
syncthingController: SyncthingController
): number;

export function sortByConflictDate(
a: TFile | TFile[],
b: TFile | TFile[],
syncthingController: SyncthingController
): number {
if (a instanceof TFile && b instanceof TFile) {
const filepropsA = syncthingController.parseConflictFilename(
a.basename
);

const filepropsB = syncthingController.parseConflictFilename(
b.basename
);
const dateA =
filepropsA instanceof Failure ? new Date() : filepropsA.dateTime;
const dateB =
filepropsB instanceof Failure ? new Date() : filepropsB.dateTime;
return dateB.getTime() - dateA.getTime();
}
// The `as` typing is because TypeScript doesn't seem to understand that we are left with the TFile[] type
const fileA = (a as TFile[]).sort((a, b) =>
sortByConflictDate(a, b, syncthingController)
)[0];
const fileB = (b as TFile[]).sort((a, b) =>
sortByConflictDate(a, b, syncthingController)
)[0];
return sortByConflictDate(fileA, fileB, syncthingController);
}

/**
* This utility function allows to correctly format the file size w/ a unit.
* @param bytes file size to format
* @param decimals number of decimals to display
* @returns string with formatted file size
*
*
* Credits for the `formatBytes` function:
* @see https://github.com/CattailNu/obsidian-file-info-panel-plugin
*
* obsidian-file-info-panel-plugin
*
* @copyright T. L. Ford
* @see https://www.Cattail.Nu
*/
export function formatBytes(bytes: number, decimals = 2): string {
if (bytes === 0) return "0 Bytes";

const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

const i = Math.floor(Math.log(bytes) / Math.log(k));

return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
Loading

0 comments on commit 401337e

Please sign in to comment.