chore(prod): sync with main #4
@@ -18,7 +18,7 @@ const DropdownItem = ({ item, onClick, className }: DropdownItemProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DropdownProps = {
|
type DropdownProps = {
|
||||||
label: string;
|
label: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ type props = {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Divider = () => <hr className="my-1.5 mx-2 border-gray-100" />;
|
||||||
|
|
||||||
const Sidebar = ({user, className}: props) => {
|
const Sidebar = ({user, className}: props) => {
|
||||||
return (
|
return (
|
||||||
<aside className={clsx("h-screen flex flex-col bg-white border-r border-gray-200", className)}>
|
<aside className={clsx("h-screen flex flex-col bg-white border-r border-gray-200", className)}>
|
||||||
@@ -18,12 +20,27 @@ const Sidebar = ({user, className}: props) => {
|
|||||||
alt="VoD System Logo"
|
alt="VoD System Logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-col flex-1 gap-0.5 px-2 pb-4">
|
<nav className="flex flex-col flex-1 px-2 pb-4">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
<SidebarButton url="/" logo={<Home size={18}/>} label="Home" />
|
<SidebarButton url="/" logo={<Home size={18}/>} label="Home" />
|
||||||
<SidebarButton url="/create" logo={<Plus size={18}/>} label="Create Clip" />
|
<SidebarButton url="/create" logo={<Plus size={18}/>} label="Create Clip" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<SidebarButton url="/my-clips" logo={<Film size={18}/>} label="My Clips" />
|
<Divider />
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<SidebarButton url="/my-clips" logo={<Film size={18}/>} label="Clips" />
|
||||||
|
{/*<SidebarButton url="/vods" logo={<Video size={18}/>} label="VoDs" />*/}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
<SidebarButton url="/profile" logo={<UserIcon size={18}/>} label="Profile" />
|
<SidebarButton url="/profile" logo={<UserIcon size={18}/>} label="Profile" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type props = {
|
|||||||
setSidebarToggled: (toggled: boolean) => void;
|
setSidebarToggled: (toggled: boolean) => void;
|
||||||
user: User | null;
|
user: User | null;
|
||||||
fetchUser: () => void;
|
fetchUser: () => void;
|
||||||
|
isStreaming: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ const Topbar = ({
|
|||||||
setSidebarToggled,
|
setSidebarToggled,
|
||||||
user,
|
user,
|
||||||
fetchUser,
|
fetchUser,
|
||||||
|
isStreaming,
|
||||||
className}: props) => {
|
className}: props) => {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -69,7 +71,17 @@ const Topbar = ({
|
|||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown label={user.name}>
|
<Dropdown label={
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
{user.name}
|
||||||
|
{isStreaming && (
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}>
|
||||||
<DropdownItem item="Profile"
|
<DropdownItem item="Profile"
|
||||||
onClick={() => navigate("/profile")} />
|
onClick={() => navigate("/profile")} />
|
||||||
<DropdownItem item="Logout"
|
<DropdownItem item="Logout"
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import Topbar from '../components/Topbar'
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import type {User} from "../utils/types";
|
import type {User} from "../utils/types";
|
||||||
import { getUser } from "../utils/endpoints";
|
import { getUser, getStreamStatus } from "../utils/endpoints";
|
||||||
|
|
||||||
const MainLayout = () => {
|
const MainLayout = () => {
|
||||||
const [sidebarToggled, setSidebarToggled] = useState(false);
|
const [sidebarToggled, setSidebarToggled] = useState(false);
|
||||||
const [user, setUser] = useState<null | User>(null);
|
const [user, setUser] = useState<null | User>(null);
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -23,6 +24,14 @@ const MainLayout = () => {
|
|||||||
fetchUser();
|
fetchUser();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
const poll = () => getStreamStatus().then(setIsStreaming).catch(() => {});
|
||||||
|
poll();
|
||||||
|
const interval = setInterval(poll, 10000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`transition-all duration-300 grid h-screen ${sidebarToggled ? "grid-cols-[0px_1fr]" : "grid-cols-[240px_1fr]"} grid-rows-[auto_1fr]`}>
|
<div className={`transition-all duration-300 grid h-screen ${sidebarToggled ? "grid-cols-[0px_1fr]" : "grid-cols-[240px_1fr]"} grid-rows-[auto_1fr]`}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
@@ -33,7 +42,8 @@ const MainLayout = () => {
|
|||||||
sidebarToggled={sidebarToggled}
|
sidebarToggled={sidebarToggled}
|
||||||
setSidebarToggled={setSidebarToggled}
|
setSidebarToggled={setSidebarToggled}
|
||||||
user={user}
|
user={user}
|
||||||
fetchUser={fetchUser}/>
|
fetchUser={fetchUser}
|
||||||
|
isStreaming={isStreaming}/>
|
||||||
<div className="overflow-auto bg-background">
|
<div className="overflow-auto bg-background">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -295,6 +295,13 @@ const deleteClip = async (id: number): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStreamStatus = async (): Promise<boolean> => {
|
||||||
|
const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' });
|
||||||
|
if (!response.ok) return false;
|
||||||
|
const result: APIResponse = await response.json();
|
||||||
|
return result.status === 'success' && result.data?.isStreaming === true;
|
||||||
|
};
|
||||||
|
|
||||||
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
||||||
const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"});
|
const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"});
|
||||||
return response.ok;
|
return response.ok;
|
||||||
@@ -305,6 +312,7 @@ const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
|||||||
export {
|
export {
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
getStreamStatus,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
editFile,
|
editFile,
|
||||||
processFile,
|
processFile,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
import com.ddf.vodsystem.controllers.dto.ClipSectionRequest;
|
import com.ddf.vodsystem.controllers.dto.ClipSectionRequest;
|
||||||
|
import com.ddf.vodsystem.controllers.dto.StreamResponse;
|
||||||
import com.ddf.vodsystem.dto.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import com.ddf.vodsystem.controllers.dto.SaveSectionRequest;
|
import com.ddf.vodsystem.controllers.dto.SaveSectionRequest;
|
||||||
import com.ddf.vodsystem.entities.Stream;
|
import com.ddf.vodsystem.entities.Stream;
|
||||||
@@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/stream")
|
@RequestMapping("/api/v1/stream")
|
||||||
@@ -66,4 +68,14 @@ public class StreamController {
|
|||||||
List<Stream> history = streamService.getStreamHistory(userId);
|
List<Stream> history = streamService.getStreamHistory(userId);
|
||||||
return ResponseEntity.ok(new APIResponse<>("success", "Stream history retrieved", history));
|
return ResponseEntity.ok(new APIResponse<>("success", "Stream history retrieved", history));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/current")
|
||||||
|
public ResponseEntity<APIResponse<StreamResponse>> getCurrentStream() {
|
||||||
|
Optional<Stream> stream = streamService.getActiveStream();
|
||||||
|
return stream.map(value -> ResponseEntity.ok(new APIResponse<>("success",
|
||||||
|
"Streaming status retrieved",
|
||||||
|
new StreamResponse(true, value.getId())))).orElseGet(() -> ResponseEntity.ok(new APIResponse<>("success",
|
||||||
|
"Streaming status retrieved",
|
||||||
|
new StreamResponse(false, null))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ddf.vodsystem.controllers.dto;
|
||||||
|
|
||||||
|
public record StreamResponse(
|
||||||
|
boolean isStreaming,
|
||||||
|
Long id
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -85,9 +85,14 @@ public class StreamService {
|
|||||||
streamRepository.saveAndFlush(stream);
|
streamRepository.saveAndFlush(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Stream> getActiveStream(String streamKey) {
|
public Optional<Stream> getActiveStream() {
|
||||||
User user = resolveUser(streamKey);
|
Optional<User> user = userService.getLoggedInUser();
|
||||||
return streamRepository.findByUserAndEndDateIsNull(user);
|
|
||||||
|
if (user.isEmpty()) {
|
||||||
|
throw new NotAuthenticated("Log in to see user streams");
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamRepository.findByUserAndEndDateIsNull(user.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Stream> getStreamHistory(Long userId) {
|
public List<Stream> getStreamHistory(Long userId) {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ public class UserService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
|
||||||
public UserService(UserRepository userRepository,
|
public UserService(UserRepository userRepository,
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
@Value("${google.client.id}") String googleClientId) {
|
@Value("${google.client.id}") String googleClientId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user