diff --git a/.replit b/.replit index c25f461..adcfadc 100644 --- a/.replit +++ b/.replit @@ -4,7 +4,6 @@ hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] [nix] channel = "stable-24_05" -packages = ["ffmpeg", "imagemagick"] [deployment] deploymentTarget = "autoscale" @@ -38,6 +37,3 @@ author = "agent" task = "shell.exec" args = "npm run dev" waitForPort = 5000 - -[agent] -integrations = ["javascript_database==1.0.0", "javascript_log_in_with_replit==1.0.0"] diff --git a/attached_assets/IMG_0270_1754336005971.png b/attached_assets/IMG_0270_1754336005971.png deleted file mode 100644 index c43db88..0000000 Binary files a/attached_assets/IMG_0270_1754336005971.png and /dev/null differ diff --git a/attached_assets/IMG_0271_1754336220862.png b/attached_assets/IMG_0271_1754336220862.png deleted file mode 100644 index 2f78289..0000000 Binary files a/attached_assets/IMG_0271_1754336220862.png and /dev/null differ diff --git a/attached_assets/IMG_0272_1754336435402.png b/attached_assets/IMG_0272_1754336435402.png deleted file mode 100644 index 08ec672..0000000 Binary files a/attached_assets/IMG_0272_1754336435402.png and /dev/null differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 47f094e..57750f6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,8 +5,6 @@ import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; import Home from "@/pages/home"; import VideoPage from "@/pages/video"; -import { PlaylistsPage } from "@/pages/playlists"; -import { FavoritesPage } from "@/pages/favorites"; import NotFound from "@/pages/not-found"; function Router() { @@ -14,8 +12,6 @@ function Router() { - - ); diff --git a/client/src/components/navigation.tsx b/client/src/components/navigation.tsx deleted file mode 100644 index 911707e..0000000 --- a/client/src/components/navigation.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Link, useLocation } from "wouter"; -import { Button } from "@/components/ui/button"; -import { Home, List, Heart, User } from "lucide-react"; - -export function Navigation() { - const [location] = useLocation(); - - const navItems = [ - { path: "/", label: "Home", icon: Home }, - { path: "/playlists", label: "Playlists", icon: List }, - { path: "/favorites", label: "Favorites", icon: Heart }, - ]; - - return ( - - ); -} \ No newline at end of file diff --git a/client/src/components/thumbnail-generator.tsx b/client/src/components/thumbnail-generator.tsx deleted file mode 100644 index b554cec..0000000 --- a/client/src/components/thumbnail-generator.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Download, RefreshCw, Trash2 } from "lucide-react"; -import { type Video } from "@shared/schema"; - -interface ThumbnailGeneratorProps { - video: Video; - onThumbnailGenerated?: (thumbnailUrl: string) => void; -} - -export default function ThumbnailGenerator({ video, onThumbnailGenerated }: ThumbnailGeneratorProps) { - const [isGenerating, setIsGenerating] = useState(false); - const [customTime, setCustomTime] = useState("5"); - const [generatedThumbnails, setGeneratedThumbnails] = useState>([]); - - const generateThumbnail = async (timestamp: string) => { - setIsGenerating(true); - try { - const response = await fetch(`/api/thumbnails/${video.id}/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - timestamps: [timestamp] - }), - }); - - if (response.ok) { - const data = await response.json(); - if (data.thumbnails && data.thumbnails.length > 0) { - const newThumbnail = data.thumbnails[0]; - setGeneratedThumbnails(prev => [...prev, newThumbnail]); - - if (onThumbnailGenerated) { - onThumbnailGenerated(newThumbnail.url); - } - } - } - } catch (error) { - console.error('Error generating thumbnail:', error); - } finally { - setIsGenerating(false); - } - }; - - const generateMultipleThumbnails = async () => { - setIsGenerating(true); - try { - const timestamps = ["5", "15", "30", "60", "90"]; - const response = await fetch(`/api/thumbnails/${video.id}/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - timestamps - }), - }); - - if (response.ok) { - const data = await response.json(); - setGeneratedThumbnails(data.thumbnails || []); - } - } catch (error) { - console.error('Error generating thumbnails:', error); - } finally { - setIsGenerating(false); - } - }; - - const loadExistingThumbnails = async () => { - try { - const response = await fetch(`/api/thumbnails/${video.id}`); - if (response.ok) { - const data = await response.json(); - setGeneratedThumbnails(data.thumbnails || []); - } - } catch (error) { - console.error('Error loading thumbnails:', error); - } - }; - - React.useEffect(() => { - loadExistingThumbnails(); - }, [video.id]); - - return ( - - - - - Ustvarjanje Thumbnail Slik - -

- Generirajte thumbnail slike iz različnih trenutkov videa "{video.title}" -

-
- - {/* Quick generate buttons */} -
- -
- -
-
- - {/* Custom time input */} -
- -
- setCustomTime(e.target.value)} - placeholder="Sekunde (npr. 30)" - min="0" - max={video.duration || 300} - /> - -
-

- Vnesite čas v sekundah (0 - {video.duration || 300}s) -

-
- - {/* Generated thumbnails grid */} - {generatedThumbnails.length > 0 && ( -
- -
- {generatedThumbnails.map((thumbnail, index) => ( -
-
- {`Thumbnail -
-
- {thumbnail.timestamp}s -
-
-
- - -
-
-
- ))} -
-
- )} - - {/* Instructions */} -
-

Navodila:

-
    -
  • Kliknite "Generiraj 5 slik" za hitro ustvarjanje slik iz različnih trenutkov
  • -
  • Uporabite "Določi čas" za ustvarjanje slike iz določene sekunde
  • -
  • Kliknite ✓ da nastavite sliko kot glavno thumbnail
  • -
  • Kliknite ⬇ da prenesete sliko na svoj računalnik
  • -
-
-
-
- ); -} \ No newline at end of file diff --git a/client/src/components/video-actions.tsx b/client/src/components/video-actions.tsx deleted file mode 100644 index 60fae23..0000000 --- a/client/src/components/video-actions.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Card, CardContent } from "@/components/ui/card"; -import { Heart, Plus, List, Check } from "lucide-react"; -import { useFavorites } from "@/hooks/use-favorites"; -import { usePlaylists } from "@/hooks/use-playlists"; -import type { Video, Playlist } from "@shared/schema"; - -interface VideoActionsProps { - video: Video; - showLabel?: boolean; -} - -export function VideoActions({ video, showLabel = false }: VideoActionsProps) { - const [isPlaylistDialogOpen, setIsPlaylistDialogOpen] = useState(false); - const { useIsFavorited, addToFavorites, removeFromFavorites } = useFavorites(); - const { useUserPlaylists, addVideoToPlaylist } = usePlaylists(); - - const { data: favoriteStatus } = useIsFavorited(video.id); - const isFavorited = favoriteStatus?.isFavorited || false; - - const { data: playlists } = useUserPlaylists(); - const playlistsArray = Array.isArray(playlists) ? playlists : []; - - const handleFavoriteToggle = () => { - if (isFavorited) { - removeFromFavorites(video.id); - } else { - addToFavorites(video.id); - } - }; - - const handleAddToPlaylist = (playlistId: string) => { - addVideoToPlaylist({ playlistId, videoId: video.id }); - setIsPlaylistDialogOpen(false); - }; - - return ( -
- {/* Favorite Button */} - - - {/* Add to Playlist Button */} - - - - - - - Add to Playlist - -
-
- Select a playlist to add "{video.title}" to: -
- - {playlistsArray.length > 0 ? ( -
- {playlistsArray.map((playlist: Playlist) => ( - handleAddToPlaylist(playlist.id)} - data-testid={`card-playlist-option-${playlist.id}`} - > - -
-
-

{playlist.name}

- {playlist.description && ( -

- {playlist.description} -

- )} -
- -
-
-
- ))} -
- ) : ( -
- -

- You don't have any playlists yet. -

- -
- )} -
-
-
-
- ); -} \ No newline at end of file diff --git a/client/src/components/video-card.tsx b/client/src/components/video-card.tsx index b29d30b..6a29da3 100644 --- a/client/src/components/video-card.tsx +++ b/client/src/components/video-card.tsx @@ -1,7 +1,6 @@ import { Play, Share2 } from "lucide-react"; import { type Video } from "@shared/schema"; import { Button } from "@/components/ui/button"; -import { VideoActions } from "@/components/video-actions"; interface VideoCardProps { video: Video; @@ -62,21 +61,6 @@ export default function VideoCard({ video, onClick, onShare }: VideoCardProps) { alt={video.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" data-testid={`img-thumbnail-${video.id}`} - onLoad={() => { - console.log('Thumbnail loaded successfully:', video.thumbnailUrl); - }} - onError={(e) => { - console.error('Thumbnail failed to load:', video.thumbnailUrl); - // Use a data URL SVG as fallback - const target = e.target as HTMLImageElement; - const fallbackSvg = ` - - - Thumbnail Error - - `; - target.src = 'data:image/svg+xml;base64,' + btoa(fallbackSvg); - }} />
@@ -122,20 +106,17 @@ export default function VideoCard({ video, onClick, onShare }: VideoCardProps) { {formatDate(video.createdAt)}
-
- - {onShare && ( - - )} -
+ {onShare && ( + + )} diff --git a/client/src/components/video-modal.tsx b/client/src/components/video-modal.tsx index 28fa35c..cffb1c6 100644 --- a/client/src/components/video-modal.tsx +++ b/client/src/components/video-modal.tsx @@ -1,11 +1,9 @@ -import { useEffect, useRef, useState } from "react"; -import { X, Settings } from "lucide-react"; +import { useEffect, useRef } from "react"; +import { X } from "lucide-react"; import { type Video } from "@shared/schema"; import { Button } from "@/components/ui/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { apiRequest } from "@/lib/queryClient"; import Hls from "hls.js"; -import ThumbnailGenerator from "./thumbnail-generator"; interface VideoModalProps { video: Video | null; @@ -49,7 +47,6 @@ function formatDate(date: Date | string): string { export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) { const videoRef = useRef(null); const hlsRef = useRef(null); - const [activeTab, setActiveTab] = useState("video"); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { @@ -71,14 +68,74 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) }; }, [isOpen, onClose]); - // Since we're now using iframe embeds for Bunny.net videos, we don't need HLS.js + // Initialize HLS when video is available useEffect(() => { - // Just log for debugging - if (isOpen && video) { - console.log('Video modal opened with:', video.videoUrl); + if (isOpen && video && videoRef.current) { + const videoElement = videoRef.current; + + // Clean up previous HLS instance + if (hlsRef.current) { + hlsRef.current.destroy(); + hlsRef.current = null; + } + + const videoUrl = video.videoUrl; + console.log('Loading video:', videoUrl); + + // Check if the video URL is HLS (.m3u8) + if (videoUrl.includes('.m3u8')) { + if (Hls.isSupported()) { + // Use HLS.js for browsers that don't support HLS natively + const hls = new Hls({ + debug: true, + enableWorker: false, + lowLatencyMode: true, + backBufferLength: 90 + }); + + hls.loadSource(videoUrl); + hls.attachMedia(videoElement); + + hls.on(Hls.Events.MANIFEST_PARSED, () => { + console.log('HLS manifest loaded successfully'); + }); + + hls.on(Hls.Events.ERROR, (event, data) => { + console.error('HLS error:', data); + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + console.log('Network error, trying to recover...'); + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.log('Media error, trying to recover...'); + hls.recoverMediaError(); + break; + default: + console.log('Fatal error, destroying HLS instance...'); + hls.destroy(); + break; + } + } + }); + + hlsRef.current = hls; + } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { + // For Safari that supports HLS natively + videoElement.src = videoUrl; + console.log('Using native HLS support'); + } else { + console.error('HLS is not supported in this browser'); + } + } else { + // For regular MP4 videos + videoElement.src = videoUrl; + console.log('Using native video support for MP4'); + } } - - // Cleanup if needed + + // Cleanup when modal closes return () => { if (hlsRef.current) { hlsRef.current.destroy(); @@ -127,21 +184,21 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) + frameBorder="0" + onLoad={handleVideoPlay} + data-testid="video-iframe" + /> ) : ( diff --git a/client/src/hooks/use-favorites.ts b/client/src/hooks/use-favorites.ts deleted file mode 100644 index e495006..0000000 --- a/client/src/hooks/use-favorites.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { apiRequest } from "@/lib/queryClient"; -import { useToast } from "@/hooks/use-toast"; - -const CURRENT_USER_ID = "5311f8eb-aba2-4f58-96bf-0ca99fe5907c"; // Test user from database - -export function useFavorites() { - const queryClient = useQueryClient(); - const { toast } = useToast(); - - // Check if video is favorited - const useIsFavorited = (videoId: string) => { - return useQuery({ - queryKey: ["/api/favorites", CURRENT_USER_ID, videoId], - queryFn: async () => { - const response = await fetch(`/api/favorites/${CURRENT_USER_ID}/${videoId}`); - if (!response.ok) throw new Error('Failed to check favorite status'); - return response.json(); - }, - enabled: !!videoId, - }); - }; - - // Add to favorites - const addToFavoritesMutation = useMutation({ - mutationFn: (videoId: string) => - apiRequest("/api/favorites", "POST", { - userId: CURRENT_USER_ID, - videoId, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["/api/favorites"] }); - toast({ title: "Added to favorites!" }); - }, - onError: (error: any) => { - if (error.message?.includes("already in favorites")) { - toast({ title: "Already in favorites", variant: "destructive" }); - } else { - toast({ title: "Failed to add to favorites", variant: "destructive" }); - } - }, - }); - - // Remove from favorites - const removeFromFavoritesMutation = useMutation({ - mutationFn: (videoId: string) => - apiRequest(`/api/favorites/${CURRENT_USER_ID}/${videoId}`, "DELETE"), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["/api/favorites"] }); - toast({ title: "Removed from favorites" }); - }, - onError: () => { - toast({ title: "Failed to remove from favorites", variant: "destructive" }); - }, - }); - - return { - useIsFavorited, - addToFavorites: addToFavoritesMutation.mutate, - removeFromFavorites: removeFromFavoritesMutation.mutate, - isAddingToFavorites: addToFavoritesMutation.isPending, - isRemovingFromFavorites: removeFromFavoritesMutation.isPending, - }; -} \ No newline at end of file diff --git a/client/src/hooks/use-playlists.ts b/client/src/hooks/use-playlists.ts deleted file mode 100644 index a1e36b6..0000000 --- a/client/src/hooks/use-playlists.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { apiRequest } from "@/lib/queryClient"; -import { useToast } from "@/hooks/use-toast"; -import type { Playlist, InsertPlaylist } from "@shared/schema"; - -const CURRENT_USER_ID = "5311f8eb-aba2-4f58-96bf-0ca99fe5907c"; // Test user from database - -export function usePlaylists() { - const queryClient = useQueryClient(); - const { toast } = useToast(); - - // Get user playlists - const useUserPlaylists = () => { - return useQuery({ - queryKey: ["/api/playlists", CURRENT_USER_ID], - }); - }; - - // Get single playlist - const usePlaylist = (playlistId: string) => { - return useQuery({ - queryKey: ["/api/playlists", playlistId], - enabled: !!playlistId, - }); - }; - - // Get playlist videos - const usePlaylistVideos = (playlistId: string) => { - return useQuery({ - queryKey: ["/api/playlists", playlistId, "videos"], - enabled: !!playlistId, - }); - }; - - // Add video to playlist - const addVideoToPlaylistMutation = useMutation({ - mutationFn: ({ playlistId, videoId }: { playlistId: string; videoId: string }) => - apiRequest(`/api/playlists/${playlistId}/videos`, "POST", { - videoId, - position: 0, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["/api/playlists"] }); - toast({ title: "Added to playlist!" }); - }, - onError: (error: any) => { - if (error.message?.includes("already in playlist")) { - toast({ title: "Video is already in this playlist", variant: "destructive" }); - } else { - toast({ title: "Failed to add to playlist", variant: "destructive" }); - } - }, - }); - - // Remove video from playlist - const removeVideoFromPlaylistMutation = useMutation({ - mutationFn: ({ playlistId, videoId }: { playlistId: string; videoId: string }) => - apiRequest(`/api/playlists/${playlistId}/videos/${videoId}`, "DELETE"), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["/api/playlists"] }); - toast({ title: "Removed from playlist" }); - }, - onError: () => { - toast({ title: "Failed to remove from playlist", variant: "destructive" }); - }, - }); - - return { - useUserPlaylists, - usePlaylist, - usePlaylistVideos, - addVideoToPlaylist: addVideoToPlaylistMutation.mutate, - removeVideoFromPlaylist: removeVideoFromPlaylistMutation.mutate, - isAddingToPlaylist: addVideoToPlaylistMutation.isPending, - isRemovingFromPlaylist: removeVideoFromPlaylistMutation.isPending, - }; -} \ No newline at end of file diff --git a/client/src/pages/favorites.tsx b/client/src/pages/favorites.tsx deleted file mode 100644 index 8cfaeaf..0000000 --- a/client/src/pages/favorites.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { apiRequest } from "@/lib/queryClient"; -import { Card, CardContent } from "@/components/ui/card"; -import { Heart, Clock, Play } from "lucide-react"; -import VideoModal from "@/components/video-modal"; -import { useState } from "react"; -import { Navigation } from "@/components/navigation"; -import type { UserFavorite, Video } from "@shared/schema"; - -const CURRENT_USER_ID = "test-user-123"; // For demo purposes - -export function FavoritesPage() { - const [selectedVideo, setSelectedVideo] = useState