-
-
Show subscribed only:
-
- {
- handleUserConfigUpdate({ show_subed_only: !userConfig.show_subed_only });
- setRefresh(true);
- }}
- type="checkbox"
- checked={userConfig.show_subed_only}
- />
- {!userConfig.show_subed_only && (
-
- )}
- {userConfig.show_subed_only && (
-
- )}
-
-
+ {showFilterItems && (
+
+ Filter:
+
+
+ )}
+

setShowFilterItems(!showFilterItems)}
+ />
+

{
diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx
index 5a2cd271..c06b9d21 100644
--- a/frontend/src/pages/Download.tsx
+++ b/frontend/src/pages/Download.tsx
@@ -64,6 +64,7 @@ const Download = () => {
const [showHiddenForm, setShowHiddenForm] = useState(false);
const [addAsAutoStart, setAddAsAutoStart] = useState(false);
const [addAsFlat, setAddAsFlat] = useState(false);
+ const [addAsForce, setAddAsForce] = useState(false);
const [showQueueActions, setShowQueueActions] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -249,6 +250,26 @@ const Download = () => {
Fast add
+
{
label="Add to queue"
onClick={async () => {
if (downloadQueueText.trim()) {
- await updateDownloadQueue(downloadQueueText, addAsAutoStart, addAsFlat);
+ await updateDownloadQueue({
+ youtubeIdStrings: downloadQueueText,
+ autostart: addAsAutoStart,
+ flat: addAsFlat,
+ force: addAsForce,
+ });
setDownloadQueueText('');
setRefresh(true);
setShowHiddenForm(false);
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index e7b1c8d8..391c29f0 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -4,6 +4,7 @@ import Routes from '../configuration/routes/RouteList';
import Pagination from '../components/Pagination';
import loadVideoListByFilter, {
VideoListByFilterResponseType,
+ VideoTypes,
WatchTypes,
WatchTypesEnum,
} from '../api/loader/loadVideoListByPage';
@@ -21,6 +22,7 @@ import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import { SponsorBlockType } from './Video';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiResponseType } from '../functions/APIClient';
+import { useFilterBarTempConf } from '../stores/FilterbarTempConf';
export type PlayerType = {
watched: boolean;
@@ -117,6 +119,8 @@ const Home = () => {
const [continueVideoResponse, setContinueVideoResponse] =
useState
>();
+ const { filterHeight } = useFilterBarTempConf();
+
const { data: videoResponseData } = videoResponse ?? {};
const { data: continueVideoResponseData } = continueVideoResponse ?? {};
@@ -126,7 +130,7 @@ const Home = () => {
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 gridViewGrid = isGridView ? `grid-${userConfig.grid_items}` : '';
const isTableView = userConfig.view_style_home === ViewStylesEnum.Table;
@@ -135,9 +139,16 @@ const Home = () => {
(async () => {
const videos = await loadVideoListByFilter({
page: currentPage,
- watch: userConfig.hide_watched ? (WatchTypesEnum.Unwatched as WatchTypes) : undefined,
+ watch:
+ userConfig.hide_watched === null
+ ? null
+ : ((userConfig.hide_watched
+ ? WatchTypesEnum.Watched
+ : WatchTypesEnum.Unwatched) as WatchTypes),
+ type: userConfig.vid_type_filter as VideoTypes,
sort: userConfig.sort_by,
order: userConfig.sort_order,
+ height: filterHeight,
});
try {
@@ -157,6 +168,8 @@ const Home = () => {
userConfig.sort_by,
userConfig.sort_order,
userConfig.hide_watched,
+ userConfig.vid_type_filter,
+ filterHeight,
currentPage,
pagination?.current_page,
videoId,
@@ -189,10 +202,7 @@ const Home = () => {
Recent Videos
-
+
diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx
index 9bcb4c45..e0c7511e 100644
--- a/frontend/src/pages/Playlist.tsx
+++ b/frontend/src/pages/Playlist.tsx
@@ -24,6 +24,7 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import Button from '../components/Button';
import loadVideoListByFilter, {
+ VideoTypes,
WatchTypes,
WatchTypesEnum,
} from '../api/loader/loadVideoListByPage';
@@ -32,6 +33,7 @@ import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiResponseType } from '../functions/APIClient';
import NotFound from './NotFound';
import updatePlaylistSortOrder from '../api/actions/updatePlaylistSortOrder';
+import { useFilterBarTempConf } from '../stores/FilterbarTempConf';
export type VideoResponseType = {
data?: VideoType[];
@@ -45,6 +47,7 @@ const Playlist = () => {
const videoId = searchParams.get('videoId');
const { userConfig } = useUserConfigStore();
+ const { filterHeight } = useFilterBarTempConf();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = useIsAdmin();
@@ -72,8 +75,7 @@ const Playlist = () => {
const viewStyle = userConfig.view_style_home; // its a list of videos, so view_style_home
const gridItems = userConfig.grid_items;
- const hideWatched = userConfig.hide_watched;
- const isGridView = viewStyle === ViewStylesEnum.Grid;
+ const isGridView = viewStyle === ViewStylesEnum.Grid || ViewStylesEnum.Table;
const gridView = isGridView ? `boxed-${gridItems}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
@@ -83,7 +85,14 @@ const Playlist = () => {
const video = await loadVideoListByFilter({
playlist: playlistId,
page: currentPage,
- watch: hideWatched ? (WatchTypesEnum.Unwatched as WatchTypes) : undefined,
+ watch:
+ userConfig.hide_watched === null
+ ? null
+ : ((userConfig.hide_watched
+ ? WatchTypesEnum.Watched
+ : WatchTypesEnum.Unwatched) as WatchTypes),
+ type: userConfig.vid_type_filter as VideoTypes,
+ height: filterHeight,
});
setPlaylistResponse(playlist);
@@ -102,6 +111,8 @@ const Playlist = () => {
}, [
playlistId,
userConfig.hide_watched,
+ userConfig.vid_type_filter,
+ filterHeight,
refresh,
currentPage,
pagination?.current_page,
@@ -339,9 +350,9 @@ const Playlist = () => {
diff --git a/frontend/src/pages/Playlists.tsx b/frontend/src/pages/Playlists.tsx
index 91820c97..d158a0b6 100644
--- a/frontend/src/pages/Playlists.tsx
+++ b/frontend/src/pages/Playlists.tsx
@@ -4,6 +4,7 @@ import { useOutletContext } from 'react-router-dom';
import iconAdd from '/img/icon-add.svg';
import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg';
+import iconFilter from '/img/icon-filter.svg';
import { OutletContextType } from './Base';
import loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList';
@@ -28,6 +29,7 @@ const Playlists = () => {
const [showAddForm, setShowAddForm] = useState(false);
const [refresh, setRefresh] = useState(false);
const [showNotification, setShowNotification] = useState(false);
+ const [showFilterItems, setShowFilterItems] = useState(false);
const [playlistsToAddText, setPlaylistsToAddText] = useState('');
const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
@@ -39,7 +41,7 @@ const Playlists = () => {
const pagination = playlistResponseData?.paginate;
const viewStyle = userConfig.view_style_playlist;
- const showSubedOnly = userConfig.show_subed_only;
+ const showSubedOnly = userConfig.show_subed_only_playlists;
useEffect(() => {
(async () => {
@@ -53,7 +55,7 @@ const Playlists = () => {
setShowNotification(false);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [refresh, userConfig.show_subed_only, currentPage, pagination?.current_page]);
+ }, [refresh, userConfig.show_subed_only_playlists, currentPage, pagination?.current_page]);
const handleUserConfigUpdate = async (config: Partial
) => {
const updatedUserConfig = await updateUserConfig(config);
@@ -150,29 +152,35 @@ const Playlists = () => {
/>
-
-
Show subscribed only:
-
- {
- handleUserConfigUpdate({ show_subed_only: !showSubedOnly });
- }}
- type="checkbox"
- />
- {!showSubedOnly && (
-
- )}
- {showSubedOnly && (
-
- )}
-
-
+ {showFilterItems && (
+
+ Filter:
+
+
+ )}
+

setShowFilterItems(!showFilterItems)}
+ />

{
diff --git a/frontend/src/stores/FilterbarTempConf.ts b/frontend/src/stores/FilterbarTempConf.ts
new file mode 100644
index 00000000..3f32113e
--- /dev/null
+++ b/frontend/src/stores/FilterbarTempConf.ts
@@ -0,0 +1,17 @@
+// temporary values for filtering, not stored in backend
+
+import { create } from 'zustand';
+
+interface FilterbarTempConfInterface {
+ filterHeight: string;
+ setFilterHeight: (filterHeight: string) => void;
+ showFilterItems: boolean;
+ setShowFilterItems: (filterItems: boolean) => void;
+}
+
+export const useFilterBarTempConf = create
(set => ({
+ filterHeight: '',
+ setFilterHeight: (filterHeight: string) => set({ filterHeight }),
+ showFilterItems: false,
+ setShowFilterItems: (showFilterItems: boolean) => set({ showFilterItems }),
+}));
diff --git a/frontend/src/stores/UserConfigStore.ts b/frontend/src/stores/UserConfigStore.ts
index 077715c5..a04b1bc1 100644
--- a/frontend/src/stores/UserConfigStore.ts
+++ b/frontend/src/stores/UserConfigStore.ts
@@ -18,11 +18,13 @@ export const useUserConfigStore = create(set => ({
view_style_channel: ViewStylesEnum.List as ViewStylesType,
view_style_downloads: ViewStylesEnum.List as ViewStylesType,
view_style_playlist: ViewStylesEnum.Grid as ViewStylesType,
+ vid_type_filter: null,
grid_items: 3,
hide_watched: false,
file_size_unit: 'binary',
show_ignored_only: false,
- show_subed_only: false,
+ show_subed_only: null,
+ show_subed_only_playlists: null,
show_help_text: true,
},
setUserConfig: userConfig => set({ userConfig }),
diff --git a/frontend/src/stores/VideoSelectionStore.ts b/frontend/src/stores/VideoSelectionStore.ts
new file mode 100644
index 00000000..556f2e6d
--- /dev/null
+++ b/frontend/src/stores/VideoSelectionStore.ts
@@ -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(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 }),
+}));
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 2760a356..55debd37 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -178,6 +178,11 @@ button:disabled:hover {
min-width: 100px;
}
+.video-item.table td img {
+ width: 20px;
+ height: 20px;
+}
+
.button-box {
padding: 5px 2px;
display: inline-flex;
@@ -433,6 +438,7 @@ button:disabled:hover {
.view-icons,
.grid-count {
display: flex;
+ flex-wrap: wrap;
justify-content: end;
align-items: center;
}
@@ -575,6 +581,7 @@ video:-webkit-full-screen {
.video-item {
overflow: hidden;
+ position: relative;
}
.video-item:hover .video-tags {
@@ -588,6 +595,23 @@ video:-webkit-full-screen {
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,
.notification-progress-bar {
position: absolute;