feat(api): add GET endpoints for bookmarks and stars by media ID
- Implement GET /api/bookmarks/[id] to check bookmark existence and return bookmark ID
- Implement GET /api/stars/[id] to check star rating existence and return rating value
- Validate media ID as integer and handle invalid ID with 400 status
- Handle server errors with 500 status and error messages
refactor(api): change rating POST request to include mediaId in body
- Update /api/stars POST request to send mediaId and rating in JSON body
- Adjust client fetch call accordingly from /api/stars/[id] to /api/stars
feat(player): integrate bookmark and rating status checks in video player
- Add state and loading indicators for bookmark and rating status in UnifiedVideoPlayer
- Fetch current bookmark and rating status on video open using new GET API endpoints
- Update local bookmark and rating state on user actions for bookmark add/remove and rating update
- Show loading state until both bookmark and rating statuses are fetched
fix(ui): improve bookmark button tooltip and state handling in ArtPlayerWrapper
- Change bookmark button title dynamically based on bookmark state ('Add bookmark' or 'Remove bookmark')
- Remove redundant bookmark count element in button for cleaner UI
This commit is contained in:
parent
8f652fdf08
commit
5fac68e809
BIN
data/media.db
BIN
data/media.db
Binary file not shown.
|
|
@ -1,6 +1,30 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '@/db';
|
||||
|
||||
export async function GET(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 });
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
// Check if bookmark exists
|
||||
const bookmark = db.prepare(`
|
||||
SELECT id FROM bookmarks WHERE media_id = ?
|
||||
`).get(mediaId) as { id: number } | undefined;
|
||||
|
||||
return NextResponse.json({
|
||||
isBookmarked: !!bookmark,
|
||||
bookmarkId: bookmark?.id || null
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,30 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '@/db';
|
||||
|
||||
export async function GET(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 });
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
// Check if rating exists for this media
|
||||
const star = db.prepare(`
|
||||
SELECT rating FROM stars WHERE media_id = ?
|
||||
`).get(mediaId) as { rating: number } | undefined;
|
||||
|
||||
return NextResponse.json({
|
||||
hasRating: !!star,
|
||||
rating: star?.rating || 0
|
||||
});
|
||||
} 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;
|
||||
const db = getDatabase();
|
||||
|
|
|
|||
|
|
@ -215,12 +215,12 @@ const FolderViewerPage = () => {
|
|||
// Handle rating operations
|
||||
const handleRate = async (mediaId: number, rating: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/stars/${mediaId}`, {
|
||||
const response = await fetch(`/api/stars`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ rating })
|
||||
body: JSON.stringify({ mediaId, rating })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
|
|
|||
|
|
@ -628,10 +628,9 @@ export default function ArtPlayerWrapper({
|
|||
? 'bg-yellow-500/30 text-yellow-300 hover:bg-yellow-500/40'
|
||||
: 'bg-white/20 text-white hover:bg-white/30'
|
||||
}`}
|
||||
title="Toggle bookmark"
|
||||
title={localIsBookmarked ? 'Remove bookmark' : 'Add bookmark'}
|
||||
>
|
||||
<Bookmark className={`h-4 w-4 ${localIsBookmarked ? 'fill-current' : ''} drop-shadow-md`} />
|
||||
<span className="text-sm font-medium drop-shadow-md">{localBookmarkCount}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,60 @@ export default function UnifiedVideoPlayer({
|
|||
}: UnifiedVideoPlayerProps) {
|
||||
const [format, setFormat] = useState<ReturnType<typeof detectVideoFormat> | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
const [bookmarkCheckLoading, setBookmarkCheckLoading] = useState(true);
|
||||
const [currentRating, setCurrentRating] = useState(0);
|
||||
const [ratingCheckLoading, setRatingCheckLoading] = useState(true);
|
||||
|
||||
// Check current bookmark status and rating when video opens
|
||||
useEffect(() => {
|
||||
if (isOpen && video.id) {
|
||||
checkBookmarkStatus();
|
||||
checkRatingStatus();
|
||||
}
|
||||
}, [isOpen, video.id]);
|
||||
|
||||
const checkBookmarkStatus = async () => {
|
||||
if (!video.id) return;
|
||||
|
||||
setBookmarkCheckLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/bookmarks/${video.id}`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setIsBookmarked(data.isBookmarked || false);
|
||||
} else {
|
||||
setIsBookmarked(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking bookmark status:', error);
|
||||
setIsBookmarked(false);
|
||||
} finally {
|
||||
setBookmarkCheckLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkRatingStatus = async () => {
|
||||
if (!video.id) return;
|
||||
|
||||
setRatingCheckLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/stars/${video.id}`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentRating(data.rating || 0);
|
||||
} else {
|
||||
setCurrentRating(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking rating status:', error);
|
||||
setCurrentRating(0);
|
||||
} finally {
|
||||
setRatingCheckLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Detect format on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -80,13 +134,23 @@ export default function UnifiedVideoPlayer({
|
|||
const handleBookmarkToggle = useCallback(async (videoId: number) => {
|
||||
if (onBookmark) {
|
||||
await onBookmark(videoId);
|
||||
setIsBookmarked(true); // Update local state
|
||||
}
|
||||
}, [onBookmark]);
|
||||
|
||||
// Handle unbookmark
|
||||
const handleUnbookmark = useCallback(async (videoId: number) => {
|
||||
if (onUnbookmark) {
|
||||
await onUnbookmark(videoId);
|
||||
setIsBookmarked(false); // Update local state
|
||||
}
|
||||
}, [onUnbookmark]);
|
||||
|
||||
// Handle rating
|
||||
const handleRatingUpdate = useCallback(async (videoId: number, rating: number) => {
|
||||
if (onRate) {
|
||||
await onRate(videoId, rating);
|
||||
setCurrentRating(rating); // Update local state
|
||||
}
|
||||
}, [onRate]);
|
||||
|
||||
|
|
@ -100,13 +164,13 @@ export default function UnifiedVideoPlayer({
|
|||
onClose={onClose}
|
||||
onProgress={handleProgressUpdate}
|
||||
onBookmark={handleBookmarkToggle}
|
||||
onUnbookmark={onUnbookmark}
|
||||
onUnbookmark={handleUnbookmark}
|
||||
onRate={handleRatingUpdate}
|
||||
onError={handleArtPlayerError}
|
||||
useArtPlayer={true}
|
||||
isBookmarked={(video.bookmark_count || 0) > 0}
|
||||
isBookmarked={isBookmarked}
|
||||
bookmarkCount={video.bookmark_count || 0}
|
||||
avgRating={video.avg_rating || 0}
|
||||
avgRating={currentRating}
|
||||
showBookmarks={showBookmarks}
|
||||
showRatings={showRatings}
|
||||
autoplay={autoplay}
|
||||
|
|
@ -114,7 +178,7 @@ export default function UnifiedVideoPlayer({
|
|||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || bookmarkCheckLoading || ratingCheckLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center">
|
||||
<div className="text-white text-center">
|
||||
|
|
|
|||
Loading…
Reference in New Issue