Compare commits
No commits in common. "6603bbf75facfc6946d169de98ce4b20abe0aa20" and "caa99cea3e866ead5c26c79acdc73cbf68d1474c" have entirely different histories.
6603bbf75f
...
caa99cea3e
@ -21,7 +21,6 @@ type Props = {
|
|||||||
url?: string; cover?: string; showControls?: boolean; className?: string;
|
url?: string; cover?: string; showControls?: boolean; className?: string;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
onChange?: (state: State) => void;
|
onChange?: (state: State) => void;
|
||||||
onProgress?: (current:number,duration:number) => void;
|
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
}
|
}
|
||||||
export type PlayerInstance = {
|
export type PlayerInstance = {
|
||||||
@ -85,9 +84,6 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
|||||||
player.on('ended', () => {
|
player.on('ended', () => {
|
||||||
setState({end: true, playing: false, error: false})
|
setState({end: true, playing: false, error: false})
|
||||||
})
|
})
|
||||||
player.on('timeupdate', () => {
|
|
||||||
props.onProgress?.(player.currentTime(), player.duration())
|
|
||||||
})
|
|
||||||
player.on('error', () => {
|
player.on('error', () => {
|
||||||
setState({end: false, playing: false, error: true})
|
setState({end: false, playing: false, error: true})
|
||||||
})
|
})
|
||||||
|
@ -2,7 +2,7 @@ import {useSortable} from "@dnd-kit/sortable";
|
|||||||
import {useSetState} from "ahooks";
|
import {useSetState} from "ahooks";
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import {clsx} from "clsx";
|
import {clsx} from "clsx";
|
||||||
import {App, Checkbox, Popconfirm} from "antd";
|
import {Checkbox, Popconfirm} from "antd";
|
||||||
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled, LoadingOutlined} from "@ant-design/icons";
|
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled, LoadingOutlined} from "@ant-design/icons";
|
||||||
|
|
||||||
import ImageCover from '@/assets/images/cover.png'
|
import ImageCover from '@/assets/images/cover.png'
|
||||||
@ -17,7 +17,6 @@ type Props = {
|
|||||||
index?: number;
|
index?: number;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
playing?: boolean;
|
|
||||||
onCheckedChange?: (checked: boolean) => void;
|
onCheckedChange?: (checked: boolean) => void;
|
||||||
onPlay?: () => void;
|
onPlay?: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
@ -30,7 +29,7 @@ type Props = {
|
|||||||
|
|
||||||
export const VideoListItem = (
|
export const VideoListItem = (
|
||||||
{
|
{
|
||||||
id, video, onRemove, checked,playing,
|
id, video, onRemove, checked,
|
||||||
onCheckedChange, onEdit, active, editable,
|
onCheckedChange, onEdit, active, editable,
|
||||||
className, sortable, type, index,onItemClick
|
className, sortable, type, index,onItemClick
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@ -46,19 +45,11 @@ export const VideoListItem = (
|
|||||||
}, [checked])
|
}, [checked])
|
||||||
|
|
||||||
const generating = (type == 'create' && video.status == VideoStatus.Generating )
|
const generating = (type == 'create' && video.status == VideoStatus.Generating )
|
||||||
const {modal} = App.useApp()
|
|
||||||
const handleDelete = () => {
|
|
||||||
if(!onRemove) return;
|
|
||||||
modal.confirm({
|
|
||||||
title: '提示',
|
|
||||||
centered: true,
|
|
||||||
content: '是否要删除该视频',
|
|
||||||
onOk: onRemove,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return <div
|
return <div
|
||||||
className={`video-item ${className}`}
|
className={`video-item ${className}`}
|
||||||
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
|
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
|
||||||
|
onClick={onItemClick}
|
||||||
>
|
>
|
||||||
<div className={`list-row ${generating ? 'disabled' : ''} ${active?'playing':''}`}>
|
<div className={`list-row ${generating ? 'disabled' : ''} ${active?'playing':''}`}>
|
||||||
<div
|
<div
|
||||||
@ -66,7 +57,7 @@ export const VideoListItem = (
|
|||||||
{... (sortable && !generating?listeners:{})}
|
{... (sortable && !generating?listeners:{})}
|
||||||
{... (sortable && !generating?attributes:{})}
|
{... (sortable && !generating?attributes:{})}
|
||||||
>{index}</div>
|
>{index}</div>
|
||||||
<div className="col cover cursor-pointer" onClick={onItemClick}>
|
<div className="col cover cursor-pointer">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover}/>
|
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover}/>
|
||||||
{generating &&
|
{generating &&
|
||||||
@ -75,7 +66,7 @@ export const VideoListItem = (
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{/* && active*/}
|
{/* && active*/}
|
||||||
{!generating && playing && <div className={'absolute rounded inset-0 bg-black/30 text-sm text-white flex items-center justify-center'}>
|
{!generating && active && <div className={'absolute rounded inset-0 bg-black/30 text-sm text-white flex items-center justify-center'}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<IconPlaying className="inline-block text-xl" />
|
<IconPlaying className="inline-block text-xl" />
|
||||||
<div>播放中</div>
|
<div>播放中</div>
|
||||||
@ -113,7 +104,14 @@ export const VideoListItem = (
|
|||||||
<IconEdit/>
|
<IconEdit/>
|
||||||
</button>}
|
</button>}
|
||||||
|
|
||||||
{onRemove && <button className="hover:text-blue-500" onClick={handleDelete}><IconDelete/></button>}
|
{onRemove && <Popconfirm
|
||||||
|
title={<div style={{minWidth: 150}}><span>请确认删除此视频?</span></div>}
|
||||||
|
onConfirm={onRemove}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<button className="hover:text-blue-500"><IconDelete/></button>
|
||||||
|
</Popconfirm>}
|
||||||
<Checkbox checked={state.checked} onChange={() => {
|
<Checkbox checked={state.checked} onChange={() => {
|
||||||
if (onCheckedChange) {
|
if (onCheckedChange) {
|
||||||
onCheckedChange(!state.checked)
|
onCheckedChange(!state.checked)
|
||||||
|
@ -32,8 +32,7 @@ export default function LiveIndex() {
|
|||||||
muted: true,
|
muted: true,
|
||||||
showToTop: false,
|
showToTop: false,
|
||||||
checkedAll: false,
|
checkedAll: false,
|
||||||
originSort: '',
|
originSort:''
|
||||||
playProgress: 0
|
|
||||||
})
|
})
|
||||||
const activeIndex = useRef(state.activeIndex)
|
const activeIndex = useRef(state.activeIndex)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -175,15 +174,6 @@ export default function LiveIndex() {
|
|||||||
// 计算总时长
|
// 计算总时长
|
||||||
return videoData.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0);
|
return videoData.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0);
|
||||||
}, [videoData])
|
}, [videoData])
|
||||||
// 根据当前播放index计算已经播放时长
|
|
||||||
const currentTotalDuration = useMemo(() => {
|
|
||||||
if (state.activeIndex == -1 || !videoData || videoData.length == 0) return 0;
|
|
||||||
// 计算总时长
|
|
||||||
return videoData
|
|
||||||
.filter((_, index) => (index < state.activeIndex))
|
|
||||||
.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0) + state.playProgress
|
|
||||||
;
|
|
||||||
}, [videoData, state.playProgress])
|
|
||||||
|
|
||||||
const currentSelectedId = useMemo(() => {
|
const currentSelectedId = useMemo(() => {
|
||||||
if (state.activeIndex < 0 || state.activeIndex >= videoData.length) return [];
|
if (state.activeIndex < 0 || state.activeIndex >= videoData.length) return [];
|
||||||
@ -195,21 +185,16 @@ export default function LiveIndex() {
|
|||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="video-player-container mr-8 flex flex-col">
|
<div className="video-player-container mr-8 flex flex-col">
|
||||||
<div className="text-center text-base">
|
<div className="text-center text-base">数字人直播间</div>
|
||||||
<span>视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="video-player flex justify-center flex-1 mt-5">
|
<div className="video-player flex justify-center flex-1 mt-5">
|
||||||
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
|
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
|
||||||
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
|
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
|
||||||
<Player
|
<Player ref={player} className="w-[360px] h-[636px] bg-white" muted={true}/>
|
||||||
ref={player} className="w-[360px] h-[636px] bg-white"
|
|
||||||
muted={true}
|
|
||||||
onProgress={(progress) => {
|
|
||||||
setState({playProgress: progress})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 text-center text-sm">
|
||||||
|
<span>视频时长: {formatDuration(totalDuration)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="video-list-container video-list-sort-container flex-1">
|
<div className="video-list-container video-list-sort-container flex-1">
|
||||||
@ -251,8 +236,7 @@ export default function LiveIndex() {
|
|||||||
<InfiniteScroller
|
<InfiniteScroller
|
||||||
ref={scrollerRef}
|
ref={scrollerRef}
|
||||||
onScroll={top => setState({showToTop: top > 30})}
|
onScroll={top => setState({showToTop: top > 30})}
|
||||||
onCallback={() => {
|
onCallback={()=>{}}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="sort-list-container flex-1">
|
<div className="sort-list-container flex-1">
|
||||||
<DndContext onDragEnd={(e) => {
|
<DndContext onDragEnd={(e) => {
|
||||||
@ -298,17 +282,17 @@ export default function LiveIndex() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="page-action">
|
<div className="page-action">
|
||||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||||
{checkedIdArray.length > 0 && <ButtonBatch
|
<ButtonBatch
|
||||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||||
selected={checkedIdArray}
|
selected={currentSelectedId}
|
||||||
emptyMessage={`请选择要删除的视频`}
|
emptyMessage={`请选择要删除的视频`}
|
||||||
confirmMessage={`是否删除当前的${checkedIdArray.length}条视频?`}
|
confirmMessage={`是否删除当前的${currentSelectedId.length}条视频?`}
|
||||||
onSuccess={loadList}
|
onSuccess={loadList}
|
||||||
onProcess={processDeleteVideo}
|
onProcess={processDeleteVideo}
|
||||||
>
|
>
|
||||||
<span className={'text'}>批量删除</span>
|
<span className={'text'}>批量删除</span>
|
||||||
<IconDelete/>
|
<IconDelete/>
|
||||||
</ButtonBatch>}
|
</ButtonBatch>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -112,7 +112,7 @@ export default function SearchPanel({onSearch}: SearchPanelProps) {
|
|||||||
const setFalse = ()=>togglePinnedManagePanel(false)
|
const setFalse = ()=>togglePinnedManagePanel(false)
|
||||||
useClickAway(() => setFalse(), pinnedManagePanel)
|
useClickAway(() => setFalse(), pinnedManagePanel)
|
||||||
|
|
||||||
return (<div className={`${styles.searchPanel} pt-6 pb-2`}>
|
return (<div className={`${styles.searchPanel} pt-8 pb-2`}>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="search-form flex items-center gap-4">
|
<div className="search-form flex items-center gap-4">
|
||||||
<Input
|
<Input
|
||||||
|
@ -68,7 +68,7 @@ export default function NewEdit() {
|
|||||||
|
|
||||||
return (<div className="container pb-5 news-edit">
|
return (<div className="container pb-5 news-edit">
|
||||||
<div className="search-panel-container my-5">
|
<div className="search-panel-container my-5">
|
||||||
<div className="search-form flex pt-1 gap-5 justify-between">
|
<div className="search-form flex gap-5 justify-between">
|
||||||
<EditSearchForm onSubmit={setParams}/>
|
<EditSearchForm onSubmit={setParams}/>
|
||||||
{/*<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>*/}
|
{/*<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>*/}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Checkbox, Empty} from "antd";
|
import {Checkbox, Empty, Modal} from "antd";
|
||||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||||
import {DndContext} from "@dnd-kit/core";
|
import {DndContext} from "@dnd-kit/core";
|
||||||
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||||
@ -9,30 +9,26 @@ import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
|||||||
import {deleteByIds, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
|
import {deleteByIds, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
|
||||||
import {formatDuration} from "@/util/strings.ts";
|
import {formatDuration} from "@/util/strings.ts";
|
||||||
import ButtonBatch from "@/components/button-batch.tsx";
|
import ButtonBatch from "@/components/button-batch.tsx";
|
||||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
import {showToast} from "@/components/message.ts";
|
||||||
import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
||||||
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
|
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
|
||||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||||
import {IconDelete} from "@/components/icons";
|
import {IconDelete} from "@/components/icons";
|
||||||
import {useLocation} from "react-router-dom";
|
import {useLocation} from "react-router-dom";
|
||||||
import {playState} from "@/service/api/live.ts";
|
|
||||||
|
|
||||||
export default function VideoIndex() {
|
export default function VideoIndex() {
|
||||||
const [editId, setEditId] = useState(-1)
|
const [editId, setEditId] = useState(-1)
|
||||||
const loc = useLocation()
|
const loc = useLocation()
|
||||||
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||||
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
const player = useRef<PlayerInstance | null>(null)
|
const player = useRef<PlayerInstance | null>(null)
|
||||||
const scrollerRef = useRef<InfiniteScrollerRef|null>(null)
|
const scrollerRef = useRef<InfiniteScrollerRef|null>(null)
|
||||||
const [state, setState] = useSetState({
|
const [state, setState] = useSetState({
|
||||||
checkedAll: false,
|
checkedAll: false,
|
||||||
playingIndex: -1,
|
playingIndex: -1,
|
||||||
showToTop: false,
|
showToTop: false,
|
||||||
showStatePos: false,
|
showStatePos: false
|
||||||
playState: {
|
|
||||||
current: -1,
|
|
||||||
total: -1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const [checkedIdArray, setCheckedIdArray] = useState<Id[]>([])
|
const [checkedIdArray, setCheckedIdArray] = useState<Id[]>([])
|
||||||
|
|
||||||
@ -61,13 +57,13 @@ export default function VideoIndex() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(video.status == VideoStatus.Generating ) return;
|
if(video.status == VideoStatus.Generating ) return;
|
||||||
// setState({playingIndex})
|
|
||||||
// player.current?.play('https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-17/1186196465916190720.flv', 30)
|
|
||||||
//
|
|
||||||
if (video.oss_video_url && video.status !== 1) {
|
|
||||||
setState({playingIndex})
|
setState({playingIndex})
|
||||||
player.current?.play(video.oss_video_url, 0)
|
player.current?.play('https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-17/1186196465916190720.flv',30)
|
||||||
}
|
//
|
||||||
|
// if (video.oss_video_url && video.status !== 1) {
|
||||||
|
// setState({playingIndex})
|
||||||
|
// player.current?.play(video.oss_video_url, 0)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
// 处理全选
|
// 处理全选
|
||||||
const handleAllCheckedChange = () => {
|
const handleAllCheckedChange = () => {
|
||||||
@ -105,14 +101,9 @@ export default function VideoIndex() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},[videoData,scrollerRef])
|
},[videoData,scrollerRef])
|
||||||
const processDeleteVideo = async (ids: Id[]) => {
|
|
||||||
deleteByIds(ids).then(() => {
|
|
||||||
showToast('删除成功!', 'success')
|
|
||||||
loadList()
|
|
||||||
}).catch(showErrorToast)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<div className="container py-10 page-live">
|
return (<div className="container py-10 page-live">
|
||||||
|
{contextHolder}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="video-player-container mr-16 w-[360px] flex flex-col">
|
<div className="video-player-container mr-16 w-[360px] flex flex-col">
|
||||||
<div className="text-center text-base text-gray-400">预览视频 - 点击视频列表播放</div>
|
<div className="text-center text-base text-gray-400">预览视频 - 点击视频列表播放</div>
|
||||||
@ -121,21 +112,12 @@ export default function VideoIndex() {
|
|||||||
<Player
|
<Player
|
||||||
ref={player} url={videoData[state.playingIndex]?.oss_video_url}
|
ref={player} url={videoData[state.playingIndex]?.oss_video_url}
|
||||||
onChange={(state) => {
|
onChange={(state) => {
|
||||||
console.log(state)
|
|
||||||
if (state.end || state.error) setState({playingIndex: -1})
|
if (state.end || state.error) setState({playingIndex: -1})
|
||||||
}}
|
}}
|
||||||
onProgress={(current, duration) => {
|
|
||||||
setState({
|
|
||||||
playState: {
|
|
||||||
current: current,
|
|
||||||
total: duration
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
className="w-[360px] h-[640px] bg-white"/>
|
className="w-[360px] h-[640px] bg-white"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm mt-4 text-gray-400">{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}</div>
|
<div className="text-center text-sm mt-4 text-gray-400">视频时长: {formatDuration(totalDuration)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="video-list-container rounded flex flex-col flex-1">
|
<div className="video-list-container rounded flex flex-col flex-1">
|
||||||
<div className="live-control flex justify-between">
|
<div className="live-control flex justify-between">
|
||||||
@ -194,8 +176,7 @@ export default function VideoIndex() {
|
|||||||
id={v.id}
|
id={v.id}
|
||||||
key={index}
|
key={index}
|
||||||
type={'create'}
|
type={'create'}
|
||||||
active={checkedIdArray.includes(v.id)}
|
active={state.playingIndex == index}
|
||||||
playing={state.playingIndex == index}
|
|
||||||
checked={checkedIdArray.includes(v.id)}
|
checked={checkedIdArray.includes(v.id)}
|
||||||
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status}`}
|
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status}`}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@ -206,7 +187,7 @@ export default function VideoIndex() {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onItemClick={ () => playVideo(v, index)}
|
onItemClick={ () => playVideo(v, index)}
|
||||||
onRemove={() => processDeleteVideo([v.id])}
|
onRemove={()=>{}}
|
||||||
onEdit={v.status == VideoStatus.Generating ? undefined : () => {
|
onEdit={v.status == VideoStatus.Generating ? undefined : () => {
|
||||||
setEditId(v.article_id)
|
setEditId(v.article_id)
|
||||||
}}
|
}}
|
||||||
|
@ -90,10 +90,6 @@ export function calcContentLengthLikeWord(str:string) {
|
|||||||
|
|
||||||
// 将时长转换成 时:分:秒
|
// 将时长转换成 时:分:秒
|
||||||
export function formatDuration(duration: number) {
|
export function formatDuration(duration: number) {
|
||||||
if(duration < 0 || isNaN(duration)){
|
|
||||||
return '00:00:00';
|
|
||||||
}
|
|
||||||
duration = Math.ceil(duration);
|
|
||||||
const hour = Math.floor(duration / 3600);
|
const hour = Math.floor(duration / 3600);
|
||||||
const minute = Math.floor((duration - hour * 3600) / 60);
|
const minute = Math.floor((duration - hour * 3600) / 60);
|
||||||
const second = duration - hour * 3600 - minute * 60;
|
const second = duration - hour * 3600 - minute * 60;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user