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
This commit is contained in:
+68
-13
@@ -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<string, RepoTab> = {};
|
||||
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}<em>{repo.changed} changed</em>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-row-icon repo-row-favorite"
|
||||
class:active={isFavoriteRepo(repo.path)}
|
||||
type="button"
|
||||
onclick={(event) => toggleFavoriteRepo(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
title={isFavoriteRepo(repo.path) ? "Remove from favorites" : "Add to favorites"}
|
||||
aria-label={isFavoriteRepo(repo.path) ? `Remove ${repo.name} from favorites` : `Add ${repo.name} to favorites`}
|
||||
>
|
||||
<Star size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button class="repo-row-icon" type="button" onclick={(event) => closeRepoTab(repo.path, event)} disabled={isBusy} title="Close tab" aria-label={`Close ${repo.name}`}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -2752,6 +2786,17 @@
|
||||
{#if repo.changed > 0}<em>{repo.changed} changed</em>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-row-icon repo-row-favorite"
|
||||
class:active={isFavoriteRepo(repo.path)}
|
||||
type="button"
|
||||
onclick={(event) => toggleFavoriteRepo(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
title={isFavoriteRepo(repo.path) ? "Remove from favorites" : "Add to favorites"}
|
||||
aria-label={isFavoriteRepo(repo.path) ? `Remove ${repo.name} from favorites` : `Add ${repo.name} to favorites`}
|
||||
>
|
||||
<Star size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button class="repo-row-icon" type="button" onclick={(event) => removeRepoFromManagement(repo.path, event)} disabled={isBusy} title="Remove from recent" aria-label={`Remove ${repo.name}`}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -2763,25 +2808,35 @@
|
||||
|
||||
<section class="repo-section">
|
||||
<header>
|
||||
<h2>All repositories</h2>
|
||||
<span>{allRepoRows.length}</span>
|
||||
<h2>Favorites</h2>
|
||||
<span>{favoriteRepoRows.length}</span>
|
||||
</header>
|
||||
{#if allRepoRows.length === 0}
|
||||
<div class="repo-empty">Browse for a repository to add it here.</div>
|
||||
{#if favoriteRepoRows.length === 0}
|
||||
<div class="repo-empty">Mark repositories with the star to keep them here.</div>
|
||||
{:else}
|
||||
<div class="repo-table">
|
||||
{#each allRepoRows as repo (repo.path)}
|
||||
{#each favoriteRepoRows as repo (repo.path)}
|
||||
<div class="repo-row">
|
||||
<button class="repo-row-main" type="button" onclick={() => openRepo(repo.path)} disabled={isBusy}>
|
||||
<span class="repo-row-name">{repo.name}</span>
|
||||
<span class="repo-row-path">{repo.path}</span>
|
||||
<span class="repo-row-meta">
|
||||
{#if repo.branch}<strong><GitBranch size={11} aria-hidden="true" />{repo.branch}</strong>{:else}<em class="quiet">known repo</em>{/if}
|
||||
{#if repo.branch}<strong><GitBranch size={11} aria-hidden="true" />{repo.branch}</strong>{:else}<em class="quiet">favorite</em>{/if}
|
||||
{#if repo.ahead > 0}<em class="ahead">↑ {repo.ahead}</em>{/if}
|
||||
{#if repo.behind > 0}<em class="behind">↓ {repo.behind}</em>{/if}
|
||||
{#if repo.changed > 0}<em>{repo.changed} changed</em>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-row-icon repo-row-favorite active"
|
||||
type="button"
|
||||
onclick={(event) => toggleFavoriteRepo(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
title="Remove from favorites"
|
||||
aria-label={`Remove ${repo.name} from favorites`}
|
||||
>
|
||||
<Star size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button class="repo-row-icon" type="button" onclick={(event) => removeRepoFromManagement(repo.path, event)} disabled={isBusy} title="Remove" aria-label={`Remove ${repo.name}`}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user