Compare commits

...

3 Commits

Author SHA1 Message Date
tigeren a56492f36a feat: refine path display and layout adjustments for folder and media grids
- Enhanced path display logic to accommodate longer directory names and improved truncation for better clarity.
- Adjusted grid item dimensions and padding for a more consistent and responsive layout across components.
- Updated title formatting and line clamping to enhance readability and prevent overflow in media item displays.
2025-08-29 15:45:44 +00:00
tigeren efd5e70e1f feat: enhance folder viewer and grid layout for improved usability
- Updated path display logic to show full paths or truncated versions based on length, enhancing clarity for users navigating directories.
- Adjusted grid layout and item dimensions for better responsiveness and visual consistency across different screen sizes.
- Improved title formatting and added line clamping to prevent overflow, ensuring a cleaner presentation of media item names.
- Refined star rating component to support additional size options, allowing for better integration with various UI elements.
2025-08-29 15:37:31 +00:00
tigeren 6c289e591d feat: update video viewer layout and enhance video info display
- Simplified the title overlay in the video viewer by removing unnecessary elements for a cleaner look.
- Introduced a new video info bar that displays the video title and size, improving user accessibility to video details.
- Maintained existing bookmark and rating functionalities while enhancing their layout for better usability.
2025-08-29 02:55:10 +00:00
6 changed files with 155 additions and 118 deletions

BIN
media.db

Binary file not shown.

View File

