|
1 | 1 | <script lang="ts"> |
2 | 2 | import { type FileStatus, getGithubUsername, GITHUB_URL_PARAM, installGithubApp, loginWithGithub, logoutGithub } from "$lib/github.svelte"; |
3 | | - import { Button, Dialog, Separator } from "bits-ui"; |
| 3 | + import { Button, Dialog, Separator, Popover } from "bits-ui"; |
4 | 4 | import InfoPopup from "./InfoPopup.svelte"; |
5 | 5 | import { page } from "$app/state"; |
6 | 6 | import { goto } from "$app/navigation"; |
|
12 | 12 | import { createTwoFilesPatch } from "diff"; |
13 | 13 | import DirectorySelect from "$lib/components/files/DirectorySelect.svelte"; |
14 | 14 | import { DirectoryEntry, FileEntry } from "$lib/components/files/index.svelte"; |
| 15 | + import { SvelteSet } from "svelte/reactivity"; |
15 | 16 |
|
16 | 17 | const viewer = MultiFileDiffViewerState.get(); |
17 | 18 |
|
|
23 | 24 | let fileB = $state<File | undefined>(undefined); |
24 | 25 | let dirA = $state<DirectoryEntry | undefined>(undefined); |
25 | 26 | let dirB = $state<DirectoryEntry | undefined>(undefined); |
| 27 | + let dirBlacklistInput = $state<string>(""); |
| 28 | + const defaultDirBlacklist = [".git/"]; |
| 29 | + let dirBlacklist = new SvelteSet(defaultDirBlacklist); |
| 30 | + let dirBlacklistRegexes = $derived.by(() => { |
| 31 | + return Array.from(dirBlacklist).map((pattern) => new RegExp(pattern)); |
| 32 | + }); |
| 33 | +
|
| 34 | + function addBlacklistEntry() { |
| 35 | + if (dirBlacklistInput === "") { |
| 36 | + return; |
| 37 | + } |
| 38 | + dirBlacklist.add(dirBlacklistInput); |
| 39 | + dirBlacklistInput = ""; |
| 40 | + } |
26 | 41 |
|
27 | 42 | onMount(async () => { |
28 | 43 | const url = page.url.searchParams.get(GITHUB_URL_PARAM); |
|
101 | 116 | file: File; |
102 | 117 | }; |
103 | 118 |
|
104 | | - // TODO: option to respect gitignore? |
105 | 119 | async function compareDirs() { |
106 | 120 | if (!dirA || !dirB) { |
107 | 121 | alert("Both directories must be selected to compare."); |
108 | 122 | return; |
109 | 123 | } |
110 | 124 |
|
111 | | - const entriesA: ProtoFileDetails[] = flatten(dirA); |
112 | | - const entriesB: ProtoFileDetails[] = flatten(dirB); |
| 125 | + const blacklist = (entry: ProtoFileDetails) => { |
| 126 | + return !dirBlacklistRegexes.some((pattern) => pattern.test(entry.path)); |
| 127 | + }; |
| 128 | + const entriesA: ProtoFileDetails[] = flatten(dirA).filter(blacklist); |
| 129 | + const entriesB: ProtoFileDetails[] = flatten(dirB).filter(blacklist); |
113 | 130 |
|
114 | 131 | const fileDetails: FileDetails[] = []; |
115 | 132 |
|
|
312 | 329 | } |
313 | 330 | </script> |
314 | 331 |
|
| 332 | +{#snippet blacklistPopoverContent()} |
| 333 | + <div class="mb-2 flex bg-neutral-2 py-2 ps-2 pe-6"> |
| 334 | + <span class="me-1 text-lg font-semibold">Blacklist patterns</span> |
| 335 | + <InfoPopup>Regex patterns for directories and files to ignore.</InfoPopup> |
| 336 | + </div> |
| 337 | + <div class="flex items-center gap-1 px-2"> |
| 338 | + <div class="flex"> |
| 339 | + <input |
| 340 | + bind:value={dirBlacklistInput} |
| 341 | + onkeydown={(e) => { |
| 342 | + if (e.key === "Enter") { |
| 343 | + addBlacklistEntry(); |
| 344 | + } |
| 345 | + }} |
| 346 | + type="text" |
| 347 | + class="w-full rounded-l-md border-t border-b border-l px-2 py-1" |
| 348 | + /> |
| 349 | + <Button.Root title="Add blacklist entry" class="flex rounded-r-md btn-primary px-2 py-1" onclick={addBlacklistEntry}> |
| 350 | + <span class="iconify size-4 shrink-0 place-self-center octicon--plus-16" aria-hidden="true"></span> |
| 351 | + </Button.Root> |
| 352 | + </div> |
| 353 | + <Button.Root |
| 354 | + title="Reset blacklist to defaults" |
| 355 | + class="flex rounded-md btn-danger p-1" |
| 356 | + onclick={() => { |
| 357 | + dirBlacklist.clear(); |
| 358 | + defaultDirBlacklist.forEach((entry) => { |
| 359 | + dirBlacklist.add(entry); |
| 360 | + }); |
| 361 | + }} |
| 362 | + > |
| 363 | + <span class="iconify size-4 shrink-0 place-self-center octicon--undo-16" aria-hidden="true"></span> |
| 364 | + </Button.Root> |
| 365 | + </div> |
| 366 | + <ul class="m-2 max-h-96 overflow-y-auto rounded-md border"> |
| 367 | + {#each dirBlacklist as entry (entry)} |
| 368 | + <li class="flex"> |
| 369 | + <span class="grow border-b px-2 py-1">{entry}</span> |
| 370 | + <div class="border-b p-1 ps-0"> |
| 371 | + <Button.Root |
| 372 | + title="Delete blacklist entry" |
| 373 | + class="flex rounded-md btn-danger p-1" |
| 374 | + onclick={() => { |
| 375 | + dirBlacklist.delete(entry); |
| 376 | + }} |
| 377 | + > |
| 378 | + <span class="iconify size-4 shrink-0 place-self-center octicon--trash-16" aria-hidden="true"></span> |
| 379 | + </Button.Root> |
| 380 | + </div> |
| 381 | + </li> |
| 382 | + {/each} |
| 383 | + {#if dirBlacklist.size === 0} |
| 384 | + <li class="px-2 py-1 text-em-med">No patterns added</li> |
| 385 | + {/if} |
| 386 | + </ul> |
| 387 | +{/snippet} |
| 388 | + |
315 | 389 | <Dialog.Root bind:open={modalOpen}> |
316 | 390 | <Dialog.Trigger class="h-fit rounded-md btn-primary px-2 py-0.5" onclick={() => (dragActive = false)}>Load another diff</Dialog.Trigger> |
317 | 391 | <Dialog.Portal> |
318 | 392 | <Dialog.Overlay class="fixed inset-0 z-50 bg-black/50 dark:bg-white/20" /> |
319 | 393 | <Dialog.Content class="fixed top-1/2 left-1/2 z-50 w-192 max-w-[95%] -translate-x-1/2 -translate-y-1/2 rounded-md bg-neutral shadow-md"> |
320 | 394 | <header class="relative flex flex-row items-center justify-between rounded-t-md bg-neutral-2 p-4"> |
321 | 395 | <Dialog.Title class="text-xl font-semibold">Load a diff</Dialog.Title> |
322 | | - <Dialog.Close class="flex size-6 items-center justify-center rounded-md btn-ghost text-primary"> |
323 | | - <span class="iconify octicon--x-16"></span> |
| 396 | + <Dialog.Close title="Close dialog" class="flex size-6 items-center justify-center rounded-md btn-ghost text-primary"> |
| 397 | + <span class="iconify octicon--x-16" aria-hidden="true"></span> |
324 | 398 | </Dialog.Close> |
325 | 399 | </header> |
326 | 400 |
|
|
354 | 428 | <span class="iconify shrink-0 octicon--person-16"></span> |
355 | 429 | {getGithubUsername()} |
356 | 430 | </div> |
357 | | - <Button.Root class="flex items-center gap-2 rounded-md bg-red-400 px-2 py-1 text-white hover:bg-red-500" onclick={logoutGithub}> |
| 431 | + <Button.Root class="flex items-center gap-2 rounded-md btn-danger px-2 py-1" onclick={logoutGithub}> |
358 | 432 | <span class="iconify shrink-0 octicon--sign-out-16"></span> |
359 | 433 | Sign out |
360 | 434 | </Button.Root> |
|
413 | 487 | </section> |
414 | 488 |
|
415 | 489 | <section> |
416 | | - <h4 class="mb-2 font-semibold">Compare Directories</h4> |
417 | | - <div class="flex flex-row items-center gap-1"> |
418 | | - <DirectorySelect bind:directory={dirA} placeholder="Directory A" /> |
419 | | - <span class="iconify size-4 shrink-0 octicon--arrow-right-16"></span> |
420 | | - <DirectorySelect bind:directory={dirB} placeholder="Directory B" /> |
421 | | - <Button.Root onclick={compareDirs} class="rounded-md btn-primary px-2 py-1">Go</Button.Root> |
| 490 | + <div class="mb-2 flex items-center"> |
| 491 | + <h4 class="me-1 font-semibold">Compare Directories</h4> |
422 | 492 | <InfoPopup> |
423 | 493 | Compares the entire contents of the directories, including subdirectories. Does not attempt to detect renames. When possible, |
424 | 494 | preparing a unified diff (<code class="rounded-sm bg-neutral-2 px-1 py-0.5">.patch</code> file) using Git or another tool and loading |
425 | 495 | it with the above button should be preferred. |
426 | 496 | </InfoPopup> |
427 | 497 | </div> |
| 498 | + <div class="flex items-center gap-1"> |
| 499 | + <DirectorySelect bind:directory={dirA} placeholder="Directory A" /> |
| 500 | + <span class="iconify size-4 shrink-0 octicon--arrow-right-16"></span> |
| 501 | + <DirectorySelect bind:directory={dirB} placeholder="Directory B" /> |
| 502 | + <div class="flex"> |
| 503 | + <Button.Root onclick={compareDirs} class="relative rounded-l-md btn-primary"> |
| 504 | + <div class="px-2 py-1">Go</div> |
| 505 | + <div class="absolute top-0 right-0 h-full w-px bg-neutral-3/20"></div> |
| 506 | + </Button.Root> |
| 507 | + <Popover.Root> |
| 508 | + <Popover.Trigger title="Edit filters" class="flex rounded-r-md btn-primary p-2 data-[state=open]:btn-primary-hover"> |
| 509 | + <span class="iconify size-4 shrink-0 place-self-center octicon--filter-16" aria-hidden="true"></span> |
| 510 | + </Popover.Trigger> |
| 511 | + <Popover.Content side="top" class="overflow-hidden rounded-md border bg-neutral"> |
| 512 | + {@render blacklistPopoverContent()} |
| 513 | + </Popover.Content> |
| 514 | + </Popover.Root> |
| 515 | + </div> |
| 516 | + </div> |
428 | 517 | </section> |
429 | 518 | </section> |
430 | 519 | </Dialog.Content> |
|
0 commit comments