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:
parent
6744a2736b
commit
6fe6a43cf0
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue