mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-04 21:39:49 +00:00
add multi select, add redownload
This commit is contained in:
@@ -79,6 +79,7 @@ class AddToDownloadQuerySerializer(serializers.Serializer):
|
|||||||
|
|
||||||
autostart = serializers.BooleanField(required=False)
|
autostart = serializers.BooleanField(required=False)
|
||||||
flat = serializers.BooleanField(required=False)
|
flat = serializers.BooleanField(required=False)
|
||||||
|
force = serializers.BooleanField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class BulkUpdateDowloadQuerySerializer(serializers.Serializer):
|
class BulkUpdateDowloadQuerySerializer(serializers.Serializer):
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class PendingList(PendingIndex):
|
|||||||
task=None,
|
task=None,
|
||||||
auto_start=False,
|
auto_start=False,
|
||||||
flat=False,
|
flat=False,
|
||||||
|
force=False,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = AppConfig().config
|
self.config = AppConfig().config
|
||||||
@@ -116,6 +117,7 @@ class PendingList(PendingIndex):
|
|||||||
self.task = task
|
self.task = task
|
||||||
self.auto_start = auto_start
|
self.auto_start = auto_start
|
||||||
self.flat = flat
|
self.flat = flat
|
||||||
|
self.force = force
|
||||||
self.to_skip = False
|
self.to_skip = False
|
||||||
self.missing_videos: list[dict] = []
|
self.missing_videos: list[dict] = []
|
||||||
self.added = 0
|
self.added = 0
|
||||||
@@ -173,10 +175,16 @@ class PendingList(PendingIndex):
|
|||||||
PendingInteract(youtube_id=url, status="priority").update_status()
|
PendingInteract(youtube_id=url, status="priority").update_status()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if url in self.missing_videos or url in self.to_skip:
|
if not self.force and (
|
||||||
|
url in self.missing_videos or url in self.to_skip
|
||||||
|
):
|
||||||
print(f"{url}: skipped adding already indexed video to download.")
|
print(f"{url}: skipped adding already indexed video to download.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self.force and url in self.all_ignored or url in self.all_pending:
|
||||||
|
print(f"{url}: skipped adding force video already in queue.")
|
||||||
|
return None
|
||||||
|
|
||||||
to_add = self._parse_video(url, vid_type)
|
to_add = self._parse_video(url, vid_type)
|
||||||
if to_add:
|
if to_add:
|
||||||
self.missing_videos.append(to_add)
|
self.missing_videos.append(to_add)
|
||||||
|
|||||||
@@ -118,12 +118,15 @@ class DownloadApiListView(ApiBaseView):
|
|||||||
|
|
||||||
auto_start = validated_query.get("autostart")
|
auto_start = validated_query.get("autostart")
|
||||||
flat = validated_query.get("flat", False)
|
flat = validated_query.get("flat", False)
|
||||||
print(f"auto_start: {auto_start}, flat: {flat}")
|
force = validated_query.get("force", False)
|
||||||
|
print(f"auto_start: {auto_start}, flat: {flat}, force: {force}")
|
||||||
to_add = validated_data["data"]
|
to_add = validated_data["data"]
|
||||||
|
|
||||||
pending = [i["youtube_id"] for i in to_add if i["status"] == "pending"]
|
pending = [i["youtube_id"] for i in to_add if i["status"] == "pending"]
|
||||||
url_str = " ".join(pending)
|
url_str = " ".join(pending)
|
||||||
task = extrac_dl.delay(url_str, auto_start=auto_start, flat=flat)
|
task = extrac_dl.delay(
|
||||||
|
url_str, auto_start=auto_start, flat=flat, force=force
|
||||||
|
)
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"message": "add to queue task started",
|
"message": "add to queue task started",
|
||||||
@@ -153,15 +156,12 @@ class DownloadApiListView(ApiBaseView):
|
|||||||
query_serializer.is_valid(raise_exception=True)
|
query_serializer.is_valid(raise_exception=True)
|
||||||
validated_query = query_serializer.validated_data
|
validated_query = query_serializer.validated_data
|
||||||
status_filter = validated_query.get("filter")
|
status_filter = validated_query.get("filter")
|
||||||
channel = validated_query.get("channel")
|
|
||||||
vid_type = validated_query.get("vid_type")
|
|
||||||
error = validated_query.get("error")
|
|
||||||
|
|
||||||
PendingInteract(status=status_filter).update_bulk(
|
PendingInteract(status=status_filter).update_bulk(
|
||||||
channel_id=channel,
|
channel_id=validated_query.get("channel"),
|
||||||
vid_type=vid_type,
|
vid_type=validated_query.get("vid_type"),
|
||||||
new_status=new_status,
|
new_status=validated_data["status"],
|
||||||
error=error,
|
error=validated_query.get("error"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if new_status == "priority":
|
if new_status == "priority":
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from celery import Task, shared_task
|
|||||||
from celery.exceptions import Retry
|
from celery.exceptions import Retry
|
||||||
from channel.src.index import YoutubeChannel
|
from channel.src.index import YoutubeChannel
|
||||||
from common.src.ta_redis import RedisArchivist
|
from common.src.ta_redis import RedisArchivist
|
||||||
from common.src.urlparser import Parser
|
from common.src.urlparser import ParsedURLType, Parser
|
||||||
from download.src.queue import PendingList
|
from download.src.queue import PendingList
|
||||||
from download.src.subscriptions import SubscriptionHandler, SubscriptionScanner
|
from download.src.subscriptions import SubscriptionHandler, SubscriptionScanner
|
||||||
from download.src.thumbnails import ThumbFilesystem, ThumbValidator
|
from download.src.thumbnails import ThumbFilesystem, ThumbValidator
|
||||||
@@ -150,8 +150,13 @@ def download_pending(self, auto_only=False):
|
|||||||
|
|
||||||
@shared_task(name="extract_download", bind=True, base=BaseTask)
|
@shared_task(name="extract_download", bind=True, base=BaseTask)
|
||||||
def extrac_dl(
|
def extrac_dl(
|
||||||
self, youtube_ids, auto_start=False, flat=False, status="pending"
|
self,
|
||||||
):
|
youtube_ids: str | list[ParsedURLType],
|
||||||
|
auto_start: bool = False,
|
||||||
|
flat: bool = False,
|
||||||
|
force: bool = False,
|
||||||
|
status: str = "pending",
|
||||||
|
) -> str | None:
|
||||||
"""parse list passed and add to pending"""
|
"""parse list passed and add to pending"""
|
||||||
TaskManager().init(self)
|
TaskManager().init(self)
|
||||||
if isinstance(youtube_ids, str):
|
if isinstance(youtube_ids, str):
|
||||||
@@ -160,7 +165,11 @@ def extrac_dl(
|
|||||||
to_add = youtube_ids
|
to_add = youtube_ids
|
||||||
|
|
||||||
pending_handler = PendingList(
|
pending_handler = PendingList(
|
||||||
youtube_ids=to_add, task=self, auto_start=auto_start, flat=flat
|
youtube_ids=to_add,
|
||||||
|
task=self,
|
||||||
|
auto_start=auto_start,
|
||||||
|
flat=flat,
|
||||||
|
force=force,
|
||||||
)
|
)
|
||||||
videos_added = pending_handler.parse_url_list(status=status)
|
videos_added = pending_handler.parse_url_list(status=status)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<svg
|
||||||
viewBox="131 -131 512 512" style="enable-background:new 131 -131 512 512;" xml:space="preserve">
|
version="1.1"
|
||||||
<g id="XMLID_2_">
|
id="Layer_1"
|
||||||
<path id="XMLID_4_" d="M640.9-116.5c3.7,10.2,2.8,18.6-4.7,25.1L456.6,87.4v270c0,10.2-4.7,17.7-14,21.4c-2.8,0.9-6.5,1.9-9.3,1.9
|
x="0px"
|
||||||
c-6.5,0-12.1-1.9-16.8-6.5L323.4,281c-4.7-4.7-6.5-10.2-6.5-16.8V87.4L138.2-91.4c-7.4-7.4-9.3-15.8-4.7-25.1
|
y="0px"
|
||||||
c3.7-9.3,11.2-14,21.4-14h464.5C629.7-131.4,636.2-126.7,640.9-116.5z"/>
|
viewBox="131 -131 512 512"
|
||||||
|
style="enable-background:new 131 -131 512 512;"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="icon-filter.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.6130873"
|
||||||
|
inkscape:cx="122.12606"
|
||||||
|
inkscape:cy="215.73537"
|
||||||
|
inkscape:window-width="2532"
|
||||||
|
inkscape:window-height="1379"
|
||||||
|
inkscape:window-x="1932"
|
||||||
|
inkscape:window-y="45"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" />
|
||||||
|
<g
|
||||||
|
id="XMLID_2_"
|
||||||
|
transform="matrix(0.71904421,0,0,0.71904421,108.73394,35.132323)">
|
||||||
|
<path
|
||||||
|
id="XMLID_4_"
|
||||||
|
d="m 640.9,-116.5 c 3.7,10.2 2.8,18.6 -4.7,25.1 L 456.6,87.4 v 270 c 0,10.2 -4.7,17.7 -14,21.4 -2.8,0.9 -6.5,1.9 -9.3,1.9 -6.5,0 -12.1,-1.9 -16.8,-6.5 L 323.4,281 c -4.7,-4.7 -6.5,-10.2 -6.5,-16.8 V 87.4 L 138.2,-91.4 c -7.4,-7.4 -9.3,-15.8 -4.7,-25.1 3.7,-9.3 11.2,-14 21.4,-14 h 464.5 c 10.3,-0.9 16.8,3.8 21.5,14 z" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 1.5 KiB |
36
frontend/public/img/icon-multi-select.svg
Normal file
36
frontend/public/img/icon-multi-select.svg
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="icon-multi-select.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="2.0722656"
|
||||||
|
inkscape:cx="255.75872"
|
||||||
|
inkscape:cy="255.75872"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1407"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="33"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<!--! Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
d="m 55.494816,230.93685 c 0,-27.64778 22.43935,-50.12629 50.126294,-50.12629 h 50.1263 v 100.25259 c 0,41.51084 32.9737,75.18944 75.18944,75.18944 h 100.25259 v 50.1263 c 0,27.64778 -22.47851,50.12629 -50.12629,50.12629 H 105.62111 c -27.686944,0 -50.126294,-22.47851 -50.126294,-50.12629 z M 230.93685,331.18944 c -27.64778,0 -50.12629,-22.47851 -50.12629,-50.12629 V 105.62111 c 0,-27.686944 22.47851,-50.126294 50.12629,-50.126294 h 175.44204 c 27.64778,0 50.12629,22.43935 50.12629,50.126294 v 175.44204 c 0,27.64778 -22.47851,50.12629 -50.12629,50.12629 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.783223" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,11 +1,18 @@
|
|||||||
import APIClient from '../../functions/APIClient';
|
import APIClient from '../../functions/APIClient';
|
||||||
|
|
||||||
const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean, flat: boolean) => {
|
type UpdateDownloadQueueType = {
|
||||||
|
youtubeIdStrings: string;
|
||||||
|
autostart?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDownloadQueue = async (params: UpdateDownloadQueueType) => {
|
||||||
const urls = [];
|
const urls = [];
|
||||||
const containsMultiple = youtubeIdStrings.includes('\n');
|
const containsMultiple = params.youtubeIdStrings.includes('\n');
|
||||||
|
|
||||||
if (containsMultiple) {
|
if (containsMultiple) {
|
||||||
const youtubeIds = youtubeIdStrings.split('\n');
|
const youtubeIds = params.youtubeIdStrings.split('\n');
|
||||||
|
|
||||||
youtubeIds.forEach(youtubeId => {
|
youtubeIds.forEach(youtubeId => {
|
||||||
if (youtubeId.trim()) {
|
if (youtubeId.trim()) {
|
||||||
@@ -13,12 +20,13 @@ const updateDownloadQueue = async (youtubeIdStrings: string, autostart: boolean,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
urls.push({ youtube_id: youtubeIdStrings, status: 'pending' });
|
urls.push({ youtube_id: params.youtubeIdStrings, status: 'pending' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
if (autostart) searchParams.append('autostart', 'true');
|
if (params.autostart === true) searchParams.append('autostart', 'true');
|
||||||
if (flat) searchParams.append('flat', 'true');
|
if (params.flat === true) searchParams.append('flat', 'true');
|
||||||
|
if (params.force === true) searchParams.append('force', 'true');
|
||||||
const endpoint = `/api/download/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
const endpoint = `/api/download/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||||
|
|
||||||
return APIClient(endpoint, {
|
return APIClient(endpoint, {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import iconGridView from '/img/icon-gridview.svg';
|
|||||||
import iconListView from '/img/icon-listview.svg';
|
import iconListView from '/img/icon-listview.svg';
|
||||||
import iconTableView from '/img/icon-tableview.svg';
|
import iconTableView from '/img/icon-tableview.svg';
|
||||||
import iconFilter from '/img/icon-filter.svg';
|
import iconFilter from '/img/icon-filter.svg';
|
||||||
|
import iconMultiSelect from '/img/icon-multi-select.svg';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
import { ViewStyleNamesType, ViewStylesEnum } from '../configuration/constants/ViewStyle';
|
import { ViewStyleNamesType, ViewStylesEnum } from '../configuration/constants/ViewStyle';
|
||||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||||
@@ -17,6 +18,9 @@ import {
|
|||||||
VideoTypes,
|
VideoTypes,
|
||||||
} from '../api/loader/loadVideoListByPage';
|
} from '../api/loader/loadVideoListByPage';
|
||||||
import { useFilterBarTempConf } from '../stores/FilterbarTempConf';
|
import { useFilterBarTempConf } from '../stores/FilterbarTempConf';
|
||||||
|
import { useVideoSelectionStore } from '../stores/VideoSelectionStore';
|
||||||
|
import Button from './Button';
|
||||||
|
import updateDownloadQueue from '../api/actions/updateDownloadQueue';
|
||||||
|
|
||||||
type FilterbarProps = {
|
type FilterbarProps = {
|
||||||
viewStyle: ViewStyleNamesType;
|
viewStyle: ViewStyleNamesType;
|
||||||
@@ -26,6 +30,14 @@ type FilterbarProps = {
|
|||||||
|
|
||||||
const Filterbar = ({ viewStyle, showSort = true, showTypeFilter = false }: FilterbarProps) => {
|
const Filterbar = ({ viewStyle, showSort = true, showTypeFilter = false }: FilterbarProps) => {
|
||||||
const { userConfig, setUserConfig } = useUserConfigStore();
|
const { userConfig, setUserConfig } = useUserConfigStore();
|
||||||
|
const {
|
||||||
|
selectedVideoIds,
|
||||||
|
clearSelected,
|
||||||
|
showSelection,
|
||||||
|
setShowSelection,
|
||||||
|
selectedAction,
|
||||||
|
setSelectedAction,
|
||||||
|
} = useVideoSelectionStore();
|
||||||
|
|
||||||
const [showHidden, setShowHidden] = useState(false);
|
const [showHidden, setShowHidden] = useState(false);
|
||||||
const { filterHeight, setFilterHeight, showFilterItems, setShowFilterItems } =
|
const { filterHeight, setFilterHeight, showFilterItems, setShowFilterItems } =
|
||||||
@@ -55,141 +67,209 @@ const Filterbar = ({ viewStyle, showSort = true, showTypeFilter = false }: Filte
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const redownloadSelected = async (ids: string[]) => {
|
||||||
|
updateDownloadQueue({
|
||||||
|
youtubeIdStrings: ids.join(' '),
|
||||||
|
autostart: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionList = [
|
||||||
|
{
|
||||||
|
label: 'Redownload',
|
||||||
|
handler: redownloadSelected,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleActionSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === '') {
|
||||||
|
setSelectedAction(null);
|
||||||
|
} else {
|
||||||
|
setSelectedAction(actionList[Number(value)].handler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectedActionRun = async () => {
|
||||||
|
if (selectedAction) {
|
||||||
|
selectedAction(selectedVideoIds);
|
||||||
|
setSelectedAction(null);
|
||||||
|
clearSelected();
|
||||||
|
setShowSelection(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="view-controls three">
|
<>
|
||||||
<div className="view-icons">
|
<div className="view-controls">
|
||||||
{showFilterItems && (
|
<div className="view-icons">
|
||||||
<>
|
<img
|
||||||
<select
|
alt="icon multi select"
|
||||||
value={userConfig.hide_watched === null ? '' : userConfig.hide_watched.toString()}
|
src={iconMultiSelect}
|
||||||
onChange={event => {
|
onClick={() => setShowSelection(!showSelection)}
|
||||||
handleUserConfigUpdate({
|
title={showSelection ? 'Hide multi select boxes' : 'Show multi select boxes'}
|
||||||
hide_watched: event.target.value === '' ? null : event.target.value === 'true',
|
/>
|
||||||
});
|
</div>
|
||||||
}}
|
<div className="view-icons">
|
||||||
>
|
{showFilterItems && (
|
||||||
<option value="">All watched state</option>
|
<>
|
||||||
<option value="true">Watched only</option>
|
<span>Filter:</span>
|
||||||
<option value="false">Unwatched only</option>
|
|
||||||
</select>
|
|
||||||
{showTypeFilter && (
|
|
||||||
<select
|
<select
|
||||||
value={userConfig.vid_type_filter === null ? '' : userConfig.vid_type_filter}
|
value={userConfig.hide_watched === null ? '' : userConfig.hide_watched.toString()}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
handleUserConfigUpdate({
|
handleUserConfigUpdate({
|
||||||
vid_type_filter:
|
hide_watched: event.target.value === '' ? null : event.target.value === 'true',
|
||||||
event.target.value === '' ? null : (event.target.value as VideoTypes),
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">All Types</option>
|
<option value="">All watched state</option>
|
||||||
<option value="videos">Videos</option>
|
<option value="true">Watched only</option>
|
||||||
<option value="streams">Streams</option>
|
<option value="false">Unwatched only</option>
|
||||||
<option value="shorts">Shorts</option>
|
|
||||||
</select>
|
</select>
|
||||||
)}
|
{showTypeFilter && (
|
||||||
<input
|
<select
|
||||||
placeholder="height in px"
|
value={userConfig.vid_type_filter === null ? '' : userConfig.vid_type_filter}
|
||||||
value={filterHeight}
|
onChange={event => {
|
||||||
onChange={e => setFilterHeight(e.target.value)}
|
handleUserConfigUpdate({
|
||||||
|
vid_type_filter:
|
||||||
|
event.target.value === '' ? null : (event.target.value as VideoTypes),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="videos">Videos</option>
|
||||||
|
<option value="streams">Streams</option>
|
||||||
|
<option value="shorts">Shorts</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
placeholder="height in px"
|
||||||
|
value={filterHeight}
|
||||||
|
onChange={e => setFilterHeight(e.target.value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={iconFilter}
|
||||||
|
alt="icon filter"
|
||||||
|
onClick={() => setShowFilterItems(!showFilterItems)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="view-icons">
|
||||||
|
{showHidden && (
|
||||||
|
<div className="sort">
|
||||||
|
<span>Sort:</span>
|
||||||
|
<select
|
||||||
|
name="sort_by"
|
||||||
|
id="sort"
|
||||||
|
value={userConfig.sort_by}
|
||||||
|
onChange={event => {
|
||||||
|
handleUserConfigUpdate({ sort_by: event.target.value as SortByType });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(SortByEnum).map(([key, value], idx) => {
|
||||||
|
return (
|
||||||
|
<option key={idx} value={value}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
name="sort_order"
|
||||||
|
id="sort-order"
|
||||||
|
value={userConfig.sort_order}
|
||||||
|
onChange={event => {
|
||||||
|
handleUserConfigUpdate({ sort_order: event.target.value as SortOrderType });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(SortOrderEnum).map(([key, value], idx) => {
|
||||||
|
return (
|
||||||
|
<option key={idx} value={value}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showSort && (
|
||||||
|
<img
|
||||||
|
src={iconSort}
|
||||||
|
alt="sort-icon"
|
||||||
|
onClick={() => {
|
||||||
|
setShowHidden(!showHidden);
|
||||||
|
}}
|
||||||
|
id="animate-icon"
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
<img
|
<div className="view-icons">
|
||||||
src={iconFilter}
|
{userConfig.grid_items !== undefined && isGridView && (
|
||||||
alt="icon filter"
|
<div className="grid-count">
|
||||||
onClick={() => setShowFilterItems(!showFilterItems)}
|
{userConfig.grid_items < 7 && (
|
||||||
/>
|
<img
|
||||||
|
src={iconAdd}
|
||||||
|
onClick={() => {
|
||||||
|
handleUserConfigUpdate({ grid_items: userConfig.grid_items + 1 });
|
||||||
|
}}
|
||||||
|
alt="grid plus row"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{userConfig.grid_items > 3 && (
|
||||||
|
<img
|
||||||
|
src={iconSubstract}
|
||||||
|
onClick={() => {
|
||||||
|
handleUserConfigUpdate({ grid_items: userConfig.grid_items - 1 });
|
||||||
|
}}
|
||||||
|
alt="grid minus row"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={iconGridView}
|
||||||
|
onClick={() => {
|
||||||
|
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.Grid });
|
||||||
|
}}
|
||||||
|
alt="grid view"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={iconListView}
|
||||||
|
onClick={() => {
|
||||||
|
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.List });
|
||||||
|
}}
|
||||||
|
alt="list view"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={iconTableView}
|
||||||
|
onClick={() => {
|
||||||
|
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.Table });
|
||||||
|
}}
|
||||||
|
alt="table view"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showSelection && (
|
||||||
{showHidden && (
|
<div className="info-box-item">
|
||||||
<div className="sort">
|
<p>
|
||||||
<div id="form">
|
Selected Videos: {selectedVideoIds.length} -{' '}
|
||||||
<span>Sort by:</span>
|
<Button onClick={clearSelected}>Clear</Button>
|
||||||
<select
|
</p>
|
||||||
name="sort_by"
|
<p>Apply action:</p>
|
||||||
id="sort"
|
<select onChange={handleActionSelectChange}>
|
||||||
value={userConfig.sort_by}
|
<option value="">---</option>
|
||||||
onChange={event => {
|
{actionList.map((actionItem, idx) => (
|
||||||
handleUserConfigUpdate({ sort_by: event.target.value as SortByType });
|
<option key={idx} value={idx}>
|
||||||
}}
|
{actionItem.label}
|
||||||
>
|
</option>
|
||||||
{Object.entries(SortByEnum).map(([key, value]) => {
|
))}
|
||||||
return <option value={value}>{key}</option>;
|
</select>
|
||||||
})}
|
{selectedAction !== null && <Button onClick={handleSelectedActionRun}>Apply</Button>}
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
name="sort_order"
|
|
||||||
id="sort-order"
|
|
||||||
value={userConfig.sort_order}
|
|
||||||
onChange={event => {
|
|
||||||
handleUserConfigUpdate({ sort_order: event.target.value as SortOrderType });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.entries(SortOrderEnum).map(([key, value]) => {
|
|
||||||
return <option value={value}>{key}</option>;
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="view-icons">
|
</>
|
||||||
{showSort && (
|
|
||||||
<img
|
|
||||||
src={iconSort}
|
|
||||||
alt="sort-icon"
|
|
||||||
onClick={() => {
|
|
||||||
setShowHidden(!showHidden);
|
|
||||||
}}
|
|
||||||
id="animate-icon"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{userConfig.grid_items !== undefined && isGridView && (
|
|
||||||
<div className="grid-count">
|
|
||||||
{userConfig.grid_items < 7 && (
|
|
||||||
<img
|
|
||||||
src={iconAdd}
|
|
||||||
onClick={() => {
|
|
||||||
handleUserConfigUpdate({ grid_items: userConfig.grid_items + 1 });
|
|
||||||
}}
|
|
||||||
alt="grid plus row"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{userConfig.grid_items > 3 && (
|
|
||||||
<img
|
|
||||||
src={iconSubstract}
|
|
||||||
onClick={() => {
|
|
||||||
handleUserConfigUpdate({ grid_items: userConfig.grid_items - 1 });
|
|
||||||
}}
|
|
||||||
alt="grid minus row"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<img
|
|
||||||
src={iconGridView}
|
|
||||||
onClick={() => {
|
|
||||||
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.Grid });
|
|
||||||
}}
|
|
||||||
alt="grid view"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src={iconListView}
|
|
||||||
onClick={() => {
|
|
||||||
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.List });
|
|
||||||
}}
|
|
||||||
alt="list view"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src={iconTableView}
|
|
||||||
onClick={() => {
|
|
||||||
handleUserConfigUpdate({ [viewStyle]: ViewStylesEnum.Table });
|
|
||||||
}}
|
|
||||||
alt="table view"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { VideoType } from '../pages/Home';
|
|||||||
import iconPlay from '/img/icon-play.svg';
|
import iconPlay from '/img/icon-play.svg';
|
||||||
import iconDotMenu from '/img/icon-dot-menu.svg';
|
import iconDotMenu from '/img/icon-dot-menu.svg';
|
||||||
import iconClose from '/img/icon-close.svg';
|
import iconClose from '/img/icon-close.svg';
|
||||||
|
import iconChecked from '/img/icon-seen.svg';
|
||||||
|
import iconUnchecked from '/img/icon-unseen.svg';
|
||||||
import updateWatchedState from '../api/actions/updateWatchedState';
|
import updateWatchedState from '../api/actions/updateWatchedState';
|
||||||
import formatDate from '../functions/formatDates';
|
import formatDate from '../functions/formatDates';
|
||||||
import WatchedCheckBox from './WatchedCheckBox';
|
import WatchedCheckBox from './WatchedCheckBox';
|
||||||
@@ -12,6 +14,7 @@ import { useState } from 'react';
|
|||||||
import deleteVideoProgressById from '../api/actions/deleteVideoProgressById';
|
import deleteVideoProgressById from '../api/actions/deleteVideoProgressById';
|
||||||
import VideoThumbnail from './VideoThumbail';
|
import VideoThumbnail from './VideoThumbail';
|
||||||
import { ViewStylesType } from '../configuration/constants/ViewStyle';
|
import { ViewStylesType } from '../configuration/constants/ViewStyle';
|
||||||
|
import { useVideoSelectionStore } from '../stores/VideoSelectionStore';
|
||||||
|
|
||||||
type VideoListItemProps = {
|
type VideoListItemProps = {
|
||||||
video: VideoType;
|
video: VideoType;
|
||||||
@@ -31,6 +34,8 @@ const VideoListItem = ({
|
|||||||
const [, setSearchParams] = useSearchParams();
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [showReorderMenu, setShowReorderMenu] = useState(false);
|
const [showReorderMenu, setShowReorderMenu] = useState(false);
|
||||||
|
const { selectedVideoIds, appendVideoId, removeVideoId, showSelection } =
|
||||||
|
useVideoSelectionStore();
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return <p>No video found.</p>;
|
return <p>No video found.</p>;
|
||||||
@@ -38,6 +43,19 @@ const VideoListItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`video-item ${viewStyle}`}>
|
<div className={`video-item ${viewStyle}`}>
|
||||||
|
{showSelection && (
|
||||||
|
<div className="video-item-select-wrapper">
|
||||||
|
<img
|
||||||
|
src={selectedVideoIds.includes(video.youtube_id) ? iconChecked : iconUnchecked}
|
||||||
|
className="video-item-select"
|
||||||
|
onClick={() =>
|
||||||
|
selectedVideoIds.includes(video.youtube_id)
|
||||||
|
? removeVideoId(video.youtube_id)
|
||||||
|
: appendVideoId(video.youtube_id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchParams(params => {
|
setSearchParams(params => {
|
||||||
@@ -75,28 +93,32 @@ const VideoListItem = ({
|
|||||||
</a>
|
</a>
|
||||||
<div className={`video-desc ${viewStyle}`}>
|
<div className={`video-desc ${viewStyle}`}>
|
||||||
<div className="video-desc-player" id={`video-info-${video.youtube_id}`}>
|
<div className="video-desc-player" id={`video-info-${video.youtube_id}`}>
|
||||||
<WatchedCheckBox
|
{!showSelection && (
|
||||||
watched={video.player.watched}
|
<>
|
||||||
onClick={async status => {
|
<WatchedCheckBox
|
||||||
await updateWatchedState({
|
watched={video.player.watched}
|
||||||
id: video.youtube_id,
|
onClick={async status => {
|
||||||
is_watched: status,
|
await updateWatchedState({
|
||||||
});
|
id: video.youtube_id,
|
||||||
}}
|
is_watched: status,
|
||||||
onDone={() => {
|
});
|
||||||
refreshVideoList(true);
|
}}
|
||||||
}}
|
onDone={() => {
|
||||||
/>
|
refreshVideoList(true);
|
||||||
{video.player.progress && (
|
}}
|
||||||
<img
|
/>
|
||||||
src={iconClose}
|
{video.player.progress && (
|
||||||
className="video-popup-menu-close-button"
|
<img
|
||||||
title="Delete watch progress"
|
src={iconClose}
|
||||||
onClick={async () => {
|
className="video-popup-menu-close-button"
|
||||||
await deleteVideoProgressById(video.youtube_id);
|
title="Delete watch progress"
|
||||||
refreshVideoList(true);
|
onClick={async () => {
|
||||||
}}
|
await deleteVideoProgressById(video.youtube_id);
|
||||||
/>
|
refreshVideoList(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{formatDate(video.published)} | {video.player.duration_str}
|
{formatDate(video.published)} | {video.player.duration_str}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { ViewStylesType } from '../configuration/constants/ViewStyle';
|
|||||||
import humanFileSize from '../functions/humanFileSize';
|
import humanFileSize from '../functions/humanFileSize';
|
||||||
import { FileSizeUnits } from '../api/actions/updateUserConfig';
|
import { FileSizeUnits } from '../api/actions/updateUserConfig';
|
||||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||||
|
import { useVideoSelectionStore } from '../stores/VideoSelectionStore';
|
||||||
|
import iconChecked from '/img/icon-seen.svg';
|
||||||
|
import iconUnchecked from '/img/icon-unseen.svg';
|
||||||
|
|
||||||
const StreamsTypeEmun = {
|
const StreamsTypeEmun = {
|
||||||
Video: 'video',
|
Video: 'video',
|
||||||
@@ -18,6 +21,8 @@ type VideoListItemProps = {
|
|||||||
|
|
||||||
const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => {
|
const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => {
|
||||||
const { userConfig } = useUserConfigStore();
|
const { userConfig } = useUserConfigStore();
|
||||||
|
const { showSelection, selectedVideoIds, removeVideoId, appendVideoId } =
|
||||||
|
useVideoSelectionStore();
|
||||||
|
|
||||||
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
||||||
|
|
||||||
@@ -26,8 +31,9 @@ const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Channel</th>
|
{showSelection && <th />}
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
|
<th>Channel</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Resolution</th>
|
<th>Resolution</th>
|
||||||
<th>Media size</th>
|
<th>Media size</th>
|
||||||
@@ -45,12 +51,25 @@ const VideoListItemTable = ({ videoList, viewStyle }: VideoListItemProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={youtube_id}>
|
<tr key={youtube_id}>
|
||||||
<td className="no-nowrap">
|
{showSelection && (
|
||||||
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
|
<td>
|
||||||
</td>
|
<img
|
||||||
|
src={selectedVideoIds.includes(youtube_id) ? iconChecked : iconUnchecked}
|
||||||
|
className="video-item-select"
|
||||||
|
onClick={() =>
|
||||||
|
selectedVideoIds.includes(youtube_id)
|
||||||
|
? removeVideoId(youtube_id)
|
||||||
|
: appendVideoId(youtube_id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
<td className="no-nowrap title">
|
<td className="no-nowrap title">
|
||||||
<Link to={Routes.Video(youtube_id)}>{title}</Link>
|
<Link to={Routes.Video(youtube_id)}>{title}</Link>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="no-nowrap">
|
||||||
|
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
|
||||||
|
</td>
|
||||||
<td>{vid_type}</td>
|
<td>{vid_type}</td>
|
||||||
<td>{`${videoStream?.width || '-'}x${videoStream?.height || '-'}`}</td>
|
<td>{`${videoStream?.width || '-'}x${videoStream?.height || '-'}`}</td>
|
||||||
<td>{humanFileSize(media_size, useSiUnits)}</td>
|
<td>{humanFileSize(media_size, useSiUnits)}</td>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
|
|||||||
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
||||||
|
|
||||||
const viewStyle = userConfig.view_style_home;
|
const viewStyle = userConfig.view_style_home;
|
||||||
const isGridView = viewStyle === ViewStylesEnum.Grid;
|
const isGridView = viewStyle === ViewStylesEnum.Grid || ViewStylesEnum.Table;
|
||||||
const gridView = isGridView ? `boxed-${userConfig.grid_items}` : '';
|
const gridView = isGridView ? `boxed-${userConfig.grid_items}` : '';
|
||||||
const gridViewGrid = isGridView ? `grid-${userConfig.grid_items}` : '';
|
const gridViewGrid = isGridView ? `grid-${userConfig.grid_items}` : '';
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const Download = () => {
|
|||||||
const [showHiddenForm, setShowHiddenForm] = useState(false);
|
const [showHiddenForm, setShowHiddenForm] = useState(false);
|
||||||
const [addAsAutoStart, setAddAsAutoStart] = useState(false);
|
const [addAsAutoStart, setAddAsAutoStart] = useState(false);
|
||||||
const [addAsFlat, setAddAsFlat] = useState(false);
|
const [addAsFlat, setAddAsFlat] = useState(false);
|
||||||
|
const [addAsForce, setAddAsForce] = useState(false);
|
||||||
const [showQueueActions, setShowQueueActions] = useState(false);
|
const [showQueueActions, setShowQueueActions] = useState(false);
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState('');
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
@@ -249,6 +250,26 @@ const Download = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span>Fast add</span>
|
<span>Fast add</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="toggle">
|
||||||
|
<div className="toggleBox">
|
||||||
|
<input
|
||||||
|
id="add_force"
|
||||||
|
type="checkbox"
|
||||||
|
checked={addAsForce}
|
||||||
|
onChange={() => setAddAsForce(!addAsForce)}
|
||||||
|
/>
|
||||||
|
{addAsForce ? (
|
||||||
|
<label htmlFor="" className="onbtn">
|
||||||
|
On
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<label htmlFor="" className="ofbtn">
|
||||||
|
Off
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span>Re-Download</span>
|
||||||
|
</div>
|
||||||
<div className="toggle">
|
<div className="toggle">
|
||||||
<div className="toggleBox">
|
<div className="toggleBox">
|
||||||
<input
|
<input
|
||||||
@@ -280,7 +301,12 @@ const Download = () => {
|
|||||||
label="Add to queue"
|
label="Add to queue"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (downloadQueueText.trim()) {
|
if (downloadQueueText.trim()) {
|
||||||
await updateDownloadQueue(downloadQueueText, addAsAutoStart, addAsFlat);
|
await updateDownloadQueue({
|
||||||
|
youtubeIdStrings: downloadQueueText,
|
||||||
|
autostart: addAsAutoStart,
|
||||||
|
flat: addAsFlat,
|
||||||
|
force: addAsForce,
|
||||||
|
});
|
||||||
setDownloadQueueText('');
|
setDownloadQueueText('');
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
setShowHiddenForm(false);
|
setShowHiddenForm(false);
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const Home = () => {
|
|||||||
|
|
||||||
const hasVideos = videoResponseData?.data?.length !== 0;
|
const hasVideos = videoResponseData?.data?.length !== 0;
|
||||||
|
|
||||||
const isGridView = userConfig.view_style_home === ViewStylesEnum.Grid;
|
const isGridView = userConfig.view_style_home === ViewStylesEnum.Grid || ViewStylesEnum.Table;
|
||||||
const gridView = isGridView ? `boxed-${userConfig.grid_items}` : '';
|
const gridView = isGridView ? `boxed-${userConfig.grid_items}` : '';
|
||||||
const gridViewGrid = isGridView ? `grid-${userConfig.grid_items}` : '';
|
const gridViewGrid = isGridView ? `grid-${userConfig.grid_items}` : '';
|
||||||
const isTableView = userConfig.view_style_home === ViewStylesEnum.Table;
|
const isTableView = userConfig.view_style_home === ViewStylesEnum.Table;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const Playlist = () => {
|
|||||||
|
|
||||||
const viewStyle = userConfig.view_style_home; // its a list of videos, so view_style_home
|
const viewStyle = userConfig.view_style_home; // its a list of videos, so view_style_home
|
||||||
const gridItems = userConfig.grid_items;
|
const gridItems = userConfig.grid_items;
|
||||||
const isGridView = viewStyle === ViewStylesEnum.Grid;
|
const isGridView = viewStyle === ViewStylesEnum.Grid || ViewStylesEnum.Table;
|
||||||
const gridView = isGridView ? `boxed-${gridItems}` : '';
|
const gridView = isGridView ? `boxed-${gridItems}` : '';
|
||||||
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
|
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
|
||||||
|
|
||||||
|
|||||||
37
frontend/src/stores/VideoSelectionStore.ts
Normal file
37
frontend/src/stores/VideoSelectionStore.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface SelectionState {
|
||||||
|
selectedVideoIds: string[];
|
||||||
|
appendVideoId: (id: string) => void;
|
||||||
|
removeVideoId: (id: string) => void;
|
||||||
|
clearSelected: () => void;
|
||||||
|
showSelection: boolean;
|
||||||
|
setShowSelection: (showSelection: boolean) => void;
|
||||||
|
selectedAction: ((ids: string[]) => void) | null;
|
||||||
|
setSelectedAction: (fn: ((ids: string[]) => void) | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVideoSelectionStore = create<SelectionState>(set => ({
|
||||||
|
selectedVideoIds: [],
|
||||||
|
|
||||||
|
appendVideoId: id =>
|
||||||
|
set(state => {
|
||||||
|
if (state.selectedVideoIds.includes(id)) {
|
||||||
|
return state; // avoid duplicates
|
||||||
|
}
|
||||||
|
return { selectedVideoIds: [...state.selectedVideoIds, id] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeVideoId: id =>
|
||||||
|
set(state => ({
|
||||||
|
selectedVideoIds: state.selectedVideoIds.filter(item => item !== id),
|
||||||
|
})),
|
||||||
|
|
||||||
|
clearSelected: () => set({ selectedVideoIds: [] }),
|
||||||
|
|
||||||
|
showSelection: false,
|
||||||
|
setShowSelection: (showSelection: boolean) => set({ showSelection }),
|
||||||
|
|
||||||
|
selectedAction: null,
|
||||||
|
setSelectedAction: fn => set({ selectedAction: fn }),
|
||||||
|
}));
|
||||||
@@ -178,6 +178,11 @@ button:disabled:hover {
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-item.table td img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.button-box {
|
.button-box {
|
||||||
padding: 5px 2px;
|
padding: 5px 2px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -423,8 +428,9 @@ button:disabled:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.view-controls {
|
.view-controls {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr auto auto;
|
flex-wrap: wrap;
|
||||||
|
justify-content: end;
|
||||||
border-bottom: 2px solid;
|
border-bottom: 2px solid;
|
||||||
border-color: var(--accent-font-dark);
|
border-color: var(--accent-font-dark);
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
@@ -433,6 +439,7 @@ button:disabled:hover {
|
|||||||
.view-icons,
|
.view-icons,
|
||||||
.grid-count {
|
.grid-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -575,6 +582,7 @@ video:-webkit-full-screen {
|
|||||||
|
|
||||||
.video-item {
|
.video-item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-item:hover .video-tags {
|
.video-item:hover .video-tags {
|
||||||
@@ -588,6 +596,23 @@ video:-webkit-full-screen {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-item-select-wrapper {
|
||||||
|
background-color: var(--highlight-bg);
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item-select {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
filter: var(--img-filter);
|
||||||
|
}
|
||||||
|
|
||||||
.video-progress-bar,
|
.video-progress-bar,
|
||||||
.notification-progress-bar {
|
.notification-progress-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user