Compare commits

...

2 Commits

Author SHA1 Message Date
tigeren 224b898bcd feat: implement bookmarking and unbookmarking functionality for videos
- Added POST and DELETE endpoints for managing bookmarks in the video API.
- Enhanced the VideosPage component to handle bookmarking and unbookmarking actions.
- Updated the InlineVideoPlayer and VideoViewer components to reflect bookmark state and count.
- Improved error handling for invalid media IDs and existing bookmarks.
2025-08-26 18:42:09 +00:00
tigeren 933d12dd14 feat: enhance PhotoViewer with keyboard navigation and ID retrieval
- Implemented keyboard navigation for the PhotoViewer, allowing users to close the viewer with 'Escape' and navigate through photos using 'ArrowLeft' and 'ArrowRight' keys.
- Added a utility function to safely retrieve the photo ID, ensuring it is defined before use in various functionalities like bookmarking and rating.
2025-08-26 18:17:36 +00:00
6 changed files with 168 additions and 33 deletions

BIN
media.db

Binary file not shown.

View File

@ -1,33 +1,78 @@
import { NextResponse } from 'next/server';
import db from '@/db';
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const parsedId = parseInt(id);
const mediaId = parseInt(id);
if (isNaN(parsedId)) {
return NextResponse.json({ error: 'Invalid bookmark ID' }, { status: 400 });
if (isNaN(mediaId)) {
return NextResponse.json({ error: 'Invalid media 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;
// Check if media exists
const media = db.prepare(`
SELECT id FROM media WHERE id = ?
`).get(mediaId) as { id: number } | undefined;
if (!bookmark) {
return NextResponse.json({ error: 'Bookmark not found' }, { status: 404 });
if (!media) {
return NextResponse.json({ error: 'Media not found' }, { status: 404 });
}
// Delete bookmark
db.prepare(`DELETE FROM bookmarks WHERE id = ?`).run(parsedId);
// 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(bookmark.media_id, bookmark.media_id);
`).run(mediaId, mediaId);
return NextResponse.json({ id: result.lastInsertRowid });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const mediaId = parseInt(id);
if (isNaN(mediaId)) {
return NextResponse.json({ error: 'Invalid media ID' }, { status: 400 });
}
// Check if bookmark exists
const bookmark = db.prepare(`
SELECT id FROM bookmarks WHERE media_id = ?
`).get(mediaId) as { id: number } | undefined;
if (!bookmark) {
return NextResponse.json({ error: 'Bookmark not found' }, { status: 404 });
}
// Delete bookmark
db.prepare(`DELETE FROM bookmarks WHERE media_id = ?`).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({ success: true });
} catch (error: any) {

View File

@ -67,6 +67,37 @@ const VideosPage = () => {
setSelectedVideo(null);
};
const handleBookmark = async (videoId: number) => {
try {
await fetch(`/api/bookmarks/${videoId}`, { method: 'POST' });
fetchVideos();
} catch (error) {
console.error('Error bookmarking video:', error);
}
};
const handleUnbookmark = async (videoId: number) => {
try {
await fetch(`/api/bookmarks/${videoId}`, { method: 'DELETE' });
fetchVideos();
} catch (error) {
console.error('Error unbookmarking video:', error);
}
};
const handleRate = async (videoId: number, rating: number) => {
try {
await fetch(`/api/stars/${videoId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rating })
});
fetchVideos();
} catch (error) {
console.error('Error rating video:', error);
}
};
if (loading) {
return (
<div className="min-h-screen p-6">
@ -218,6 +249,9 @@ const VideosPage = () => {
showBookmarks={true}
showRatings={true}
formatFileSize={formatFileSize}
onBookmark={handleBookmark}
onUnbookmark={handleUnbookmark}
onRate={handleRate}
/>
</>
);

View File

@ -160,11 +160,7 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
}
} else {
// Add bookmark
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: video.id })
});
const response = await fetch(`/api/bookmarks/${video.id}`, { method: 'POST' });
if (response.ok) {
setIsBookmarked(true);
setBookmarkCount(prev => prev + 1);

View File

@ -64,6 +64,39 @@ export default function PhotoViewer({
}
}, [isOpen, photo]);
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!isOpen) return;
switch (event.key) {
case 'Escape':
handleClose();
break;
case 'ArrowLeft':
if (onPrev) {
event.preventDefault();
handlePrev();
}
break;
case 'ArrowRight':
if (onNext) {
event.preventDefault();
handleNext();
}
break;
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onNext, onPrev]);
const handleClose = () => {
setIsPhotoLoading(false);
onClose();
@ -84,19 +117,19 @@ export default function PhotoViewer({
};
const handleBookmark = () => {
if (onBookmark && 'id' in photo) {
if (onBookmark && 'id' in photo && photo.id !== undefined) {
onBookmark(photo.id);
}
};
const handleUnbookmark = () => {
if (onUnbookmark && 'id' in photo) {
if (onUnbookmark && 'id' in photo && photo.id !== undefined) {
onUnbookmark(photo.id);
}
};
const handleRate = (rating: number) => {
if (onRate && 'id' in photo) {
if (onRate && 'id' in photo && photo.id !== undefined) {
onRate(photo.id, rating);
}
};
@ -130,6 +163,11 @@ export default function PhotoViewer({
return 0;
};
const getPhotoId = () => {
if ('id' in photo && photo.id !== undefined) return photo.id;
return 0;
};
if (!isOpen || typeof window === 'undefined') return null;
return createPortal(
@ -175,7 +213,7 @@ export default function PhotoViewer({
)}
<img
src={`/api/photos/${('id' in photo ? photo.id : photo.id) || ''}`}
src={`/api/photos/${getPhotoId()}`}
alt={getPhotoTitle()}
className={`max-w-full max-h-[90vh] w-auto h-auto object-contain rounded-lg ${isPhotoLoading ? 'hidden' : ''}`}
onLoad={() => setIsPhotoLoading(false)}

View File

@ -56,10 +56,20 @@ export default function VideoViewer({
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [showControls, setShowControls] = useState(true);
const [isBookmarked, setIsBookmarked] = useState(false);
const [bookmarkCount, setBookmarkCount] = useState(0);
const videoRef = useRef<HTMLVideoElement>(null);
// Update local bookmark state when video changes
useEffect(() => {
if (isOpen && videoRef.current) {
if (video && 'bookmark_count' in video) {
setIsBookmarked(video.bookmark_count > 0);
setBookmarkCount(video.bookmark_count);
}
}, [video]);
useEffect(() => {
if (isOpen && videoRef.current && video) {
videoRef.current.src = `/api/stream/${('id' in video ? video.id : video.id) || ''}`;
videoRef.current.load();
@ -133,30 +143,37 @@ export default function VideoViewer({
};
const handleBookmark = () => {
if (onBookmark && 'id' in video) {
if (onBookmark && video && 'id' in video && video.id !== undefined) {
onBookmark(video.id);
// Update local state immediately
setIsBookmarked(true);
setBookmarkCount(prev => prev + 1);
}
};
const handleUnbookmark = () => {
if (onUnbookmark && 'id' in video) {
if (onUnbookmark && video && 'id' in video && video.id !== undefined) {
onUnbookmark(video.id);
// Update local state immediately
setIsBookmarked(false);
setBookmarkCount(prev => Math.max(0, prev - 1));
}
};
const handleRate = (rating: number) => {
if (onRate && 'id' in video) {
if (onRate && video && 'id' in video && video.id !== undefined) {
onRate(video.id, rating);
}
};
const getVideoTitle = () => {
if ('title' in video) return video.title;
if ('name' in video) return video.name;
if (video && 'title' in video) return video.title;
if (video && 'name' in video) return video.name;
return 'Video';
};
const getVideoSize = () => {
if (!video) return '0 Bytes';
if (formatFileSize) {
return formatFileSize(video.size);
}
@ -170,12 +187,17 @@ export default function VideoViewer({
};
const getBookmarkCount = () => {
if ('bookmark_count' in video) return video.bookmark_count;
if (video && 'bookmark_count' in video) return video.bookmark_count;
return 0;
};
const getAvgRating = () => {
if ('avg_rating' in video) return video.avg_rating;
if (video && 'avg_rating' in video) return video.avg_rating;
return 0;
};
const getVideoId = () => {
if (video && 'id' in video && video.id !== undefined) return video.id;
return 0;
};
@ -214,7 +236,7 @@ export default function VideoViewer({
onMouseMove={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
<source src={`/api/stream/${('id' in video ? video.id : video.id) || ''}`} type="video/mp4" />
<source src={`/api/stream/${getVideoId()}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
@ -226,10 +248,10 @@ export default function VideoViewer({
<div className="flex items-center gap-4">
{showBookmarks && (
<button
onClick={getBookmarkCount() > 0 ? handleUnbookmark : handleBookmark}
onClick={isBookmarked ? handleUnbookmark : handleBookmark}
className="flex items-center gap-1 text-white hover:text-yellow-400 transition-colors"
>
<Bookmark className={`h-4 w-4 ${getBookmarkCount() > 0 ? 'fill-yellow-400 text-yellow-400' : ''}`} />
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</button>
)}
{showRatings && (