Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
- **Manual Update Check**: A "Check now" button in the Updates section lets you check your selected channel on demand instead of waiting for the startup check.
- **Instant Grid on Launch**: Opening a library now shows its packages immediately from the local index while a fresh scan reconciles with disk in the background — no more waiting on an empty grid. Files deleted since the last scan drop off automatically once the scan completes.
- **Cross-Library Dependencies**: In the details panel, a dependency or "Used By" entry that lives in a *different* library is now labelled with that library's name and is clickable — clicking switches to that library and **locates & highlights** the package in the grid (your current selection and open panel stay put). Those rows are coloured by the target's enabled/disabled state, so only the library label is blue. Previously such entries were unclickable and could leave the grid empty.
- **Ratings & Favourites**: You can now rate a package from **1 to 5 stars** and mark it as a **favourite**. Right-click any package to set its rating or toggle the heart; ratings and favourites are then searchable and filterable from the sidebar (the star row, the *Only favourites* toggle) or the search bar (`rating:4`, `rating:>=3`, `favorite:true`). A rating applies to the whole package family, so every version shares it, and your choices persist in the local database. (Desktop for now; the mobile/web entry point arrives with the upcoming card redesign.)

### Changed
- **Redesigned Sidebar Filters**: **Status**, **Creators**, and **Categories** are now compact **dropdowns** that overlay when opened instead of long lists that pushed the panel around, each with its own built-in search and each showing the values you've picked. The freed space adds new filters: a **star rating** control, an **Only favourites** toggle, and a **dependency-relationship** group — **Standalone** (packages that declare no dependencies) and **Removable** (packages nothing depends on, so removing them breaks nothing). "Removable" reports the same whether a package is enabled or disabled, since a disabled package still exists in your library. "All Packages" became a **Clear all** action that appears only when a filter is active, and the library's total package count now sits centred beneath its name. The show/hide animation is smoother on desktop and mobile. The star rating and *Only favourites* controls now filter live (see **Ratings & Favourites** above).
- **Local Database (Foundation)**: Package and library metadata is now indexed into a local SQLite database, laying the groundwork for faster startups, persistent ratings/favourites, and upcoming features. Your `config.json` remains the source of truth for configured library paths, so the change is fully backward-compatible.
- **Dependency Version Fallback**: Clicking a dependency whose exact version isn't installed no longer dead-ends with "specific version not found". YAVAM now takes you to the newest available copy — locally or in another library — with a heads-up that the exact version wasn't found.
- **Deletion**: Improved package deletion behaviour for more reliable and predictable results.
Expand Down
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,16 @@ fatal.

## Code quality & style