@ -107,13 +107,23 @@ const FolderViewerPage = () => {
const directory = path.substring(0, lastSeparatorIndex); const directory = path.substring(0, lastSeparatorIndex);
const filename = path.substring(lastSeparatorIndex + 1); const filename = path.substring(lastSeparatorIndex + 1);
// If directory is short enough, show it all // If the full path is short enough, show it all
if (directory.length <= 30) { if (path.length <= 80) {
return `${directory}/${filename}`; return path;
} }
// Truncate directory with ellipsis in the middle // If directory is short enough, show directory + truncated filename
const maxDirLength = 25; if (directory.length <= 50) {
const maxFilenameLength = 80 - directory.length - 1; // -1 for "/"
if (filename.length <= maxFilenameLength) {
return `${directory}/${filename}`;
} else {
return `${directory}/${filename.substring(0, maxFilenameLength - 3)}...`;
}
}
// For longer paths, show more of the directory structure
const maxDirLength = 45;
const startLength = Math.floor(maxDirLength / 2); const startLength = Math.floor(maxDirLength / 2);
const endLength = maxDirLength - startLength - 3; // -3 for "..." const endLength = maxDirLength - startLength - 3; // -3 for "..."
@ -121,7 +131,25 @@ const FolderViewerPage = () => {
? `${directory.substring(0, startLength)}...${directory.substring(directory.length - endLength)}` ? `${directory.substring(0, startLength)}...${directory.substring(directory.length - endLength)}`
: directory; : directory;
return `${truncatedDir}/${filename}`; // Add filename if there's space
const remainingSpace = 80 - truncatedDir.length - 1; // -1 for "/"
if (remainingSpace > 3) {
return `${truncatedDir}/${filename.length > remainingSpace ? filename.substring(0, remainingSpace - 3) + '...' : filename}`;
}
return `${truncatedDir}/...`;
};
const formatTitle = (title: string) => {
if (!title) return '';
// If title is short enough, return as is
if (title.length <= 40) {
return title;
}
// For longer titles, truncate to prevent awkward third-line display
return title.length > 60 ? title.substring(0, 60) + '...' : title;
}; };
const getLibraryRoot = (currentPath: string): string | null => { const getLibraryRoot = (currentPath: string): string | null => {
@ -351,7 +379,7 @@ const FolderViewerPage = () => {
</h1> </h1>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{error && ( {error && (
<div className="col-span-full text-center py-12"> <div className="col-span-full text-center py-12">
<div className="max-w-sm mx-auto"> <div className="max-w-sm mx-auto">
@ -374,9 +402,9 @@ const FolderViewerPage = () => {
{!error && Array.isArray(items) && items.map((item) => ( {!error && Array.isArray(items) && items.map((item) => (
<div key={item.name} <div key={item.name}
className={`group relative bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 overflow-hidden ${(item.type === 'video' || item.type === 'photo') ? 'cursor-pointer' : ''}`}> className={`group relative bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 overflow-hidden min-h-[240px] ${(item.type === 'video' || item.type === 'photo') ? 'cursor-pointer' : ''}`}>
<Link href={item.isDirectory ? `/folder-viewer?path=${item.path}` : '#'} <Link href={item.isDirectory ? `/folder-viewer?path=${item.path}` : '#'}
className="block" className="block h-full flex flex-col"
onClick={(e) => { onClick={(e) => {
if (item.type === 'video' && item.id) { if (item.type === 'video' && item.id) {
e.preventDefault(); e.preventDefault();
@ -387,7 +415,7 @@ const FolderViewerPage = () => {
handlePhotoClick(item, photoIndex); handlePhotoClick(item, photoIndex);
} }
}}> }}>
<div className="aspect-square relative overflow-hidden"> <div className="aspect-[4/3] relative overflow-hidden">
{item.isDirectory ? ( {item.isDirectory ? (
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/20 dark:to-indigo-900/20 flex items-center justify-center"> <div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/20 dark:to-indigo-900/20 flex items-center justify-center">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg" <div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg"
@ -396,44 +424,29 @@ const FolderViewerPage = () => {
</div> </div>
</div> </div>
) : isMediaFile(item) ? ( ) : isMediaFile(item) ? (
<div className={`relative overflow-hidden ${item.type === 'video' ? 'aspect-video' : 'aspect-[4/3]'}`}> <div className="relative overflow-hidden aspect-[4/3] bg-black rounded-t-xl">
<img <img
src={item.thumbnail || (item.type === 'video' ? '/placeholder-video.svg' : '/placeholder-photo.svg')} src={item.thumbnail || (item.type === 'video' ? '/placeholder-video.svg' : '/placeholder-photo.svg')}
alt={item.name} alt={item.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
{item.type === 'video' ? (
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg">
<Film className="h-4 w-4 text-slate-800" />
</div>
) : (
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg">
<Image className="h-4 w-4 text-slate-800" />
</div>
)}
</div>
{item.type === 'video' && ( {item.type === 'video' && (
<> <>
{/* Always visible small play icon */} <div className="absolute top-2 right-2 bg-black/60 text-white rounded-full p-1">
<div className="absolute top-2 right-2 bg-black/70 backdrop-blur-sm rounded-full p-1.5"> <Play className="h-3 w-3" />
<Play className="h-3 w-3 text-white" />
</div> </div>
{/* Large play button on hover */}
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="bg-white/90 backdrop-blur-sm rounded-full p-3 shadow-lg">
<Play className="h-6 w-6 text-slate-800" />
</div>
</div>
{/* Loading overlay when video is being opened */}
{isVideoLoading && selectedVideo?.id === item.id && ( {isVideoLoading && selectedVideo?.id === item.id && (
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center"> <div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div> <div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent"></div>
</div> </div>
)} )}
</> </>
)} )}
{item.type === 'photo' && (
<div className="absolute top-2 right-2 bg-black/60 text-white rounded-full p-1">
<Image className="h-3 w-3" />
</div>
)}
</div> </div>
) : ( ) : (
<div className="absolute inset-0 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800 flex items-center justify-center" <div className="absolute inset-0 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800 flex items-center justify-center"
@ -445,32 +458,27 @@ const FolderViewerPage = () => {
</div> </div>
)} )}
</div> </div>
<div className="p-3"> <div className="p-2.5 flex flex-col flex-1 bg-slate-50 dark:bg-slate-800/50 border-t border-slate-200 dark:border-slate-700">
<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 line-clamp-2 leading-tight mb-1 min-h-[2rem]" title={item.name}>{formatTitle(item.name)}</p>
{/* Star Rating for media files */} <div className="flex items-center justify-between text-xs mb-1">
{isMediaFile(item) && (item.avg_rating || 0) > 0 && ( <span className="text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</span>
<div className="mb-2"> {isMediaFile(item) && (item.avg_rating || 0) > 0 && (
<StarRating <StarRating
rating={item.avg_rating || 0} rating={item.avg_rating || 0}
count={item.star_count || 0} count={item.star_count || 0}
size="sm" size="xs"
showCount={true} showCount={false}
/> />
</div>
)}
<div className="flex items-center justify-between">
<p className="text-xs text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</p>
{!item.isDirectory && (
<p
className="text-xs text-slate-500 dark:text-slate-500 truncate ml-2 cursor-help"
title={item.path}
>
{formatFilePath(item.path)}
</p>
)} )}
</div> </div>
<p
className="text-xs text-slate-500 dark:text-slate-400 line-clamp-2 leading-tight cursor-help flex-1"
title={item.path}
>
{formatFilePath(item.path)}
</p>
</div> </div>
</Link> </Link>
</div> </div>

View File

@ -27,7 +27,7 @@ interface InfiniteVirtualGridProps {
onRate: (id: number, rating: number) => Promise<void>; onRate: (id: number, rating: number) => Promise<void>;
} }
const ITEM_HEIGHT = 300; const ITEM_HEIGHT = 220;
const ITEMS_PER_BATCH = 50; const ITEMS_PER_BATCH = 50;
const BUFFER_SIZE = 200; const BUFFER_SIZE = 200;
@ -70,11 +70,11 @@ export default function InfiniteVirtualGrid({
const directory = path.substring(0, lastSeparatorIndex); const directory = path.substring(0, lastSeparatorIndex);
const filename = path.substring(lastSeparatorIndex + 1); const filename = path.substring(lastSeparatorIndex + 1);
if (directory.length <= 30) { if (directory.length <= 40) {
return `${directory}/${filename}`; return `${directory}/${filename}`;
} }
const maxDirLength = 25; const maxDirLength = 35;
const startLength = Math.floor(maxDirLength / 2); const startLength = Math.floor(maxDirLength / 2);
const endLength = maxDirLength - startLength - 3; const endLength = maxDirLength - startLength - 3;
@ -354,7 +354,7 @@ export default function InfiniteVirtualGrid({
className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer border-border overflow-hidden h-full" className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer border-border overflow-hidden h-full"
onClick={() => onItemClick(item)} onClick={() => onItemClick(item)}
> >
<div className={`relative overflow-hidden bg-muted ${type === 'video' ? 'aspect-video' : 'aspect-square'}`}> <div className="relative overflow-hidden bg-muted aspect-video">
<img <img
src={item.thumbnail || (type === 'video' ? "/placeholder-video.svg" : "/placeholder-photo.svg")} src={item.thumbnail || (type === 'video' ? "/placeholder-video.svg" : "/placeholder-photo.svg")}
alt={item.title} alt={item.title}
@ -384,34 +384,54 @@ export default function InfiniteVirtualGrid({
</div> </div>
</div> </div>
<CardContent className="p-3"> <CardContent className="p-2.5">
<h3 className="font-medium text-foreground text-sm line-clamp-2 mb-2 group-hover:text-primary transition-colors"> <div className="flex items-start justify-between min-h-[2rem]">
{item.title || item.path.split('/').pop()} <div className="flex-1 min-w-0">
</h3> <h3 className="font-medium text-foreground text-xs line-clamp-2 group-hover:text-primary transition-colors leading-tight">
{item.title || item.path.split('/').pop()}
{(item.avg_rating > 0 || item.star_count > 0) && ( </h3>
<div className="mb-2">
<StarRating {(item.avg_rating > 0 || item.star_count > 0) && (
rating={item.avg_rating || 0} <div className="mt-0.5">
count={item.star_count} <StarRating
size="sm" rating={item.avg_rating || 0}
showCount={true} count={item.star_count}
/> size="xs"
showCount={false}
/>
</div>
)}
</div> </div>
)}
<div className="flex gap-1 ml-1 flex-shrink-0">
<div className="flex items-center gap-2 text-xs text-muted-foreground"> {type === 'video' && item.bookmark_count > 0 && (
<div className="flex items-center gap-1"> <div className="text-xs text-yellow-500">
<HardDrive className="h-3 w-3" /> <Bookmark className="h-2.5 w-2.5 fill-yellow-500" />
<span>{formatFileSize(item.size)}</span> </div>
)}
</div> </div>
</div> </div>
<p
className="text-xs text-muted-foreground mt-1 line-clamp-1 cursor-help" <div className="mt-1 space-y-0.5">
title={item.path} <div className="flex items-center justify-between text-xs text-muted-foreground">
> <div className="flex items-center gap-1">
{formatFilePath(item.path)} <HardDrive className="h-2.5 w-2.5" />
</p> <span>{formatFileSize(item.size)}</span>
</div>
{type === 'video' && item.bookmark_count > 0 && (
<span className="text-xs text-muted-foreground">
{item.bookmark_count}
</span>
)}
</div>
<p
className="text-xs text-muted-foreground line-clamp-1 cursor-help hover:text-foreground transition-colors"
title={item.path}
>
{formatFilePath(item.path)}
</p>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -6,7 +6,7 @@ import { cn } from '@/lib/utils';
interface StarRatingProps { interface StarRatingProps {
rating: number; rating: number;
count?: number; count?: number;
size?: 'sm' | 'md' | 'lg'; size?: 'xs' | 'sm' | 'md' | 'lg';
showCount?: boolean; showCount?: boolean;
interactive?: boolean; interactive?: boolean;
onRate?: (rating: number) => void; onRate?: (rating: number) => void;
@ -23,6 +23,7 @@ export function StarRating({
className className
}: StarRatingProps) { }: StarRatingProps) {
const sizeClasses = { const sizeClasses = {
xs: 'h-2.5 w-2.5',
sm: 'h-3 w-3', sm: 'h-3 w-3',
md: 'h-4 w-4', md: 'h-4 w-4',
lg: 'h-5 w-5' lg: 'h-5 w-5'

View File

@ -242,34 +242,7 @@ export default function VideoViewer({
{/* Title overlay */} {/* Title overlay */}
<div className={`absolute top-0 left-0 right-0 bg-gradient-to-b from-black/60 to-transparent p-4 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`}> <div className={`absolute top-0 left-0 right-0 bg-gradient-to-b from-black/60 to-transparent p-4 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`}>
<div className="flex items-center justify-between"> <h2 className="text-white text-lg font-semibold">{getVideoTitle()}</h2>
<h2 className="text-white text-lg font-semibold">{getVideoTitle()}</h2>
{(showBookmarks || showRatings) && (
<div className="flex items-center gap-4">
{showBookmarks && (
<button
onClick={isBookmarked ? handleUnbookmark : handleBookmark}
className="flex items-center gap-1 text-white hover:text-yellow-400 transition-colors"
>
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</button>
)}
{showRatings && (
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((rating) => (
<button
key={rating}
onClick={() => handleRate(rating)}
className="text-white hover:text-yellow-400 transition-colors"
>
<Star className={`h-4 w-4 ${rating <= Math.round(getAvgRating()) ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</button>
))}
</div>
)}
</div>
)}
</div>
</div> </div>
{/* Controls overlay */} {/* Controls overlay */}
@ -290,6 +263,41 @@ export default function VideoViewer({
</div> </div>
</div> </div>
{/* Video Info Bar (similar to photo viewer) */}
<div className="bg-black/70 backdrop-blur-sm rounded-lg p-4 mb-2">
<div className="flex items-center justify-between">
<div>
<h3 className="text-white font-medium">{getVideoTitle()}</h3>
<p className="text-gray-300 text-sm">{getVideoSize()}</p>
</div>
{(showBookmarks || showRatings) && (
<div className="flex items-center gap-4">
{showBookmarks && (
<button
onClick={isBookmarked ? handleUnbookmark : handleBookmark}
className="flex items-center gap-1 text-white hover:text-yellow-400 transition-colors"
>
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</button>
)}
{showRatings && (
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((rating) => (
<button
key={rating}
onClick={() => handleRate(rating)}
className="text-white hover:text-yellow-400 transition-colors"
>
<Star className={`h-4 w-4 ${rating <= Math.round(getAvgRating()) ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</button>
))}
</div>
)}
</div>
)}
</div>
</div>
{/* Control buttons */} {/* Control buttons */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">

View File

@ -80,11 +80,11 @@ export default function VirtualizedMediaGrid({
const directory = path.substring(0, lastSeparatorIndex); const directory = path.substring(0, lastSeparatorIndex);
const filename = path.substring(lastSeparatorIndex + 1); const filename = path.substring(lastSeparatorIndex + 1);
if (directory.length <= 30) { if (directory.length <= 40) {
return `${directory}/${filename}`; return `${directory}/${filename}`;
} }
const maxDirLength = 25; const maxDirLength = 35;
const startLength = Math.floor(maxDirLength / 2); const startLength = Math.floor(maxDirLength / 2);
const endLength = maxDirLength - startLength - 3; const endLength = maxDirLength - startLength - 3;
@ -231,13 +231,13 @@ export default function VirtualizedMediaGrid({
</div> </div>
</div> </div>
<CardContent className="p-3"> <CardContent className="p-2.5">
<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-1.5 group-hover:text-primary transition-colors">
{item.title || item.path.split('/').pop()} {item.title || item.path.split('/').pop()}
</h3> </h3>
{(item.avg_rating > 0 || item.star_count > 0) && ( {(item.avg_rating > 0 || item.star_count > 0) && (
<div className="mb-2"> <div className="mb-1.5">
<StarRating <StarRating
rating={item.avg_rating || 0} rating={item.avg_rating || 0}
count={item.star_count} count={item.star_count}