From 5e46995c895925591fb302e7e289f04b1c2130d6 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Wed, 8 Jul 2026 14:07:50 +0200 Subject: [PATCH] feat(repositories): add favorite repositories feature This update introduces a favorites system for repositories, allowing users to mark certain repositories as favorites for easier access. The UI has been modified to include a star icon for toggling favorites, and the layout has been adjusted to display favorite repositories separately. - Implemented favorite repository management with local storage - Updated UI to show favorite repositories with a star icon - Enhanced repository listing to filter and display favorites --- src/App.svelte | 81 ++++++++++++++++++++++++++++++++++++++++++-------- src/app.css | 8 ++++- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index c021194..c8e8c49 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,7 +2,7 @@ import { onDestroy, onMount, tick } from "svelte"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater"; - import { AlertCircle, BookOpen, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, X } from "@lucide/svelte"; + import { AlertCircle, BookOpen, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte"; import TitleBar from "./lib/TitleBar.svelte"; import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte"; @@ -153,6 +153,7 @@ const OPEN_REPOS_KEY = "gitlite.openRepos.v1"; const RECENT_REPOS_KEY = "gitlite.recentRepos.v1"; + const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1"; const REPO_STATUS_CACHE_KEY = "gitlite.repoStatusCache.v1"; const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1"; @@ -173,7 +174,8 @@ let activeView: AppView = "management"; let repoTabs: RepoTab[] = []; let recentRepoPaths: string[] = []; - // Last-seen branch/ahead/behind/changed for repos that are known (recent/all) + let favoriteRepoPaths: string[] = []; + // Last-seen branch/ahead/behind/changed for repos that are known (recent/favorites) // but not currently open as a tab — keyed by normalized path (repoKey). let repoStatusCache: Record = {}; let repoSearch = ""; @@ -330,7 +332,7 @@ .filter((path) => !repoTabs.some((tab) => sameRepoPath(tab.path, path))) .map(repoRowFromPath) .filter(repoMatchesSearch); - $: allRepoRows = uniqueRepoPaths([...repoTabs.map((tab) => tab.path), ...recentRepoPaths]) + $: favoriteRepoRows = favoriteRepoPaths .map(repoRowFromPath) .filter(repoMatchesSearch); @@ -434,10 +436,10 @@ // One repo per tick, round-robin: spreads the git subprocess cost over time // instead of firing N fetches at once when many repos are open. // Every repo we know about besides the active one: open tabs (minus the active - // tab) plus recent repos that aren't currently open — the same universe the - // Recent/All repositories lists in Repository Management draw from. + // tab) plus recent/favorite repos that aren't currently open — the same + // universe the Repository Management lists draw from. function knownRepoPathsForBackground(): string[] { - return uniqueRepoPaths([...repoTabs.map((tab) => tab.path), ...recentRepoPaths]) + return uniqueRepoPaths([...repoTabs.map((tab) => tab.path), ...recentRepoPaths, ...favoriteRepoPaths]) .filter((path) => !(activeView === "repository" && sameRepoPath(path, activeRepoPath))); } @@ -846,12 +848,16 @@ const openValue = JSON.parse(localStorage.getItem(OPEN_REPOS_KEY) ?? "[]") as unknown; const recentValue = JSON.parse(localStorage.getItem(RECENT_REPOS_KEY) ?? "[]") as unknown; + const favoriteValue = JSON.parse(localStorage.getItem(FAVORITE_REPOS_KEY) ?? "[]") as unknown; const openPaths = Array.isArray(openValue) ? openValue.map((item) => typeof item === "string" ? item : "").filter(Boolean) : []; const recentPaths = Array.isArray(recentValue) ? recentValue.map((item) => typeof item === "string" ? item : "").filter(Boolean) : []; + const favoritePaths = Array.isArray(favoriteValue) + ? favoriteValue.map((item) => typeof item === "string" ? item : "").filter(Boolean) + : []; // Seed from the last-known status cache so tabs show real data immediately // on startup, instead of blank until the background poll catches up. @@ -862,9 +868,11 @@ : { path, name: repoNameFromPath(path), branch: null, ahead: 0, behind: 0, changed: 0, lastOpened: 0 }; }); recentRepoPaths = uniqueRepoPaths([...recentPaths, ...openPaths]); + favoriteRepoPaths = uniqueRepoPaths(favoritePaths); } catch { repoTabs = []; recentRepoPaths = []; + favoriteRepoPaths = []; repoStatusCache = {}; } } @@ -873,6 +881,7 @@ try { localStorage.setItem(OPEN_REPOS_KEY, JSON.stringify(repoTabs.map((tab) => tab.path))); localStorage.setItem(RECENT_REPOS_KEY, JSON.stringify(recentRepoPaths)); + localStorage.setItem(FAVORITE_REPOS_KEY, JSON.stringify(favoriteRepoPaths)); } catch { // Local storage is best-effort only; the Git workflow must keep working without it. } @@ -1027,6 +1036,19 @@ persistRepoLists(); } + function isFavoriteRepo(path: string): boolean { + return favoriteRepoPaths.some((favoritePath) => sameRepoPath(favoritePath, path)); + } + + function toggleFavoriteRepo(path: string, event?: MouseEvent) { + event?.stopPropagation(); + if (isBusy) return; + favoriteRepoPaths = isFavoriteRepo(path) + ? favoriteRepoPaths.filter((favoritePath) => !sameRepoPath(favoritePath, path)) + : uniqueRepoPaths([path, ...favoriteRepoPaths]); + persistRepoLists(); + } + function upsertRepoTab(path: string, nextStatus?: GitStatus | null) { const existing = repoTabs.find((tab) => sameRepoPath(tab.path, path)); const next: RepoTab = { @@ -1049,7 +1071,7 @@ // Keeps last-known branch/ahead/behind/changed around under the repo's // normalized path, independent of repoTabs — so a closed tab (or a repo // that's only ever shown up in "recent") still displays real data instead - // of the "known repo" placeholder in the Recent/All repositories lists. + // of the "known repo" placeholder in the Recent/Favorites repositories lists. function cacheRepoStatus(row: RepoTab) { repoStatusCache = { ...repoStatusCache, [repoKey(row.path)]: row }; persistRepoStatusCache(); @@ -1416,6 +1438,7 @@ event?.stopPropagation(); if (isBusy) return; recentRepoPaths = recentRepoPaths.filter((recentPath) => !sameRepoPath(recentPath, path)); + favoriteRepoPaths = favoriteRepoPaths.filter((favoritePath) => !sameRepoPath(favoritePath, path)); persistRepoLists(); if (repoStatusCache[repoKey(path)]) { const { [repoKey(path)]: _removed, ...rest } = repoStatusCache; @@ -2722,6 +2745,17 @@ {#if repo.changed > 0}{repo.changed} changed{/if} + @@ -2752,6 +2786,17 @@ {#if repo.changed > 0}{repo.changed} changed{/if} + @@ -2763,25 +2808,35 @@
-

All repositories

- {allRepoRows.length} +

Favorites

+ {favoriteRepoRows.length}
- {#if allRepoRows.length === 0} -
Browse for a repository to add it here.
+ {#if favoriteRepoRows.length === 0} +
Mark repositories with the star to keep them here.
{:else}
- {#each allRepoRows as repo (repo.path)} + {#each favoriteRepoRows as repo (repo.path)}
+ diff --git a/src/app.css b/src/app.css index a0a5f37..b0448e9 100644 --- a/src/app.css +++ b/src/app.css @@ -782,7 +782,7 @@ } .repo-row { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto auto; align-items: stretch; min-height: 36px; border-bottom: 1px solid rgba(255,255,255,0.035); @@ -852,6 +852,12 @@ background: transparent; color: var(--color-ink-faint); } + .repo-row-favorite.active { + color: #e0c15f; + } + .repo-row-favorite.active svg { + fill: currentColor; + } /* --- Notices --- */