- **Self-documenting code.** Prefer readable code over comments. Comments explain
*how a system/architecture works and why* — not what each line does. Delete
comments that merely restate the code.
- **Self-documenting code — comments are the exception, not the default.** Make the
code readable enough that it needs no narration: clear names, small functions,
early returns. Do **not** comment each step, restate what a line does, or label
sections (`// optimistic`, `// rollback`, `// Fetch on mount`, `// Local State`).
A comment must earn its place by explaining *why* — a non-obvious constraint,
trade-off, or gotcha the code cannot express. When you reach for a comment, first
try to make it unnecessary by renaming or restructuring. Prefer a docstring on an
exported symbol (function/type/component) over inline prose, and keep it to the
contract and rationale — not a line-by-line tour. When in doubt, leave it out.
When editing existing code, delete comments that merely restate it.
- **Apply design patterns deliberately.** For non-trivial work, evaluate where a
pattern fits (and where it doesn't), and say so.
- **DRY & SOLID.** Extract repeated rules into one definition (`pkg/utils` or a
Expand Down
12 changes: 12 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,18 @@ func (a *App) LocateDependencies(ids []string) map[string]models.DependencyLocat
return a.manager.LocateDependencies(ids)
}

// SetPackageRating stores a 0–5 star rating (0 clears) for a package family
// ("Creator.Name"), version-agnostic — every version of the family shares one
// rating. Persisted in the user_metadata table.
func (a *App) SetPackageRating(family string, rating int) error {
return a.manager.SetPackageRating(family, rating)
}

// SetPackageFavorite marks a package family ("Creator.Name") as a favourite.
func (a *App) SetPackageFavorite(family string, favorite bool) error {
return a.manager.SetPackageFavorite(family, favorite)
}

func (a *App) ScanPackages(vamPath string) error {
if vamPath == "" || vamPath == "." {
return nil
Expand Down
28 changes: 19 additions & 9 deletions docs/design/search-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,29 @@ token → AND (must match)
## Token types

```
status:enabled status:disabled status:missing status:corrupt status:standalone
status:hidden status:visible
status:enabled status:disabled status:missing status:corrupt
status:standalone status:removable status:hidden status:visible
creator:acidbubbles
type:scene
tag:dress
license:cc-by license:pc license:pc-ea
rating:>=4
rating:>=4 rating:<=2 rating:3
favorite:true
size:>500mb size:10mb..100mb
```

- `tag:` is available through search only; there is no dedicated tag sidebar
section.
- `size:` supports a single bound (`size:>100mb`) or a range (`size:10mb..500mb`).
- `status:standalone` narrows to standalone packages even when the dependency
visibility mode is `all`.
- `rating:` supports a single bound (`rating:>=4`, `rating:<3`) or an exact
value (`rating:5`). The sidebar's star control emits the exact form
(`rating:N`) — with no way to also set an upper bound, a minimum could not
express "only the 3-star ones". Ratings are version-agnostic (keyed by family)
and stored in `user_metadata`; an unrated package counts as `rating:0`.
- The two dependency-relationship axes are orthogonal and both backed today:
`status:standalone` = the package declares **no dependencies**;
`status:removable` = **no package depends on it**, so removing it breaks
nothing (enable-agnostic — a disabled package still counts as existing).

## Examples

Expand Down Expand Up @@ -110,10 +117,13 @@ which itself waits on the scan/validation rework that switching libraries needs.

## Not-yet-backed tokens

`rating:`, `favorite:`, and `license:` are parsed and shown as chips but do not
filter until the ratings/favourites data layer exists; they are inert no-ops
until then. `status:standalone`, `status:hidden`, and `status:visible` likewise
wait on the dependency-visibility mode.
`license:` is parsed and shown as a chip but does not filter until license data
is surfaced; it is an inert no-op until then. `status:hidden` and
`status:visible` likewise wait on the dependency-visibility mode.
`status:standalone`, `status:removable`, `rating:`, and `favorite:` are backed
and filter live. Ratings/favourites are joined onto each package from the
`user_metadata` store (keyed by family) on the cache-first read path; they are
written on desktop through the package right-click menu and the sidebar controls.

## Related: dependency-visibility mode

Expand Down
2 changes: 1 addition & 1 deletion docs/design/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ compatible.
--yavam-card-enabled --yavam-card-disabled
--yavam-card-corrupt --yavam-card-missing-deps
--yavam-card-duplicate --yavam-card-obsolete
--yavam-card-standalone /* "Standalone" in UI; internal code: isOrphan */
--yavam-card-standalone /* root packages nothing depends on; internal code: isRemovable */

/* UI chrome */
--yavam-creator-label-bg --yavam-sidebar-icon-active
Expand Down
2 changes: 1 addition & 1 deletion docs/domain/frontend-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const Dashboard = () => {

## 6. Centralized Logic Patterns
### Package Status Authority
Package status (Valid, Corrupt, Duplicate, Root) is complex and derived from multiple properties (`isCorrupt`, `missingDeps`, `isOrphan`).
Package status (Valid, Corrupt, Duplicate, Root) is complex and derived from multiple properties (`isCorrupt`, `missingDeps`, `isRemovable`).
**Rule:** NEVER implement ad-hoc `if/else` checks for package status in your components (e.g., `PackageCard`, `Sidebar`).
**Solution:** Always use the centralized helper: `src/features/library/utils.ts` -> `getPackageStatus(pkg)`.
This ensures that "Duplicate", "Obsolete", and "Root" statuses are visualized consistently across the entire application (Grid, List, Dependants).
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ const DashboardContent = () => {

const {
handleBulkToggle, handleOpenFolder, setInstallModal, handleCopyPath,
handleCopyFiles, handleCutFile, handleDeleteClick, handleInstantMerge, handleSingleResolve
handleCopyFiles, handleCutFile, handleDeleteClick, handleInstantMerge, handleSingleResolve,
setPackageRating, setPackageFavorite
} = useActionContext();

// Drag & Drop (Upload)
Expand Down Expand Up @@ -333,6 +334,8 @@ const DashboardContent = () => {
onMerge={(p) => handleInstantMerge(p, false)}
onMergeInPlace={(p) => handleInstantMerge(p, true)}
onResolve={handleSingleResolve}
onSetRating={setPackageRating}
onSetFavorite={setPackageFavorite}
/>
)}

Expand Down
72 changes: 70 additions & 2 deletions frontend/src/components/ui/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef, useEffect, useState, useLayoutEffect } from 'react';
import { VarPackage } from '../../types';
import { Power, FolderOpen, Copy, Trash2, FileCode, Scissors, Download, Layers, Sparkles } from 'lucide-react';
import clsx from 'clsx';
import { Power, FolderOpen, Copy, Trash2, FileCode, Scissors, Download, Layers, Sparkles, Star, Heart } from 'lucide-react';

interface ContextMenuProps {
x: number;
Expand All @@ -18,14 +19,62 @@ interface ContextMenuProps {
onMerge: (pkg: VarPackage) => void;
onMergeInPlace: (pkg: VarPackage) => void;
onResolve: (pkg: VarPackage) => void;
onSetRating: (pkg: VarPackage, rating: number) => void;
onSetFavorite: (pkg: VarPackage, favorite: boolean) => void;
}

const ContextMenu = ({ x, y, pkg, selectedCount = 0, onClose, onToggle, onOpenFolder, onDownload, onCopyPath, onCopyFiles, onCutFile, onDelete, onMerge, onMergeInPlace, onResolve }: ContextMenuProps) => {
// Controlled rating row for the right-click menu: `value` is the live rating,
// `onRate` receives the star clicked (the parent resolves click-active-to-clear).
const MenuRatingStars = ({ value, onRate }: { value: number, onRate: (n: number) => void }) => {
const [hover, setHover] = useState(0);
const shown = hover || value;
return (
<div className="flex items-center gap-0.5" onMouseLeave={() => setHover(0)}>
{[1, 2, 3, 4, 5].map(n => (
<button
key={n}
onClick={() => onRate(n)}
onMouseEnter={() => setHover(n)}
className="p-0.5 text-gray-500 hover:text-yellow-400 transition-colors"
title={`Rate ${n} star${n > 1 ? 's' : ''}`}
>
<Star size={15} className={clsx(n <= shown ? "fill-yellow-400 text-yellow-400" : "fill-transparent")} />
</button>
))}
</div>
);
};

const ContextMenu = ({ x, y, pkg, selectedCount = 0, onClose, onToggle, onOpenFolder, onDownload, onCopyPath, onCopyFiles, onCutFile, onDelete, onMerge, onMergeInPlace, onResolve, onSetRating, onSetFavorite }: ContextMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ top: y, left: x });
// @ts-ignore
const isWeb = !window.go;

// Live rating/favourite state so the row updates the instant it's clicked,
// independent of the (snapshot) pkg prop. Reseeded when the menu retargets a
// different package.
const [rating, setRating] = useState(pkg?.rating ?? 0);
const [isFavorite, setIsFavorite] = useState(!!pkg?.isFavorite);
useEffect(() => {
setRating(pkg?.rating ?? 0);
setIsFavorite(!!pkg?.isFavorite);
}, [pkg?.filePath]); // eslint-disable-line react-hooks/exhaustive-deps

const rate = (n: number) => {
if (!pkg) return;
const next = n === rating ? 0 : n; // click the active star to clear
setRating(next);
onSetRating(pkg, next);
};

const toggleFavorite = () => {
if (!pkg) return;
const next = !isFavorite;
setIsFavorite(next);
onSetFavorite(pkg, next);
};

useLayoutEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
Expand Down Expand Up @@ -75,6 +124,25 @@ const ContextMenu = ({ x, y, pkg, selectedCount = 0, onClose, onToggle, onOpenFo
{selectedCount > 1 ? `${selectedCount} items selected` : pkg.fileName}
</div>

{/* Rating + favourite (user_metadata, version-agnostic per family).
Desktop-only: web has no persistence endpoint yet. */}
{!isWeb && (
<>
<div className="flex items-center justify-between px-3 py-1.5">
<MenuRatingStars value={rating} onRate={rate} />
<button
onClick={toggleFavorite}
className={clsx("p-0.5 transition-colors", isFavorite ? "text-pink-400" : "text-gray-500 hover:text-pink-400")}
title={isFavorite ? "Remove from favourites" : "Add to favourites"}
>
<Heart size={16} className={clsx(isFavorite && "fill-pink-400")} />
</button>
</div>

<div className="border-t border-gray-700 my-1"></div>
</>
)}

<button
onClick={() => { onToggle(pkg); onClose(); }}
className="w-full text-left px-3 py-2 hover:bg-gray-700 flex items-center gap-2 text-white"
Expand Down
Loading