Compare commits
3 Commits
74980b5059
...
5fac68e809
| Author | SHA1 | Date |
|---|---|---|
|
|
5fac68e809 | |
|
|
8f652fdf08 | |
|
|
dd08765ab9 |
BIN
data/media.db
BIN
data/media.db
Binary file not shown.
|
|
@ -1,6 +1,30 @@
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getDatabase } from '@/db';
|
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 }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,30 @@
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getDatabase } from '@/db';
|
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 }> }) {
|
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
|
|
|
||||||
|
|
@ -215,12 +215,12 @@ const FolderViewerPage = () => {
|
||||||
// Handle rating operations
|
// Handle rating operations
|
||||||
const handleRate = async (mediaId: number, rating: number) => {
|
const handleRate = async (mediaId: number, rating: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/stars/${mediaId}`, {
|
const response = await fetch(`/api/stars`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ rating })
|
body: JSON.stringify({ mediaId, rating })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
|
||||||
|
|
@ -173,35 +173,7 @@ export default function ArtPlayerWrapper({
|
||||||
],
|
],
|
||||||
|
|
||||||
// Custom layer for bookmark and rating controls
|
// Custom layer for bookmark and rating controls
|
||||||
layers: showBookmarks || showRatings ? [
|
layers: [],
|
||||||
{
|
|
||||||
html: `<div class="artplayer-custom-controls absolute top-4 right-16 flex items-center gap-2 z-15">
|
|
||||||
${showBookmarks ? `<div class="artplayer-bookmark-control flex items-center gap-1 px-2 py-1 rounded hover:bg-black/50 transition-colors cursor-pointer text-white bg-black/30">
|
|
||||||
<svg class="h-4 w-4" fill="${localIsBookmarked ? 'currentColor' : 'none'}" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="text-xs">${localBookmarkCount}</span>
|
|
||||||
</div>` : ''}
|
|
||||||
${showRatings ? `<div class="artplayer-rating-control flex items-center gap-1 px-2 py-1 rounded hover:bg-black/50 transition-colors cursor-pointer text-white bg-black/30">
|
|
||||||
<span class="text-xs">★ ${localAvgRating.toFixed(1)}</span>
|
|
||||||
</div>` : ''}
|
|
||||||
</div>`,
|
|
||||||
style: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '16px',
|
|
||||||
right: '64px', // Positioned to the left of close button
|
|
||||||
zIndex: '15'
|
|
||||||
} as any,
|
|
||||||
click: function(this: any, component: any, event: Event) {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (target.closest('.artplayer-bookmark-control')) {
|
|
||||||
handleBookmarkToggle();
|
|
||||||
} else if (target.closest('.artplayer-rating-control')) {
|
|
||||||
handleRatingClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
] : [],
|
|
||||||
|
|
||||||
// Custom initialization for HLS
|
// Custom initialization for HLS
|
||||||
customType: {
|
customType: {
|
||||||
|
|
@ -388,49 +360,55 @@ export default function ArtPlayerWrapper({
|
||||||
setError(`Failed to initialize player: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
setError(`Failed to initialize player: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [useArtPlayer, isOpen, video, onProgress, volume, autoplay, format?.supportLevel, localIsBookmarked, localBookmarkCount, localAvgRating]);
|
}, [useArtPlayer, isOpen, video, onProgress, volume, autoplay, format?.supportLevel]);
|
||||||
|
|
||||||
// Handle bookmark toggle
|
// Handle bookmark toggle
|
||||||
const handleBookmarkToggle = useCallback(async () => {
|
const handleBookmarkToggle = useCallback(async () => {
|
||||||
if (!video.id) return;
|
if (!video.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (localIsBookmarked) {
|
setLocalIsBookmarked(prev => {
|
||||||
if (onUnbookmark) {
|
const newBookmarked = !prev;
|
||||||
onUnbookmark(video.id);
|
if (newBookmarked) {
|
||||||
}
|
|
||||||
setLocalIsBookmarked(false);
|
|
||||||
setLocalBookmarkCount(prev => Math.max(0, prev - 1));
|
|
||||||
} else {
|
|
||||||
if (onBookmark) {
|
if (onBookmark) {
|
||||||
onBookmark(video.id);
|
onBookmark(video.id);
|
||||||
}
|
}
|
||||||
setLocalIsBookmarked(true);
|
setLocalBookmarkCount(prevCount => prevCount + 1);
|
||||||
setLocalBookmarkCount(prev => prev + 1);
|
} else {
|
||||||
|
if (onUnbookmark) {
|
||||||
|
onUnbookmark(video.id);
|
||||||
}
|
}
|
||||||
|
setLocalBookmarkCount(prevCount => Math.max(0, prevCount - 1));
|
||||||
|
}
|
||||||
|
return newBookmarked;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling bookmark:', error);
|
console.error('Error toggling bookmark:', error);
|
||||||
}
|
}
|
||||||
}, [video.id, localIsBookmarked, onBookmark, onUnbookmark]);
|
}, [video.id, onBookmark, onUnbookmark]);
|
||||||
|
|
||||||
// Handle rating click
|
// Handle rating click
|
||||||
const handleRatingClick = useCallback(() => {
|
const handleRatingClick = useCallback(() => {
|
||||||
if (!video.id || !onRate) return;
|
if (!video.id || !onRate) return;
|
||||||
|
|
||||||
const currentRating = Math.round(localAvgRating);
|
setLocalAvgRating(prev => {
|
||||||
|
const currentRating = Math.round(prev);
|
||||||
const newRating = currentRating >= 5 ? 1 : currentRating + 1;
|
const newRating = currentRating >= 5 ? 1 : currentRating + 1;
|
||||||
|
|
||||||
onRate(video.id, newRating);
|
onRate(video.id, newRating);
|
||||||
setLocalAvgRating(newRating);
|
return newRating;
|
||||||
}, [video.id, localAvgRating, onRate]);
|
});
|
||||||
|
}, [video.id, onRate]);
|
||||||
|
|
||||||
// Handle individual star click for rating
|
// Handle individual star click for rating
|
||||||
const handleStarClick = useCallback((rating: number) => {
|
const handleStarClick = useCallback((rating: number) => {
|
||||||
if (onRate) {
|
if (onRate) {
|
||||||
onRate(video.id, rating);
|
const currentRating = Math.round(localAvgRating);
|
||||||
setLocalAvgRating(rating);
|
// If clicking the same star as current rating, cancel the rating
|
||||||
|
const newRating = currentRating === rating ? 0 : rating;
|
||||||
|
onRate(video.id, newRating);
|
||||||
|
setLocalAvgRating(newRating);
|
||||||
}
|
}
|
||||||
}, [onRate, video.id]);
|
}, [onRate, video.id, localAvgRating]);
|
||||||
|
|
||||||
// Format time display
|
// Format time display
|
||||||
const formatTime = (time: number) => {
|
const formatTime = (time: number) => {
|
||||||
|
|
@ -650,10 +628,9 @@ export default function ArtPlayerWrapper({
|
||||||
? 'bg-yellow-500/30 text-yellow-300 hover:bg-yellow-500/40'
|
? 'bg-yellow-500/30 text-yellow-300 hover:bg-yellow-500/40'
|
||||||
: 'bg-white/20 text-white hover:bg-white/30'
|
: '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`} />
|
<Bookmark className={`h-4 w-4 ${localIsBookmarked ? 'fill-current' : ''} drop-shadow-md`} />
|
||||||
<span className="text-sm font-medium drop-shadow-md">{localBookmarkCount}</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,60 @@ export default function UnifiedVideoPlayer({
|
||||||
}: UnifiedVideoPlayerProps) {
|
}: UnifiedVideoPlayerProps) {
|
||||||
const [format, setFormat] = useState<ReturnType<typeof detectVideoFormat> | null>(null);
|
const [format, setFormat] = useState<ReturnType<typeof detectVideoFormat> | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
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
|
// Detect format on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -80,13 +134,23 @@ export default function UnifiedVideoPlayer({
|
||||||
const handleBookmarkToggle = useCallback(async (videoId: number) => {
|
const handleBookmarkToggle = useCallback(async (videoId: number) => {
|
||||||
if (onBookmark) {
|
if (onBookmark) {
|
||||||
await onBookmark(videoId);
|
await onBookmark(videoId);
|
||||||
|
setIsBookmarked(true); // Update local state
|
||||||
}
|
}
|
||||||
}, [onBookmark]);
|
}, [onBookmark]);
|
||||||
|
|
||||||
|
// Handle unbookmark
|
||||||
|
const handleUnbookmark = useCallback(async (videoId: number) => {
|
||||||
|
if (onUnbookmark) {
|
||||||
|
await onUnbookmark(videoId);
|
||||||
|
setIsBookmarked(false); // Update local state
|
||||||
|
}
|
||||||
|
}, [onUnbookmark]);
|
||||||
|
|
||||||
// Handle rating
|
// Handle rating
|
||||||
const handleRatingUpdate = useCallback(async (videoId: number, rating: number) => {
|
const handleRatingUpdate = useCallback(async (videoId: number, rating: number) => {
|
||||||
if (onRate) {
|
if (onRate) {
|
||||||
await onRate(videoId, rating);
|
await onRate(videoId, rating);
|
||||||
|
setCurrentRating(rating); // Update local state
|
||||||
}
|
}
|
||||||
}, [onRate]);
|
}, [onRate]);
|
||||||
|
|
||||||
|
|
@ -100,13 +164,13 @@ export default function UnifiedVideoPlayer({
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onProgress={handleProgressUpdate}
|
onProgress={handleProgressUpdate}
|
||||||
onBookmark={handleBookmarkToggle}
|
onBookmark={handleBookmarkToggle}
|
||||||
onUnbookmark={onUnbookmark}
|
onUnbookmark={handleUnbookmark}
|
||||||
onRate={handleRatingUpdate}
|
onRate={handleRatingUpdate}
|
||||||
onError={handleArtPlayerError}
|
onError={handleArtPlayerError}
|
||||||
useArtPlayer={true}
|
useArtPlayer={true}
|
||||||
isBookmarked={(video.bookmark_count || 0) > 0}
|
isBookmarked={isBookmarked}
|
||||||
bookmarkCount={video.bookmark_count || 0}
|
bookmarkCount={video.bookmark_count || 0}
|
||||||
avgRating={video.avg_rating || 0}
|
avgRating={currentRating}
|
||||||
showBookmarks={showBookmarks}
|
showBookmarks={showBookmarks}
|
||||||
showRatings={showRatings}
|
showRatings={showRatings}
|
||||||
autoplay={autoplay}
|
autoplay={autoplay}
|
||||||
|
|
@ -114,7 +178,7 @@ export default function UnifiedVideoPlayer({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || bookmarkCheckLoading || ratingCheckLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center">
|
||||||
<div className="text-white text-center">
|
<div className="text-white text-center">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue