Changeset 9c1dcc7


Ignore:
Timestamp:
02/07/26 19:52:09 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
8675f75
Parents:
8a47b30
Message:

add experimental video miniplayer

Files:
2 added
10 edited

Legend:

Unmodified
Added
Removed
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/SongController.java

    r8a47b30 r9c1dcc7  
    44import com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto;
    55import com.ukim.finki.develop.finkwave.model.dto.SongDetailsDto;
     6import com.ukim.finki.develop.finkwave.model.dto.SongDto;
    67import com.ukim.finki.develop.finkwave.repository.SongRepository;
    78import com.ukim.finki.develop.finkwave.service.AuthService;
     
    2223
    2324    @GetMapping("/top")
    24     public HttpEntity<List<MusicalEntityDto>> getSongs(){
     25    public HttpEntity<List<SongDto>> getSongs(){
    2526        return ResponseEntity.ok(songService.getTopSongs());
    2627    }
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/SongRepository.java

    r8a47b30 r9c1dcc7  
    77import org.springframework.data.jpa.repository.Query;
    88import org.springframework.data.repository.query.Param;
    9 import org.springframework.security.core.parameters.P;
    109import org.springframework.stereotype.Repository;
    1110
     
    5049
    5150    @Query("""
    52         SELECT NEW com.ukim.finki.develop.finkwave.model.dto.MusicalEntityDto(
     51        SELECT NEW com.ukim.finki.develop.finkwave.model.dto.SongDto(
    5352            s.id,
    5453            s.musicalEntities.title,
     
    5857            s.musicalEntities.cover,
    5958            (EXISTS (SELECT 1 FROM Like l WHERE l.musicalEntity.id = s.id AND l.listener.id = :currentUserId)),
    60             s.album.musicalEntities.title
     59            s.album.musicalEntities.title,
     60            s.link
    6161        )
    6262        FROM Song s
     
    6666    """)
    6767    // todo: add paging
    68     List<MusicalEntityDto> findTopByListens(@Param("currentUserId")Long currentUserId);
     68    List<SongDto> findTopByListens(@Param("currentUserId")Long currentUserId);
    6969
    7070    @Query("""
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/SongService.java

    r8a47b30 r9c1dcc7  
    1818    private final SongDetailsMapper songDetailsMapper;
    1919
    20     public List<MusicalEntityDto> getTopSongs(){
     20    public List<SongDto> getTopSongs(){
    2121        Long userId = null;
    2222        try {
  • frontend/src/App.tsx

    r8a47b30 r9c1dcc7  
    99import "react-toastify/dist/ReactToastify.css";
    1010import LoadingSpinner from "./components/LoadingSpinner";
     11import MiniPlayer from "./components/MiniPlayer";
    1112import Sidebar from "./components/Sidebar";
    1213import { useAuth } from "./context/authContext";
     14import { usePlayer } from "./context/playerContext";
    1315import AllUsers from "./pages/AllUsers";
    1416import LandingPage from "./pages/LandingPage";
     
    2224const MainLayout = () => {
    2325        const { user } = useAuth();
     26        const { currentSong } = usePlayer();
    2427        const location = useLocation();
    2528        // show sidebar only if user is logged in and is on the landing page
     
    5659                                className={`grow transition-all duration-300 pt-20 ${
    5760                                        isSidebarOpen ? "ml-64" : "ml-0"
    58                                 }`}
     61                                } ${currentSong ? "pb-20" : ""}`}
    5962                        >
    6063                                <Outlet />
    6164                        </main>
     65                        <MiniPlayer />
    6266                </div>
    6367        );
  • frontend/src/components/Sidebar.tsx

    r8a47b30 r9c1dcc7  
    11import { useEffect, useState } from "react";
     2import { useNavigate } from "react-router-dom";
    23import axiosInstance from "../api/axiosInstance";
    34import { useAuth } from "../context/authContext";
     5import { usePlayer } from "../context/playerContext";
    46import type { BasicPlaylist, BasicSong, SidebarProps } from "../utils/types";
     7
     8const toEmbedUrl = (url: string): string => {
     9        try {
     10                const parsed = new URL(url);
     11                if (
     12                        (parsed.hostname === "www.youtube.com" ||
     13                                parsed.hostname === "youtube.com") &&
     14                        parsed.searchParams.has("v")
     15                ) {
     16                        return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
     17                }
     18                if (parsed.hostname === "youtu.be") {
     19                        return `https://www.youtube.com/embed${parsed.pathname}`;
     20                }
     21                return url;
     22        } catch {
     23                return url;
     24        }
     25};
    526
    627const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
    728        const { user } = useAuth();
     29        const navigate = useNavigate();
     30        const { play, currentSong } = usePlayer();
    831        const [recentlyListened, setRecentlyListened] = useState<BasicSong[]>([]);
    932        const [playlists, setPlaylists] = useState<BasicPlaylist[]>([]);
     
    7497                                                        <div
    7598                                                                key={song.id}
    76                                                                 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
     99                                                                onClick={() => navigate(`/songs/${song.id}`)}
     100                                                                className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group relative"
    77101                                                        >
    78102                                                                <img
     
    80104                                                                        alt={song.title}
    81105                                                                        className="w-10 h-10 rounded object-cover"
     106                                                                        onError={(e) => {
     107                                                                                (e.target as HTMLImageElement).src = "/favicon.png";
     108                                                                        }}
    82109                                                                />
    83110                                                                <div className="flex-1 min-w-0">
     
    89116                                                                        </p>
    90117                                                                </div>
     118                                                                {song.link && (
     119                                                                        <button
     120                                                                                onClick={(e) => {
     121                                                                                        e.stopPropagation();
     122                                                                                        play({
     123                                                                                                id: song.id,
     124                                                                                                title: song.title,
     125                                                                                                artist: song.artist,
     126                                                                                                cover: song.cover,
     127                                                                                                embedUrl: toEmbedUrl(song.link!),
     128                                                                                        });
     129                                                                                }}
     130                                                                                className={`p-1.5 rounded-full transition-all opacity-0 group-hover:opacity-100 ${
     131                                                                                        currentSong?.id === song.id
     132                                                                                                ? "bg-white text-[#1db954]"
     133                                                                                                : "bg-[#1db954] text-black hover:scale-110"
     134                                                                                }`}
     135                                                                                aria-label={
     136                                                                                        currentSong?.id === song.id ? "Now playing" : "Play song"
     137                                                                                }
     138                                                                        >
     139                                                                                {currentSong?.id === song.id ? (
     140                                                                                        <svg
     141                                                                                                className="w-4 h-4"
     142                                                                                                fill="currentColor"
     143                                                                                                viewBox="0 0 24 24"
     144                                                                                        >
     145                                                                                                <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     146                                                                                        </svg>
     147                                                                                ) : (
     148                                                                                        <svg
     149                                                                                                className="w-4 h-4"
     150                                                                                                fill="currentColor"
     151                                                                                                viewBox="0 0 24 24"
     152                                                                                        >
     153                                                                                                <path d="M8 5v14l11-7z" />
     154                                                                                        </svg>
     155                                                                                )}
     156                                                                        </button>
     157                                                                )}
    91158                                                        </div>
    92159                                                ))}
  • frontend/src/components/search/SongResult.tsx

    r8a47b30 r9c1dcc7  
     1import { useNavigate } from "react-router-dom";
     2import { usePlayer } from "../../context/playerContext";
    13import type { Song } from "../../utils/types";
    24
     
    57}
    68
     9const toEmbedUrl = (url: string): string => {
     10        try {
     11                const parsed = new URL(url);
     12                if (
     13                        (parsed.hostname === "www.youtube.com" ||
     14                                parsed.hostname === "youtube.com") &&
     15                        parsed.searchParams.has("v")
     16                ) {
     17                        return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
     18                }
     19                if (parsed.hostname === "youtu.be") {
     20                        return `https://www.youtube.com/embed${parsed.pathname}`;
     21                }
     22                return url;
     23        } catch {
     24                return url;
     25        }
     26};
     27
    728const SongResult = ({ song }: SongResultProps) => {
     29        const navigate = useNavigate();
     30        const { play, currentSong } = usePlayer();
     31
    832        return (
    9                 <div className="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group">
     33                <div
     34                        onClick={() => navigate(`/songs/${song.id}`)}
     35                        className="flex items-center gap-4 p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors group"
     36                >
    1037                        <img
    1138                                src={song.cover || "/favicon.png"}
     
    2249                                </p>
    2350                        </div>
    24                         <span className="text-xs text-gray-500 uppercase tracking-wider">
     51                        <span className="text-xs text-gray-500 uppercase tracking-wider mr-2">
    2552                                {song.genre}
    2653                        </span>
     54                        {song.link && (
     55                                <button
     56                                        onClick={(e) => {
     57                                                e.stopPropagation();
     58                                                play({
     59                                                        id: song.id,
     60                                                        title: song.title,
     61                                                        artist: song.releasedBy,
     62                                                        cover: song.cover,
     63                                                        embedUrl: toEmbedUrl(song.link!),
     64                                                });
     65                                        }}
     66                                        className={`p-2 rounded-full transition-all opacity-0 group-hover:opacity-100 ${
     67                                                currentSong?.id === song.id
     68                                                        ? "bg-white text-[#1db954]"
     69                                                        : "bg-[#1db954] text-black hover:scale-110"
     70                                        }`}
     71                                        aria-label={currentSong?.id === song.id ? "Now playing" : "Play song"}
     72                                >
     73                                        {currentSong?.id === song.id ? (
     74                                                <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
     75                                                        <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     76                                                </svg>
     77                                        ) : (
     78                                                <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
     79                                                        <path d="M8 5v14l11-7z" />
     80                                                </svg>
     81                                        )}
     82                                </button>
     83                        )}
    2784                </div>
    2885        );
  • frontend/src/main.tsx

    r8a47b30 r9c1dcc7  
    33import App from "./App.tsx";
    44import { AuthProvider } from "./context/authContext.tsx";
     5import { PlayerProvider } from "./context/playerContext.tsx";
    56import "./index.css";
    67
     
    89        <StrictMode>
    910                <AuthProvider>
    10                         <App />
     11                        <PlayerProvider>
     12                                <App />
     13                        </PlayerProvider>
    1114                </AuthProvider>
    1215        </StrictMode>,
  • frontend/src/pages/LandingPage.tsx

    r8a47b30 r9c1dcc7  
    11import { useEffect, useRef, useState } from "react";
     2import { useNavigate } from "react-router-dom";
    23import axiosInstance from "../api/axiosInstance";
    34import AlbumResult from "../components/search/AlbumResult";
    45import SongResult from "../components/search/SongResult";
    56import UserResult from "../components/search/UserResult";
     7import { usePlayer } from "../context/playerContext";
    68import type {
    79        Album,
     
    1113} from "../utils/types";
    1214
     15// Convert a regular YouTube URL to an embeddable URL
     16const toEmbedUrl = (url: string): string => {
     17        try {
     18                const parsed = new URL(url);
     19                if (
     20                        (parsed.hostname === "www.youtube.com" ||
     21                                parsed.hostname === "youtube.com") &&
     22                        parsed.searchParams.has("v")
     23                ) {
     24                        return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
     25                }
     26                if (parsed.hostname === "youtu.be") {
     27                        return `https://www.youtube.com/embed${parsed.pathname}`;
     28                }
     29                return url;
     30        } catch {
     31                return url;
     32        }
     33};
     34
    1335const CATEGORIES: { value: SearchCategory; label: string }[] = [
    1436        { value: "songs", label: "Songs" },
     
    1941
    2042const LandingPage = () => {
     43        const navigate = useNavigate();
     44        const { play, currentSong } = usePlayer();
    2145        const [songs, setSongs] = useState<Song[]>([]);
    2246        const [loading, setLoading] = useState<boolean>(true);
     
    327351                                                                                        <div
    328352                                                                                                key={song.id}
     353                                                                                                onClick={() => navigate(`/songs/${song.id}`)}
    329354                                                                                                className="bg-[#282828] rounded-xl overflow-hidden cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group"
    330355                                                                                        >
     
    340365                                                                                                        />
    341366                                                                                                        <div className="absolute top-0 left-0 w-full h-full bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
    342                                                                                                                 <div className="w-15 h-15 rounded-full bg-[#1db954] flex items-center justify-center text-2xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
    343                                                                                                                         ▶
    344                                                                                                                 </div>
     367                                                                                                                {song.link ? (
     368                                                                                                                        <button
     369                                                                                                                                onClick={(e) => {
     370                                                                                                                                        e.stopPropagation();
     371                                                                                                                                        play({
     372                                                                                                                                                id: song.id,
     373                                                                                                                                                title: song.title,
     374                                                                                                                                                artist: song.releasedBy,
     375                                                                                                                                                cover: song.cover,
     376                                                                                                                                                embedUrl: toEmbedUrl(song.link!),
     377                                                                                                                                        });
     378                                                                                                                                }}
     379                                                                                                                                className={`w-14 h-14 rounded-full flex items-center justify-center text-xl shadow-[0_4px_12px_rgba(29,185,84,0.5)] transition-all cursor-pointer ${
     380                                                                                                                                        currentSong?.id === song.id
     381                                                                                                                                                ? "bg-white text-[#1db954]"
     382                                                                                                                                                : "bg-[#1db954] text-black hover:scale-105"
     383                                                                                                                                }`}
     384                                                                                                                                aria-label={
     385                                                                                                                                        currentSong?.id === song.id
     386                                                                                                                                                ? "Now playing"
     387                                                                                                                                                : "Play song"
     388                                                                                                                                }
     389                                                                                                                        >
     390                                                                                                                                {currentSong?.id === song.id ? (
     391                                                                                                                                        <svg
     392                                                                                                                                                className="w-6 h-6"
     393                                                                                                                                                fill="currentColor"
     394                                                                                                                                                viewBox="0 0 24 24"
     395                                                                                                                                        >
     396                                                                                                                                                <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     397                                                                                                                                        </svg>
     398                                                                                                                                ) : (
     399                                                                                                                                        <svg
     400                                                                                                                                                className="w-6 h-6"
     401                                                                                                                                                fill="currentColor"
     402                                                                                                                                                viewBox="0 0 24 24"
     403                                                                                                                                        >
     404                                                                                                                                                <path d="M8 5v14l11-7z" />
     405                                                                                                                                        </svg>
     406                                                                                                                                )}
     407                                                                                                                        </button>
     408                                                                                                                ) : (
     409                                                                                                                        <div className="w-14 h-14 rounded-full bg-[#1db954] flex items-center justify-center text-xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
     410                                                                                                                                ▶
     411                                                                                                                        </div>
     412                                                                                                                )}
    345413                                                                                                        </div>
    346414                                                                                                </div>
  • frontend/src/pages/SongDetail.tsx

    r8a47b30 r9c1dcc7  
    33import axiosInstance from "../api/axiosInstance";
    44import { useAuth } from "../context/authContext";
     5import { usePlayer } from "../context/playerContext";
    56import type { SongDetail as SongDetailType } from "../utils/types";
    67
     
    6263        const { id } = useParams<{ id: string }>();
    6364        const { user } = useAuth();
     65        const { play, currentSong } = usePlayer();
    6466        const [song, setSong] = useState<SongDetailType | null>(null);
    6567        const [loading, setLoading] = useState(true);
     
    7274                                const response = await axiosInstance.get(`/songs/${id}/details`);
    7375                                setSong(response.data);
     76                                // Don't autoplay - user must click play button
    7477                        } catch (err) {
    7578                                console.error("Error fetching song details:", err);
     
    207210
    208211                                                {/* Action buttons */}
    209                                                 {/* todo: add add to playlist button */}
    210212                                                <div className="flex items-center gap-3 mt-4">
     213                                                        {/* Play button */}
     214                                                        {song.link && (
     215                                                                <button
     216                                                                        onClick={() =>
     217                                                                                play({
     218                                                                                        id: song.id,
     219                                                                                        title: song.title,
     220                                                                                        artist: song.releasedBy,
     221                                                                                        cover: song.cover,
     222                                                                                        embedUrl: toEmbedUrl(song.link!),
     223                                                                                })
     224                                                                        }
     225                                                                        className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
     226                                                                                currentSong?.id === song.id
     227                                                                                        ? "bg-white text-[#1db954]"
     228                                                                                        : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
     229                                                                        }`}
     230                                                                >
     231                                                                        {currentSong?.id === song.id ? (
     232                                                                                <>
     233                                                                                        <svg
     234                                                                                                className="w-5 h-5"
     235                                                                                                fill="currentColor"
     236                                                                                                viewBox="0 0 24 24"
     237                                                                                        >
     238                                                                                                <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     239                                                                                        </svg>
     240                                                                                        Now Playing
     241                                                                                </>
     242                                                                        ) : (
     243                                                                                <>
     244                                                                                        <svg
     245                                                                                                className="w-5 h-5"
     246                                                                                                fill="currentColor"
     247                                                                                                viewBox="0 0 24 24"
     248                                                                                        >
     249                                                                                                <path d="M8 5v14l11-7z" />
     250                                                                                        </svg>
     251                                                                                        Play Song
     252                                                                                </>
     253                                                                        )}
     254                                                                </button>
     255                                                        )}
     256
    211257                                                        {user && (
    212258                                                                <button
     
    248294                                </div>
    249295
    250                                 {/* YouTube embed */}
     296                                {/* Play button — plays in persistent mini-player */}
    251297                                {song.link && (
    252298                                        <section className="mb-10">
    253                                                 <div className="rounded-xl overflow-hidden shadow-lg aspect-video bg-black">
    254                                                         <iframe
    255                                                                 src={toEmbedUrl(song.link)}
    256                                                                 title={song.title}
    257                                                                 className="w-full h-full"
    258                                                                 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    259                                                                 allowFullScreen
    260                                                         />
    261                                                 </div>
     299                                                {currentSong?.id === song.id ? (
     300                                                        <div className="bg-[#1a1a2e]/60 rounded-xl p-4 flex items-center gap-3">
     301                                                                <div className="w-10 h-10 rounded-full bg-[#1db954] flex items-center justify-center text-black animate-pulse">
     302                                                                        <svg
     303                                                                                className="w-5 h-5"
     304                                                                                fill="currentColor"
     305                                                                                viewBox="0 0 24 24"
     306                                                                        >
     307                                                                                <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
     308                                                                        </svg>
     309                                                                </div>
     310                                                                <p className="text-[#1db954] font-medium">
     311                                                                        Now playing in mini-player below
     312                                                                </p>
     313                                                        </div>
     314                                                ) : (
     315                                                        <button
     316                                                                onClick={() =>
     317                                                                        play({
     318                                                                                id: song.id,
     319                                                                                title: song.title,
     320                                                                                artist: song.releasedBy,
     321                                                                                cover: song.cover,
     322                                                                                embedUrl: toEmbedUrl(song.link!),
     323                                                                        })
     324                                                                }
     325                                                                className="w-full bg-[#1a1a2e]/60 hover:bg-[#1a1a2e]/80 rounded-xl p-4 flex items-center gap-3 transition-colors cursor-pointer group"
     326                                                        >
     327                                                                <div className="w-10 h-10 rounded-full bg-[#1db954] flex items-center justify-center text-black group-hover:scale-105 transition-transform">
     328                                                                        <svg
     329                                                                                className="w-5 h-5"
     330                                                                                fill="currentColor"
     331                                                                                viewBox="0 0 24 24"
     332                                                                        >
     333                                                                                <path d="M8 5v14l11-7z" />
     334                                                                        </svg>
     335                                                                </div>
     336                                                                <p className="text-white font-medium">Play this song</p>
     337                                                        </button>
     338                                                )}
    262339                                        </section>
    263340                                )}
  • frontend/src/utils/types.ts

    r8a47b30 r9c1dcc7  
    2929        type: "SONG";
    3030        album?: string;
     31        link?: string;
    3132}
    3233
     
    9293        artist: string;
    9394        cover?: string;
     95        link?: string;
    9496}
    9597
Note: See TracChangeset for help on using the changeset viewer.