feat: add average rating and star count to media retrieval and display

- Updated media retrieval query to include average ratings and star counts for media files.
- Enhanced FolderViewer and VideosPage components to display star ratings, improving user feedback on media quality.
- Integrated StarRating component for visual representation of ratings in the UI.
This commit is contained in:
tigeren 2025-08-27 14:53:30 +00:00
parent 6744a2736b
commit 6fe6a43cf0
5 changed files with 102 additions and 4 deletions

BIN
media.db

Binary file not shown.

View File

@ -20,10 +20,10 @@ export async function GET(request: Request) {
// Get media files from database for this path // Get media files from database for this path
const mediaFiles = db.prepare(` const mediaFiles = db.prepare(`
SELECT id, path, type, thumbnail SELECT id, path, type, thumbnail, avg_rating, star_count
FROM media FROM media
WHERE path LIKE ? WHERE path LIKE ?
`).all(`${dirPath}%`) as Array<{id: number, path: string, type: string, thumbnail: string | null}>; `).all(`${dirPath}%`) as Array<{id: number, path: string, type: string, thumbnail: string | null, avg_rating: number, star_count: number}>;
const result = files.map((file) => { const result = files.map((file) => {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
@ -48,6 +48,8 @@ export async function GET(request: Request) {
type: mediaFile?.type || type, type: mediaFile?.type || type,
thumbnail: mediaFile?.thumbnail, thumbnail: mediaFile?.thumbnail,
id: mediaFile?.id, id: mediaFile?.id,
avg_rating: mediaFile?.avg_rating || 0,
star_count: mediaFile?.star_count || 0,
}; };
}); });

View File

@ -3,7 +3,8 @@
import { useState, useEffect, Suspense } from "react"; import { useState, useEffect, Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation"; import { useSearchParams, useRouter } from "next/navigation";
import { Folder, File, Image, Film, Play, ChevronLeft, Home } from "lucide-react"; import { Folder, File, Image, Film, Play, ChevronLeft, Home, Star } from "lucide-react";
import { StarRating } from "@/components/star-rating";
import Link from "next/link"; import Link from "next/link";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import PhotoViewer from "@/components/photo-viewer"; import PhotoViewer from "@/components/photo-viewer";
@ -18,6 +19,8 @@ interface FileSystemItem {
thumbnail?: string; thumbnail?: string;
type?: string; type?: string;
id?: number; id?: number;
avg_rating?: number;
star_count?: number;
} }
interface BreadcrumbItem { interface BreadcrumbItem {
@ -364,6 +367,19 @@ const FolderViewerPage = () => {
</div> </div>
<div className="p-3"> <div className="p-3">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">{item.name}</p> <p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">{item.name}</p>
{/* Star Rating for media files */}
{isMediaFile(item) && (item.avg_rating || 0) > 0 && (
<div className="mb-2">
<StarRating
rating={item.avg_rating || 0}
count={item.star_count || 0}
size="sm"
showCount={true}
/>
</div>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-xs text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</p> <p className="text-xs text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</p>
{!item.isDirectory && ( {!item.isDirectory && (

View File

@ -2,11 +2,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { Film, Play, Clock, HardDrive, Search, Filter } from "lucide-react"; import { Film, Play, Clock, HardDrive, Search, Filter, Star } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import VideoViewer from "@/components/video-viewer"; import VideoViewer from "@/components/video-viewer";
import { StarRating } from "@/components/star-rating";
interface Video { interface Video {
id: number; id: number;
@ -225,6 +226,19 @@ const VideosPage = () => {
<h3 className="font-medium text-foreground text-sm line-clamp-2 mb-2 group-hover:text-primary transition-colors"> <h3 className="font-medium text-foreground text-sm line-clamp-2 mb-2 group-hover:text-primary transition-colors">
{video.title} {video.title}
</h3> </h3>
{/* Star Rating */}
{(video.avg_rating > 0 || video.star_count > 0) && (
<div className="mb-2">
<StarRating
rating={video.avg_rating || 0}
count={video.star_count}
size="sm"
showCount={true}
/>
</div>
)}
<div className="flex items-center gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<HardDrive className="h-3 w-3" /> <HardDrive className="h-3 w-3" />

View File

@ -0,0 +1,66 @@
'use client';
import { Star } from 'lucide-react';
import { cn } from '@/lib/utils';
interface StarRatingProps {
rating: number;
count?: number;
size?: 'sm' | 'md' | 'lg';
showCount?: boolean;
interactive?: boolean;
onRate?: (rating: number) => void;
className?: string;
}
export function StarRating({
rating,
count = 0,
size = 'md',
showCount = false,
interactive = false,
onRate,
className
}: StarRatingProps) {
const sizeClasses = {
sm: 'h-3 w-3',
md: 'h-4 w-4',
lg: 'h-5 w-5'
};
const handleRate = (newRating: number) => {
if (interactive && onRate) {
onRate(newRating);
}
};
return (
<div className={cn("flex items-center gap-1", className)}>
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
disabled={!interactive}
onClick={() => handleRate(star)}
className={cn(
"transition-colors",
interactive ? "cursor-pointer hover:text-yellow-400" : "cursor-default",
star <= Math.round(rating) ? "text-yellow-400" : "text-gray-300"
)}
>
<Star
className={cn(
sizeClasses[size],
star <= Math.round(rating) ? "fill-yellow-400" : "fill-transparent"
)}
/>
</button>
))}
{showCount && count > 0 && (
<span className="text-xs text-muted-foreground ml-1">
({count})
</span>
)}
</div>
);
}