diff --git a/backend/channel/serializers.py b/backend/channel/serializers.py index 81464957..1f364a97 100644 --- a/backend/channel/serializers.py +++ b/backend/channel/serializers.py @@ -64,7 +64,9 @@ class ChannelListSerializer(serializers.Serializer): class ChannelListQuerySerializer(serializers.Serializer): """serialize list query""" - filter = serializers.ChoiceField(choices=["subscribed"], required=False) + filter = serializers.ChoiceField( + choices=["subscribed", "unsubscribed"], required=False + ) page = serializers.IntegerField(required=False) diff --git a/backend/channel/views.py b/backend/channel/views.py index 1f0daf57..204aaf73 100644 --- a/backend/channel/views.py +++ b/backend/channel/views.py @@ -51,8 +51,11 @@ class ChannelApiListView(ApiBaseView): must_list = [] query_filter = validated_data.get("filter") - if query_filter: - must_list.append({"term": {"channel_subscribed": {"value": True}}}) + if query_filter is not None: + channel_subscribed = query_filter == "subscribed" + must_list.append( + {"term": {"channel_subscribed": {"value": channel_subscribed}}} + ) self.data["query"] = {"bool": {"must": must_list}} self.get_document_list(request) diff --git a/backend/playlist/serializers.py b/backend/playlist/serializers.py index 741c770d..cb2a12c7 100644 --- a/backend/playlist/serializers.py +++ b/backend/playlist/serializers.py @@ -46,7 +46,7 @@ class PlaylistListQuerySerializer(serializers.Serializer): """serialize playlist list query params""" channel = serializers.CharField(required=False) - subscribed = serializers.BooleanField(required=False) + subscribed = serializers.BooleanField(required=False, allow_null=True) type = serializers.ChoiceField( choices=["regular", "custom"], required=False ) diff --git a/backend/playlist/src/query_building.py b/backend/playlist/src/query_building.py index 3f5593e5..71374ee6 100644 --- a/backend/playlist/src/query_building.py +++ b/backend/playlist/src/query_building.py @@ -26,7 +26,7 @@ class QueryBuilder: must_list.append({"match": {"playlist_channel_id": channel}}) subscribed = self.request_params.get("subscribed") - if subscribed: + if subscribed is not None: must_list.append({"match": {"playlist_subscribed": subscribed}}) playlist_type = self.request_params.get("type") diff --git a/backend/user/serializers.py b/backend/user/serializers.py index 8f5cbd51..dea69916 100644 --- a/backend/user/serializers.py +++ b/backend/user/serializers.py @@ -44,7 +44,8 @@ class UserMeConfigSerializer(serializers.Serializer): hide_watched = serializers.BooleanField(allow_null=True) file_size_unit = serializers.ChoiceField(choices=["binary", "metric"]) show_ignored_only = serializers.BooleanField() - show_subed_only = serializers.BooleanField() + show_subed_only = serializers.BooleanField(allow_null=True) + show_subed_only_playlists = serializers.BooleanField(allow_null=True) show_help_text = serializers.BooleanField() diff --git a/backend/user/src/user_config.py b/backend/user/src/user_config.py index 137fa681..5037c390 100644 --- a/backend/user/src/user_config.py +++ b/backend/user/src/user_config.py @@ -25,7 +25,8 @@ class UserConfigType(TypedDict, total=False): hide_watched: bool | None file_size_unit: str show_ignored_only: bool - show_subed_only: bool + show_subed_only: bool | None + show_subed_only_playlists: bool | None show_help_text: bool @@ -49,7 +50,8 @@ class UserConfig: hide_watched=False, file_size_unit="binary", show_ignored_only=False, - show_subed_only=False, + show_subed_only=None, + show_subed_only_playlists=None, show_help_text=True, ) diff --git a/frontend/src/api/actions/updateUserConfig.ts b/frontend/src/api/actions/updateUserConfig.ts index 2f2a3a4e..d9a4b7f8 100644 --- a/frontend/src/api/actions/updateUserConfig.ts +++ b/frontend/src/api/actions/updateUserConfig.ts @@ -36,7 +36,8 @@ export type UserConfigType = { hide_watched: boolean | null; file_size_unit: 'binary' | 'metric'; show_ignored_only: boolean; - show_subed_only: boolean; + show_subed_only: boolean | null; + show_subed_only_playlists: boolean | null; show_help_text: boolean; }; diff --git a/frontend/src/api/loader/loadChannelList.ts b/frontend/src/api/loader/loadChannelList.ts index 928fe911..a46a9574 100644 --- a/frontend/src/api/loader/loadChannelList.ts +++ b/frontend/src/api/loader/loadChannelList.ts @@ -9,11 +9,13 @@ export type ChannelsListResponse = { config?: ConfigType; }; -const loadChannelList = async (page: number, showSubscribed: boolean) => { +const loadChannelList = async (page: number, showSubscribed: boolean | null) => { const searchParams = new URLSearchParams(); if (page) searchParams.append('page', page.toString()); - if (showSubscribed) searchParams.append('filter', 'subscribed'); + if (showSubscribed !== null) { + searchParams.append('filter', showSubscribed ? 'subscribed' : 'unsubscribed'); + } const endpoint = `/api/channel/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; diff --git a/frontend/src/api/loader/loadPlaylistList.ts b/frontend/src/api/loader/loadPlaylistList.ts index d03c9d91..b2cb31f7 100644 --- a/frontend/src/api/loader/loadPlaylistList.ts +++ b/frontend/src/api/loader/loadPlaylistList.ts @@ -12,7 +12,7 @@ type PlaylistCategoryType = 'regular' | 'custom'; type LoadPlaylistListProps = { channel?: string; page?: number | undefined; - subscribed?: boolean; + subscribed?: boolean | null; type?: PlaylistCategoryType; }; @@ -21,7 +21,8 @@ const loadPlaylistList = async ({ channel, page, subscribed, type }: LoadPlaylis if (channel) searchParams.append('channel', channel); if (page) searchParams.append('page', page.toString()); - if (subscribed) searchParams.append('subscribed', subscribed.toString()); + if (subscribed !== undefined && subscribed !== null) + searchParams.append('subscribed', subscribed.toString()); if (type) searchParams.append('type', type); const endpoint = `/api/playlist/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; diff --git a/frontend/src/pages/ChannelPlaylist.tsx b/frontend/src/pages/ChannelPlaylist.tsx index 03597913..efb87bf7 100644 --- a/frontend/src/pages/ChannelPlaylist.tsx +++ b/frontend/src/pages/ChannelPlaylist.tsx @@ -7,6 +7,7 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop'; import loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList'; import iconGridView from '/img/icon-gridview.svg'; import iconListView from '/img/icon-listview.svg'; +import iconFilter from '/img/icon-filter.svg'; import { useUserConfigStore } from '../stores/UserConfigStore'; import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig'; import { ApiResponseType } from '../functions/APIClient'; @@ -18,6 +19,7 @@ const ChannelPlaylist = () => { const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const [refreshPlaylists, setRefreshPlaylists] = useState(false); + const [showFilterItems, setShowFilterItems] = useState(false); const [playlistsResponse, setPlaylistsResponse] = useState>(); @@ -28,7 +30,7 @@ const ChannelPlaylist = () => { const pagination = playlistsResponseData?.paginate; const viewStyle = userConfig.view_style_playlist; - const showSubedOnly = userConfig.show_subed_only; + const showSubedOnly = userConfig.show_subed_only_playlists; const handleUserConfigUpdate = async (config: Partial) => { const updatedUserConfig = await updateUserConfig(config); @@ -58,30 +60,35 @@ const ChannelPlaylist = () => {
-
- Show subscribed only: -
- { - handleUserConfigUpdate({ show_subed_only: !showSubedOnly }); - setRefreshPlaylists(true); - }} - type="checkbox" - /> - {!showSubedOnly && ( - - )} - {showSubedOnly && ( - - )} -
-
+ {showFilterItems && ( +
+ Filter: + +
+ )} + icon filter setShowFilterItems(!showFilterItems)} + /> { diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 4477d363..2a6fb5f3 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -3,6 +3,7 @@ import loadChannelList, { ChannelsListResponse } from '../api/loader/loadChannel import iconGridView from '/img/icon-gridview.svg'; import iconListView from '/img/icon-listview.svg'; import iconAdd from '/img/icon-add.svg'; +import iconFilter from '/img/icon-filter.svg'; import { useEffect, useState } from 'react'; import Pagination from '../components/Pagination'; import { OutletContextType } from './Base'; @@ -52,6 +53,7 @@ const Channels = () => { useState>(); const [showAddForm, setShowAddForm] = useState(false); const [refresh, setRefresh] = useState(true); + const [showFilterItems, setShowFilterItems] = useState(false); const [showNotification, setShowNotification] = useState(false); const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState(''); @@ -144,31 +146,34 @@ const Channels = () => { />
-
- 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: + +
+ )} + icon filter setShowFilterItems(!showFilterItems)} + /> + { 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: + +
+ )} + icon filter setShowFilterItems(!showFilterItems)} + /> { diff --git a/frontend/src/stores/UserConfigStore.ts b/frontend/src/stores/UserConfigStore.ts index e4d2c812..a04b1bc1 100644 --- a/frontend/src/stores/UserConfigStore.ts +++ b/frontend/src/stores/UserConfigStore.ts @@ -23,7 +23,8 @@ export const useUserConfigStore = create(set => ({ 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 }),