Skip to content

Commit f83b287

Browse files
committed
feat: add duplicate finder view
1 parent 2606835 commit f83b287

File tree

9 files changed

+192
-66
lines changed

9 files changed

+192
-66
lines changed

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DUPLICATE_FINDER_VIEW = "duplicate-finder";

src/main.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,53 @@
11
import { Plugin } from "obsidian";
2-
import FindDuplicatesModal from "./obsidian/find-duplicates-modal";
2+
import { DUPLICATE_FINDER_VIEW } from "./constants";
3+
import DuplicateFinderView from "./obsidian/duplicate-finder-view";
34

4-
interface FindDuplicatesSettings {}
5+
interface DuplicateFinderSettings {}
56

6-
const DEFAULT_SETTINGS: FindDuplicatesSettings = {};
7+
const DEFAULT_SETTINGS: DuplicateFinderSettings = {};
78

8-
export default class FindDuplicatesPlugin extends Plugin {
9-
settings: FindDuplicatesSettings = DEFAULT_SETTINGS;
9+
export default class DuplicateFinderPlugin extends Plugin {
10+
settings: DuplicateFinderSettings = DEFAULT_SETTINGS;
1011

1112
async onload() {
1213
await this.loadSettings();
1314

15+
this.registerView(
16+
DUPLICATE_FINDER_VIEW,
17+
(leaf) => new DuplicateFinderView(leaf, this)
18+
);
19+
1420
this.addCommand({
15-
id: "open-find-duplicates",
16-
name: "Open Find Duplicates",
17-
callback: () => {
18-
new FindDuplicatesModal(this.app).open();
21+
id: "open",
22+
name: "Open finder",
23+
callback: async () => {
24+
this.openFinderView();
1925
},
2026
});
21-
// // This adds a settings tab so the user can configure various aspects of the plugin
27+
28+
this.addRibbonIcon("file-search", "Open finder", async () => {
29+
this.openFinderView();
30+
});
31+
32+
// TODO add when needed
2233
// this.addSettingTab(new SampleSettingTab(this.app, this));
2334
}
2435

36+
private openFinderView() {
37+
const leaves = this.app.workspace.getLeavesOfType(
38+
DUPLICATE_FINDER_VIEW
39+
);
40+
if (leaves.length !== 0) {
41+
const leaf = leaves[0];
42+
this.app.workspace.revealLeaf(leaf);
43+
} else {
44+
this.app.workspace.getLeaf("tab").setViewState({
45+
type: DUPLICATE_FINDER_VIEW,
46+
active: true,
47+
});
48+
}
49+
}
50+
2551
onunload() {}
2652

2753
async loadSettings() {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { App, PluginSettingTab } from "obsidian";
2+
import DuplicateFinderPlugin from "src/main";
3+
4+
export class DuplicateFinderSettingTab extends PluginSettingTab {
5+
plugin: DuplicateFinderPlugin;
6+
7+
constructor(app: App, plugin: DuplicateFinderPlugin) {
8+
super(app, plugin);
9+
this.plugin = plugin;
10+
}
11+
12+
display(): void {
13+
const { containerEl } = this;
14+
15+
containerEl.empty();
16+
}
17+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ItemView, WorkspaceLeaf } from "obsidian";
2+
import { DUPLICATE_FINDER_VIEW } from "src/constants";
3+
import type DuplicateFinderPlugin from "src/main";
4+
import { mount, unmount } from "svelte";
5+
import SvelteApp from "../svelte/index.svelte";
6+
7+
export default class DuplicateFinderView extends ItemView {
8+
svelteApp: ReturnType<typeof mount> | null;
9+
plugin: DuplicateFinderPlugin;
10+
11+
constructor(leaf: WorkspaceLeaf, plugin: DuplicateFinderPlugin) {
12+
super(leaf);
13+
this.svelteApp = null;
14+
this.plugin = plugin;
15+
this.navigation = true;
16+
}
17+
18+
getIcon(): string {
19+
return "file-search";
20+
}
21+
22+
getViewType(): string {
23+
return DUPLICATE_FINDER_VIEW;
24+
}
25+
getDisplayText(): string {
26+
return "Duplicate Finder";
27+
}
28+
29+
async onOpen() {
30+
const { contentEl } = this;
31+
this.svelteApp = mount(SvelteApp, {
32+
target: contentEl,
33+
props: { obsidianApp: this.app },
34+
});
35+
}
36+
37+
async onClose() {
38+
if (this.svelteApp) {
39+
unmount(this.svelteApp);
40+
}
41+
}
42+
}

src/obsidian/find-duplicates-modal.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/obsidian/find-duplicates-setting-tab.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/svelte/index.svelte

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
2-
import type { App } from "obsidian";
3-
import { findDuplicateUrls } from "src/find-duplicate-urls";
2+
import { Menu, type App } from "obsidian";
3+
import { findDuplicateUrls } from "src/utils/find-duplicate-urls";
4+
import { openInNewTab, openToTheRight } from "src/utils/open-file-utils";
45
import { onMount } from "svelte";
56
67
interface AppProps {
@@ -11,26 +12,85 @@
1112
1213
let duplicateUrls: Map<string, string[]> = $state(new Map());
1314
15+
function handleItemClick(filePath: string) {
16+
openInNewTab(obsidianApp, filePath);
17+
}
18+
19+
function handleItemContextMenuClick(event: MouseEvent, filePath: string) {
20+
const menu = new Menu();
21+
menu.setUseNativeMenu(true);
22+
menu.addItem((item) => {
23+
item.setTitle("Open in new tab");
24+
item.onClick(() => openInNewTab(obsidianApp, filePath));
25+
});
26+
menu.addItem((item) => {
27+
item.setTitle("Open to the right");
28+
item.onClick(() => openToTheRight(obsidianApp, filePath));
29+
});
30+
menu.showAtMouseEvent(event);
31+
}
32+
1433
onMount(async () => {
1534
duplicateUrls = await findDuplicateUrls(obsidianApp);
1635
});
36+
37+
let duplicateUrlCount = $derived(
38+
Array.from(duplicateUrls.entries()).filter(
39+
([_, files]) => files.length > 1,
40+
).length,
41+
);
1742
</script>
1843

1944
<div>
20-
<h1 class="wtf">Find Duplicates</h1>
21-
{#each Array.from(duplicateUrls.entries()) as [url, files]}
22-
<div>
23-
<h2>{url}</h2>
24-
<p>{files.length} files</p>
25-
</div>
26-
{/each}
45+
<h1>Duplicate Finder</h1>
46+
<p>Duplicate URLs: {duplicateUrlCount}</p>
47+
<div class="accordion-container">
48+
{#each Array.from(duplicateUrls.entries()) as [url, files]}
49+
{#if files.length > 1}
50+
<div class="accordion">
51+
<details>
52+
<summary>{url} ({files.length} files)</summary>
53+
{#each files as file}
54+
<div
55+
role="button"
56+
tabindex="0"
57+
class="accordion-item"
58+
onkeydown={(event) => {
59+
if (event.key === "Enter") {
60+
handleItemClick(file);
61+
}
62+
}}
63+
onclick={() => handleItemClick(file)}
64+
oncontextmenu={(event) =>
65+
handleItemContextMenuClick(event, file)}
66+
>
67+
{file}
68+
</div>
69+
{/each}
70+
</details>
71+
</div>
72+
{/if}
73+
{/each}
74+
</div>
2775
</div>
2876

2977
<style>
30-
:global(h1) {
31-
font-size: 1rem;
78+
h1 {
79+
font-size: 2rem;
3280
}
33-
:global(.wtf) {
34-
color: red;
81+
82+
.accordion-item {
83+
padding: 8px 16px;
84+
border-bottom: 1px solid #e0e0e0;
85+
}
86+
87+
.accordion-item:hover {
88+
background-color: var(--background-modifier-hover);
89+
}
90+
91+
.accordion-container {
92+
display: flex;
93+
flex-direction: column;
94+
gap: 16px;
3595
}
3696
</style>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const findDuplicateUrls = async (
1414
if (!urlMap.has(url)) {
1515
urlMap.set(url, []);
1616
}
17-
urlMap.get(url)?.push(file.basename);
17+
urlMap.get(url)?.push(file.path);
1818
}
1919
}
2020
}

src/utils/open-file-utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { App } from "obsidian";
2+
3+
export const openToTheRight = (app: App, filePath: string) => {
4+
app.workspace.openLinkText(filePath, "", "split", {
5+
active: false,
6+
});
7+
};
8+
9+
export const openInNewTab = async (app: App, filePath: string) => {
10+
const isOpen = app.workspace.getLeavesOfType("markdown").some((leaf) => {
11+
const viewState = leaf.getViewState();
12+
return viewState.state?.file === filePath;
13+
});
14+
15+
if (isOpen) {
16+
return;
17+
}
18+
19+
app.workspace.openLinkText(filePath, "", "tab", {
20+
active: false,
21+
});
22+
};

0 commit comments

Comments
 (0)