Compare commits
No commits in common. "89bb05d3fcad5be7083ad05d31b3c958d17a1ae7" and "95a49380da093e25d9a1e19d51fbe090fc627574" have entirely different histories.
89bb05d3fc
...
95a49380da
|
|
@ -12,8 +12,6 @@ Feature requirement:
|
|||
7. there should be a database(sqlite) to save the media informations
|
||||
8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library
|
||||
9. thumbnail generate feature
|
||||
10. bookmark feature: the video can be bookmarked
|
||||
11. star feature: the video can be stared as one to five stars
|
||||
|
||||
|
||||
UI:
|
||||
|
|
@ -26,6 +24,5 @@ UI:
|
|||
6. photo section: TBD
|
||||
7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in
|
||||
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
|
||||
9. can bookmark/un-bookmark the video, can star the video
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ Feature requirement:
|
|||
7. there should be a database(sqlite) to save the media informations
|
||||
8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library
|
||||
9. thumbnail generate feature
|
||||
10. bookmark feature: the video can be bookmarked
|
||||
11. star feature: the video can be stared as one to five stars
|
||||
|
||||
|
||||
UI:
|
||||
|
|
@ -26,6 +24,5 @@ UI:
|
|||
6. photo section: TBD
|
||||
7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in
|
||||
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
|
||||
9. can bookmark/un-bookmark the video, can star the video
|
||||
|
||||
|
||||
|
|
|
|||
3
PRD.md
3
PRD.md
|
|
@ -12,8 +12,6 @@ Feature requirement:
|
|||
7. there should be a database(sqlite) to save the media informations
|
||||
8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library
|
||||
9. thumbnail generate feature
|
||||
10. bookmark feature: the video can be bookmarked
|
||||
11. star feature: the video can be stared as one to five stars
|
||||
|
||||
|
||||
UI:
|
||||
|
|
@ -26,6 +24,5 @@ UI:
|
|||
6. photo section: TBD
|
||||
7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in
|
||||
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
|
||||
9. can bookmark/un-bookmark the video, can star the video
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import db from '@/db';
|
||||
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const parsedId = parseInt(id);
|
||||
|
||||
if (isNaN(parsedId)) {
|
||||
return NextResponse.json({ error: 'Invalid bookmark ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get media_id before deleting
|
||||
const bookmark = db.prepare(`
|
||||
SELECT media_id FROM bookmarks WHERE id = ?
|
||||
`).get(parsedId) as { media_id: number } | undefined;
|
||||
|
||||
if (!bookmark) {
|
||||
return NextResponse.json({ error: 'Bookmark not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete bookmark
|
||||
db.prepare(`DELETE FROM bookmarks WHERE id = ?`).run(parsedId);
|
||||
|
||||
// Update media bookmark count
|
||||
db.prepare(`
|
||||
UPDATE media
|
||||
SET bookmark_count = (SELECT COUNT(*) FROM bookmarks WHERE media_id = ?)
|
||||
WHERE id = ?
|
||||
`).run(bookmark.media_id, bookmark.media_id);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import db from '@/db';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const bookmarks = db.prepare(`
|
||||
SELECT m.*, l.path as library_path
|
||||
FROM bookmarks b
|
||||
JOIN media m ON b.media_id = m.id
|
||||
JOIN libraries l ON m.library_id = l.id
|
||||
ORDER BY b.updated_at DESC
|
||||
`).all();
|
||||
|
||||
return NextResponse.json(bookmarks);
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mediaId } = await request.json();
|
||||
|
||||
if (!mediaId) {
|
||||
return NextResponse.json({ error: 'mediaId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if already bookmarked
|
||||
const existing = db.prepare(`
|
||||
SELECT id FROM bookmarks WHERE media_id = ?
|
||||
`).get(mediaId);
|
||||
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: 'Already bookmarked' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Insert bookmark
|
||||
const result = db.prepare(`
|
||||
INSERT INTO bookmarks (media_id) VALUES (?)
|
||||
`).run(mediaId);
|
||||
|
||||
// Update media bookmark count
|
||||
db.prepare(`
|
||||
UPDATE media
|
||||
SET bookmark_count = (SELECT COUNT(*) FROM bookmarks WHERE media_id = ?)
|
||||
WHERE id = ?
|
||||
`).run(mediaId, mediaId);
|
||||
|
||||
return NextResponse.json({ id: result.lastInsertRowid });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import db from '@/db';
|
||||
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const parsedId = parseInt(id);
|
||||
|
||||
if (isNaN(parsedId)) {
|
||||
return NextResponse.json({ error: 'Invalid star ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get media_id before deleting
|
||||
const star = db.prepare(`
|
||||
SELECT media_id FROM stars WHERE id = ?
|
||||
`).get(parsedId) as { media_id: number } | undefined;
|
||||
|
||||
if (!star) {
|
||||
return NextResponse.json({ error: 'Star rating not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete star rating
|
||||
db.prepare(`DELETE FROM stars WHERE id = ?`).run(parsedId);
|
||||
|
||||
// Update media star count and average rating
|
||||
db.prepare(`
|
||||
UPDATE media
|
||||
SET
|
||||
star_count = (SELECT COUNT(*) FROM stars WHERE media_id = ?),
|
||||
avg_rating = COALESCE((SELECT AVG(rating) FROM stars WHERE media_id = ?), 0.0)
|
||||
WHERE id = ?
|
||||
`).run(star.media_id, star.media_id, star.media_id);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import db from '@/db';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mediaId = searchParams.get('mediaId');
|
||||
|
||||
if (mediaId) {
|
||||
// Get stars for specific media
|
||||
const stars = db.prepare(`
|
||||
SELECT * FROM stars WHERE media_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`).all(parseInt(mediaId));
|
||||
|
||||
return NextResponse.json(stars);
|
||||
} else {
|
||||
// Get all stars
|
||||
const stars = db.prepare(`
|
||||
SELECT m.*, l.path as library_path, s.rating
|
||||
FROM stars s
|
||||
JOIN media m ON s.media_id = m.id
|
||||
JOIN libraries l ON m.library_id = l.id
|
||||
ORDER BY s.created_at DESC
|
||||
`).all();
|
||||
|
||||
return NextResponse.json(stars);
|
||||
}
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mediaId, rating } = await request.json();
|
||||
|
||||
if (!mediaId || !rating) {
|
||||
return NextResponse.json({ error: 'mediaId and rating are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (rating < 1 || rating > 5) {
|
||||
return NextResponse.json({ error: 'Rating must be between 1 and 5' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if already starred
|
||||
const existing = db.prepare(`
|
||||
SELECT id FROM stars WHERE media_id = ?
|
||||
`).get(mediaId);
|
||||
|
||||
if (existing) {
|
||||
// Update existing star rating
|
||||
db.prepare(`
|
||||
UPDATE stars
|
||||
SET rating = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE media_id = ?
|
||||
`).run(rating, mediaId);
|
||||
} else {
|
||||
// Insert new star rating
|
||||
db.prepare(`
|
||||
INSERT INTO stars (media_id, rating) VALUES (?, ?)
|
||||
`).run(mediaId, rating);
|
||||
}
|
||||
|
||||
// Update media star count and average rating
|
||||
db.prepare(`
|
||||
UPDATE media
|
||||
SET
|
||||
star_count = (SELECT COUNT(*) FROM stars WHERE media_id = ?),
|
||||
avg_rating = (SELECT AVG(rating) FROM stars WHERE media_id = ?)
|
||||
WHERE id = ?
|
||||
`).run(mediaId, mediaId, mediaId);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import db from '@/db';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const parsedId = parseInt(id);
|
||||
|
||||
if (isNaN(parsedId)) {
|
||||
return NextResponse.json({ error: 'Invalid video ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const video = db.prepare(`
|
||||
SELECT m.*, l.path as library_path
|
||||
FROM media m
|
||||
JOIN libraries l ON m.library_id = l.id
|
||||
WHERE m.id = ?
|
||||
`).get(parsedId);
|
||||
|
||||
if (!video) {
|
||||
return NextResponse.json({ error: 'Video not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(video);
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import InlineVideoPlayer from "@/components/inline-video-player";
|
||||
import { Bookmark, Heart, Star, Film } from 'lucide-react';
|
||||
|
||||
interface Video {
|
||||
id: number;
|
||||
title: string;
|
||||
path: string;
|
||||
size: number;
|
||||
thumbnail: string;
|
||||
type: string;
|
||||
bookmark_count: number;
|
||||
star_count: number;
|
||||
avg_rating: number;
|
||||
library_path: string;
|
||||
}
|
||||
|
||||
export default function BookmarksPage() {
|
||||
const [videos, setVideos] = useState<Video[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
|
||||
const [isPlayerOpen, setIsPlayerOpen] = useState(false);
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBookmarkedVideos();
|
||||
}, []);
|
||||
|
||||
const fetchBookmarkedVideos = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/bookmarks');
|
||||
const data = await response.json();
|
||||
setVideos(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching bookmarked videos:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const handleVideoClick = (video: Video) => {
|
||||
setScrollPosition(window.scrollY);
|
||||
setSelectedVideo(video);
|
||||
setIsPlayerOpen(true);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleClosePlayer = () => {
|
||||
setIsPlayerOpen(false);
|
||||
setSelectedVideo(null);
|
||||
// Restore scroll position
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: scrollPosition, behavior: 'smooth' });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Loading bookmarked videos...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<Bookmark className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Bookmarked Videos</h1>
|
||||
<p className="text-zinc-400 mt-1">
|
||||
{videos.length} {videos.length === 1 ? 'video' : 'videos'} bookmarked
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{videos.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="w-20 h-20 bg-zinc-900 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<Bookmark className="h-10 w-10 text-zinc-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">No Bookmarks Yet</h2>
|
||||
<p className="text-zinc-400">
|
||||
Start bookmarking videos by clicking the bookmark icon in the video player.
|
||||
</p>
|
||||
</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">
|
||||
{videos.map((video) => (
|
||||
<Card
|
||||
key={video.id}
|
||||
className="group bg-white dark:bg-slate-800 border-0 shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 overflow-hidden cursor-pointer"
|
||||
onClick={() => handleVideoClick(video)}
|
||||
>
|
||||
<CardHeader className="p-0">
|
||||
<div className="aspect-video bg-slate-200 dark:bg-slate-700 relative overflow-hidden">
|
||||
{video.thumbnail ? (
|
||||
<img
|
||||
src={video.thumbnail}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-slate-200 to-slate-300 dark:from-slate-700 dark:to-slate-800">
|
||||
<Film className="w-12 h-12 text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="p-3">
|
||||
<CardTitle className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">
|
||||
{video.title || video.path.split('/').pop()}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{formatFileSize(video.size)}
|
||||
</CardDescription>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<Heart className="h-3 w-3" />
|
||||
<span>{video.bookmark_count || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-3 w-3" />
|
||||
<span>{video.avg_rating?.toFixed(1) || '0.0'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1 truncate">
|
||||
{video.library_path?.split('/').pop()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Inline Video Player */}
|
||||
{selectedVideo && (
|
||||
<InlineVideoPlayer
|
||||
video={{
|
||||
id: selectedVideo.id,
|
||||
title: selectedVideo.title || selectedVideo.path.split('/').pop() || 'Untitled',
|
||||
path: selectedVideo.path,
|
||||
size: selectedVideo.size,
|
||||
thumbnail: selectedVideo.thumbnail || '',
|
||||
}}
|
||||
isOpen={isPlayerOpen}
|
||||
onClose={handleClosePlayer}
|
||||
scrollPosition={scrollPosition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { X, Play, Pause, Maximize, Minimize, Volume2, VolumeX, Bookmark, Star, Heart } from 'lucide-react';
|
||||
import { X, Play, Pause, Maximize, Minimize, Volume2, VolumeX } from 'lucide-react';
|
||||
|
||||
interface InlineVideoPlayerProps {
|
||||
video: {
|
||||
|
|
@ -25,19 +25,11 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
|
|||
const [duration, setDuration] = useState(0);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
const [userRating, setUserRating] = useState(0);
|
||||
const [avgRating, setAvgRating] = useState(0);
|
||||
const [bookmarkCount, setBookmarkCount] = useState(0);
|
||||
const [starCount, setStarCount] = useState(0);
|
||||
const [showRating, setShowRating] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
loadBookmarkStatus();
|
||||
loadStarRating();
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
|
|
@ -117,82 +109,6 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
|
|||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const loadBookmarkStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks?mediaId=${video.id}`);
|
||||
const data = await response.json();
|
||||
setIsBookmarked(data.length > 0);
|
||||
} catch (error) {
|
||||
console.error('Error loading bookmark status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadStarRating = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/stars?mediaId=${video.id}`);
|
||||
const stars = await response.json();
|
||||
|
||||
// Get media info for counts and avg rating
|
||||
const mediaResponse = await fetch(`/api/videos/${video.id}`);
|
||||
const mediaData = await mediaResponse.json();
|
||||
|
||||
setBookmarkCount(mediaData.bookmark_count || 0);
|
||||
setStarCount(mediaData.star_count || 0);
|
||||
setAvgRating(mediaData.avg_rating || 0);
|
||||
|
||||
// Set user's rating if exists
|
||||
if (stars.length > 0) {
|
||||
setUserRating(stars[0].rating);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading star rating:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleBookmark = async () => {
|
||||
try {
|
||||
if (isBookmarked) {
|
||||
// Remove bookmark
|
||||
const response = await fetch(`/api/bookmarks/${video.id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
setIsBookmarked(false);
|
||||
setBookmarkCount(prev => Math.max(0, prev - 1));
|
||||
}
|
||||
} else {
|
||||
// Add bookmark
|
||||
const response = await fetch('/api/bookmarks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mediaId: video.id })
|
||||
});
|
||||
if (response.ok) {
|
||||
setIsBookmarked(true);
|
||||
setBookmarkCount(prev => prev + 1);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling bookmark:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStarClick = async (rating: number) => {
|
||||
try {
|
||||
const response = await fetch('/api/stars', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mediaId: video.id, rating })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setUserRating(rating);
|
||||
// Reload star data
|
||||
await loadStarRating();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting star rating:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
|
@ -237,42 +153,6 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
|
|||
<h1 className="text-xl font-semibold text-foreground truncate max-w-md">
|
||||
{video.title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Bookmark Button */}
|
||||
<button
|
||||
onClick={toggleBookmark}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition-colors ${
|
||||
isBookmarked
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-muted hover:bg-muted/80'
|
||||
}`}
|
||||
>
|
||||
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-current' : ''}`} />
|
||||
<span className="text-sm">{bookmarkCount}</span>
|
||||
</button>
|
||||
|
||||
{/* Star Rating */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
onClick={() => handleStarClick(star)}
|
||||
className="text-yellow-500 hover:text-yellow-400 transition-colors"
|
||||
>
|
||||
<Star
|
||||
className={`h-4 w-4 ${
|
||||
star <= userRating ? 'fill-current' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{avgRating.toFixed(1)} ({starCount})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
Image as ImageIcon,
|
||||
Play,
|
||||
FolderOpen,
|
||||
Bookmark,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
|
@ -50,7 +49,6 @@ const SidebarContent = () => {
|
|||
{ href: "/", label: "Home", icon: Home },
|
||||
{ href: "/videos", label: "Videos", icon: Film },
|
||||
{ href: "/photos", label: "Photos", icon: ImageIcon },
|
||||
{ href: "/bookmarks", label: "Bookmarks", icon: Bookmark },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -21,39 +21,9 @@ db.exec(`
|
|||
title TEXT,
|
||||
size INTEGER,
|
||||
thumbnail TEXT,
|
||||
bookmark_count INTEGER DEFAULT 0,
|
||||
star_count INTEGER DEFAULT 0,
|
||||
avg_rating REAL DEFAULT 0.0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (library_id) REFERENCES libraries (id)
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS bookmarks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
media_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS stars (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
media_id INTEGER NOT NULL,
|
||||
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create indexes for performance
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_media_id ON bookmarks(media_id);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_stars_media_id ON stars(media_id);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_bookmark_count ON media(bookmark_count);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_star_count ON media(star_count);`);
|
||||
|
||||
export default db;
|
||||
|
|
|
|||
Loading…
Reference in New Issue