diff --git a/backend/user/src/user_config.py b/backend/user/src/user_config.py index 39790ad7..2febc1b2 100644 --- a/backend/user/src/user_config.py +++ b/backend/user/src/user_config.py @@ -25,7 +25,6 @@ class UserConfigType(TypedDict, total=False): hide_watched: bool show_ignored_only: bool show_subed_only: bool - sponsorblock_id: str class UserConfig: @@ -44,7 +43,6 @@ class UserConfig: hide_watched=False, show_ignored_only=False, show_subed_only=False, - sponsorblock_id=None, ) VALID_STYLESHEETS = get_stylesheets() @@ -134,9 +132,15 @@ class UserConfig: es_document_path = f"ta_config/_doc/user_{self._user_id}" response, status = ElasticWrap(es_document_path).get(print_error=False) if status == 200 and "_source" in response.keys(): - source = response.get("_source") + source = response.get("_source", {}) if "config" in source.keys(): return source.get("config") # There is no config in ES - return {} + response, status_code = ElasticWrap(es_document_path).put( + {"config": dict(self._DEFAULT_USER_SETTINGS)} + ) + if status_code == 200: + print(f"set default config for user {self._user_id}: {response}") + + return self._DEFAULT_USER_SETTINGS diff --git a/frontend/package-lock.json b/frontend/package-lock.json index be7414cf..0f2f11e0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,8 @@ "dompurify": "^3.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^7.0.2" + "react-router-dom": "^7.0.2", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^19.0.1", @@ -1213,7 +1214,7 @@ "version": "19.0.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1611,7 +1612,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.3.4", @@ -2918,6 +2919,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index bdafb8f7..2a43fe32 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "dompurify": "^3.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^7.0.2" + "react-router-dom": "^7.0.2", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^19.0.1", diff --git a/frontend/src/api/actions/updateUserConfig.ts b/frontend/src/api/actions/updateUserConfig.ts index e02a3dcc..9831d191 100644 --- a/frontend/src/api/actions/updateUserConfig.ts +++ b/frontend/src/api/actions/updateUserConfig.ts @@ -1,4 +1,3 @@ -import { ColourVariants } from '../../configuration/colours/getColours'; import { SortByType, SortOrderType, ViewLayoutType } from '../../pages/Home'; import getApiUrl from '../../configuration/getApiUrl'; import defaultHeaders from '../../configuration/defaultHeaders'; @@ -16,23 +15,24 @@ export type UserMeType = { config: UserConfigType; }; +export type ColourVariants = 'dark.css' | 'light.css' | 'matrix.css' | 'midnight.css'; + export type UserConfigType = { - stylesheet?: ColourVariants; - page_size?: number; - sort_by?: SortByType; - sort_order?: SortOrderType; - view_style_home?: ViewLayoutType; - view_style_channel?: ViewLayoutType; - view_style_downloads?: ViewLayoutType; - view_style_playlist?: ViewLayoutType; - grid_items?: number; - hide_watched?: boolean; - show_ignored_only?: boolean; - show_subed_only?: boolean; - sponsorblock_id?: number; + stylesheet: ColourVariants; + page_size: number; + sort_by: SortByType; + sort_order: SortOrderType; + view_style_home: ViewLayoutType; + view_style_channel: ViewLayoutType; + view_style_downloads: ViewLayoutType; + view_style_playlist: ViewLayoutType; + grid_items: number; + hide_watched: boolean; + show_ignored_only: boolean; + show_subed_only: boolean; }; -const updateUserConfig = async (config: UserConfigType): Promise => { +const updateUserConfig = async (config: Partial): Promise => { const apiUrl = getApiUrl(); const csrfCookie = getCookie('csrftoken'); diff --git a/frontend/src/components/ChannelList.tsx b/frontend/src/components/ChannelList.tsx index a633809a..4a98e218 100644 --- a/frontend/src/components/ChannelList.tsx +++ b/frontend/src/components/ChannelList.tsx @@ -1,6 +1,5 @@ import { Link } from 'react-router-dom'; import { ChannelType } from '../pages/Channels'; -import { ViewLayoutType } from '../pages/Home'; import Routes from '../configuration/routes/RouteList'; import updateChannelSubscription from '../api/actions/updateChannelSubscription'; import formatDate from '../functions/formatDates'; @@ -8,14 +7,18 @@ import FormattedNumber from './FormattedNumber'; import Button from './Button'; import ChannelIcon from './ChannelIcon'; import ChannelBanner from './ChannelBanner'; +import { useUserConfigStore } from '../stores/UserConfigStore'; type ChannelListProps = { channelList: ChannelType[] | undefined; - viewLayout: ViewLayoutType; refreshChannelList: (refresh: boolean) => void; }; -const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelListProps) => { +const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => { + + const { userConfig } = useUserConfigStore(); + const viewLayout = userConfig.config.view_style_channel; + if (!channelList || channelList.length === 0) { return

No channels found.

; } @@ -61,7 +64,9 @@ const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelLis title={`Unsubscribe from ${channel.channel_name}`} onClick={async () => { await updateChannelSubscription(channel.channel_id, false); - refreshChannelList(true); + setTimeout(() => { + refreshChannelList(true); + }, 1000); }} /> )} diff --git a/frontend/src/components/DownloadListItem.tsx b/frontend/src/components/DownloadListItem.tsx index 16708923..a51c7d24 100644 --- a/frontend/src/components/DownloadListItem.tsx +++ b/frontend/src/components/DownloadListItem.tsx @@ -7,15 +7,19 @@ import deleteDownloadById from '../api/actions/deleteDownloadById'; import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById'; import { useState } from 'react'; import getApiUrl from '../configuration/getApiUrl'; +import { useUserConfigStore } from '../stores/UserConfigStore'; type DownloadListItemProps = { - view: string; download: Download; - showIgnored: boolean; setRefresh: (status: boolean) => void; }; -const DownloadListItem = ({ view, download, showIgnored, setRefresh }: DownloadListItemProps) => { +const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => { + + const { userConfig } = useUserConfigStore(); + const view = userConfig.config.view_style_downloads; + const showIgnored = userConfig.config.show_ignored_only; + const [hideDownload, setHideDownload] = useState(false); return ( diff --git a/frontend/src/components/Filterbar.tsx b/frontend/src/components/Filterbar.tsx index e5083529..f8908355 100644 --- a/frontend/src/components/Filterbar.tsx +++ b/frontend/src/components/Filterbar.tsx @@ -1,79 +1,28 @@ -import { useEffect } from 'react'; -import { useRevalidator } from 'react-router-dom'; +import { useState } from 'react'; import iconSort from '/img/icon-sort.svg'; import iconAdd from '/img/icon-add.svg'; import iconSubstract from '/img/icon-substract.svg'; import iconGridView from '/img/icon-gridview.svg'; import iconListView from '/img/icon-listview.svg'; -import { SortByType, SortOrderType, ViewLayoutType } from '../pages/Home'; -import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig'; +import { SortByType, SortOrderType } from '../pages/Home'; +import { useUserConfigStore } from '../stores/UserConfigStore'; +import { ViewStyles } from '../configuration/constants/ViewStyle'; type FilterbarProps = { hideToggleText: string; - showHidden?: boolean; - hideWatched?: boolean; - isGridView?: boolean; - view: ViewLayoutType; viewStyleName: string; - gridItems: number; - sortBy?: SortByType; - sortOrder?: SortOrderType; - userMeConfig: UserConfigType; - setShowHidden?: (showHidden: boolean) => void; - setHideWatched?: (hideWatched: boolean) => void; - setView: (view: ViewLayoutType) => void; - setSortBy?: (sortBy: SortByType) => void; - setSortOrder?: (sortOrder: SortOrderType) => void; - setGridItems: (gridItems: number) => void; setRefresh?: (status: boolean) => void; }; const Filterbar = ({ hideToggleText, - showHidden, - hideWatched, - isGridView, - view, viewStyleName, - gridItems, - sortBy, - sortOrder, - userMeConfig, - setShowHidden, - setHideWatched, - setView, - setSortBy, - setSortOrder, - setGridItems, setRefresh, }: FilterbarProps) => { - const revalidator = useRevalidator(); - useEffect(() => { - (async () => { - if ( - (hideWatched !== undefined && userMeConfig.hide_watched !== hideWatched) || - (gridItems !== undefined && userMeConfig.grid_items !== gridItems) || - (sortBy !== undefined && userMeConfig.sort_by !== sortBy) || - (sortOrder !== undefined && userMeConfig.sort_order !== sortOrder) || - // @ts-ignore - userMeConfig[viewStyleName.toString()] !== view - ) { - const userConfig: UserConfigType = { - hide_watched: hideWatched, - [viewStyleName.toString()]: view, - grid_items: gridItems, - sort_by: sortBy, - sort_order: sortOrder, - }; - - await updateUserConfig(userConfig); - setRefresh?.(true); - - revalidator.revalidate(); - } - })(); - }, [hideWatched, view, gridItems, sortBy, sortOrder]); + const { userConfig, setPartialConfig } = useUserConfigStore(); + const [showHidden, setShowHidden] = useState(false); + const isGridView = userConfig.config.view_style_home === ViewStyles.grid return (
@@ -83,22 +32,23 @@ const Filterbar = ({ { - setHideWatched?.(!hideWatched); + setRefresh?.(true); + setPartialConfig({hide_watched: !userConfig.config.hide_watched}) }} /> - {!hideWatched && ( + {userConfig.config.hide_watched ? ( + + ) : ( )} - {hideWatched && ( - - )} +
@@ -109,9 +59,10 @@ const Filterbar = ({ { - setShowSubedOnly(!showSubedOnly); + setPartialConfig({show_subed_only: !showSubedOnly}); + setRefreshPlaylists(true); }} type="checkbox" /> @@ -84,14 +73,14 @@ const ChannelPlaylist = () => { { - setView('grid'); + setPartialConfig({view_style_playlist: 'grid'}); }} alt="grid view" /> { - setView('list'); + setPartialConfig({view_style_playlist: 'list'}); }} alt="list view" /> @@ -99,11 +88,10 @@ const ChannelPlaylist = () => { -
-
+
+
diff --git a/frontend/src/pages/ChannelVideo.tsx b/frontend/src/pages/ChannelVideo.tsx index a626dd68..7903a3e1 100644 --- a/frontend/src/pages/ChannelVideo.tsx +++ b/frontend/src/pages/ChannelVideo.tsx @@ -1,14 +1,11 @@ import { useEffect, useState } from 'react'; import { Link, - useLoaderData, useOutletContext, useParams, useSearchParams, } from 'react-router-dom'; -import { SortByType, SortOrderType, ViewLayoutType } from './Home'; import { OutletContextType } from './Base'; -import { UserMeType } from '../api/actions/updateUserConfig'; import VideoList from '../components/VideoList'; import Routes from '../configuration/routes/RouteList'; import Pagination from '../components/Pagination'; @@ -27,33 +24,23 @@ import loadVideoListByFilter, { } from '../api/loader/loadVideoListByPage'; import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs'; import humanFileSize from '../functions/humanFileSize'; +import { useUserConfigStore } from '../stores/UserConfigStore'; type ChannelParams = { channelId: string; }; -type ChannelVideoLoaderType = { - userConfig: UserMeType; -}; - type ChannelVideoProps = { videoType: VideoTypes; }; const ChannelVideo = ({ videoType }: ChannelVideoProps) => { const { channelId } = useParams() as ChannelParams; - const { userConfig } = useLoaderData() as ChannelVideoLoaderType; - const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const { userConfig } = useUserConfigStore(); + const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const [searchParams] = useSearchParams(); const videoId = searchParams.get('videoId'); - const userMeConfig = userConfig.config; - - const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false); - const [sortBy, setSortBy] = useState(userMeConfig.sort_by || 'published'); - const [sortOrder, setSortOrder] = useState(userMeConfig.sort_order || 'asc'); - const [view, setView] = useState(userMeConfig.view_style_home || 'grid'); - const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3); const [refresh, setRefresh] = useState(false); const [channelResponse, setChannelResponse] = useState(); @@ -67,37 +54,40 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => { const hasVideos = videoResponse?.data?.length !== 0; const showEmbeddedVideo = videoId !== null; + const view = userConfig.config.view_style_home const isGridView = view === ViewStyles.grid; - const gridView = isGridView ? `boxed-${gridItems}` : ''; - const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; + const gridView = isGridView ? `boxed-${userConfig.config.grid_items}` : ''; + const gridViewGrid = isGridView ? `grid-${userConfig.config.grid_items}` : ''; useEffect(() => { (async () => { - if ( - refresh || - pagination?.current_page === undefined || - currentPage !== pagination?.current_page - ) { - const channelResponse = await loadChannelById(channelId); - const videos = await loadVideoListByFilter({ - channel: channelId, - page: currentPage, - watch: hideWatched ? 'unwatched' : undefined, - sort: sortBy, - order: sortOrder, - type: videoType, - }); - const channelAggs = await loadChannelAggs(channelId); + const channelResponse = await loadChannelById(channelId); + const videos = await loadVideoListByFilter({ + channel: channelId, + page: currentPage, + watch: userConfig.config.hide_watched ? 'unwatched' : undefined, + sort: userConfig.config.sort_by, + order: userConfig.config.sort_order, + type: videoType, + }); + const channelAggs = await loadChannelAggs(channelId); - setChannelResponse(channelResponse); - setVideoReponse(videos); - setVideoAggsResponse(channelAggs); - setRefresh(false); - } + setChannelResponse(channelResponse); + setVideoReponse(videos); + setVideoAggsResponse(channelAggs); + setRefresh(false); })(); - // Do not add sort, order, hideWatched this will not work as expected! // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refresh, currentPage, channelId, pagination?.current_page]); + }, [ + refresh, + userConfig.config.sort_by, + userConfig.config.sort_order, + userConfig.config.hide_watched, + currentPage, + channelId, + pagination?.current_page, + videoType, + ]); if (!channel) { return ( @@ -121,7 +111,6 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => { channelSubscribed={channel.channel_subscribed} channelThumbUrl={channel.channel_thumb_url} showSubscribeButton={true} - isUserAdmin={isAdmin} setRefresh={setRefresh} />
@@ -172,18 +161,6 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 0eb93d66..f5c26fba 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -1,18 +1,19 @@ -import { useLoaderData, useOutletContext } from 'react-router-dom'; +import { useOutletContext } from 'react-router-dom'; import loadChannelList from '../api/loader/loadChannelList'; import iconGridView from '/img/icon-gridview.svg'; import iconListView from '/img/icon-listview.svg'; import iconAdd from '/img/icon-add.svg'; import { useEffect, useState } from 'react'; import Pagination, { PaginationType } from '../components/Pagination'; -import { ConfigType, ViewLayoutType } from './Home'; -import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig'; +import { ConfigType } from './Home'; import { OutletContextType } from './Base'; import ChannelList from '../components/ChannelList'; import ScrollToTopOnNavigate from '../components/ScrollToTop'; import Notifications from '../components/Notifications'; import Button from '../components/Button'; import updateChannelSubscription from '../api/actions/updateChannelSubscription'; +import loadIsAdmin from '../functions/getIsAdmin'; +import { useUserConfigStore } from '../stores/UserConfigStore'; type ChannelOverwritesType = { download_format?: string; @@ -46,21 +47,12 @@ type ChannelsListResponse = { config?: ConfigType; }; -type ChannelsLoaderDataType = { - userConfig: UserMeType; -}; - const Channels = () => { - const { userConfig } = useLoaderData() as ChannelsLoaderDataType; - const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; - - const userMeConfig = userConfig.config; + const { userConfig, setPartialConfig } = useUserConfigStore(); + const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const isAdmin = loadIsAdmin(); const [channelListResponse, setChannelListResponse] = useState(); - const [showSubscribedOnly, setShowSubscribedOnly] = useState( - userMeConfig.show_subed_only || false, - ); - const [view, setView] = useState(userMeConfig.view_style_channel || 'grid'); const [showAddForm, setShowAddForm] = useState(false); const [refresh, setRefresh] = useState(false); const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState(''); @@ -72,34 +64,10 @@ const Channels = () => { useEffect(() => { (async () => { - if ( - userMeConfig.view_style_channel !== view || - userMeConfig.show_subed_only !== showSubscribedOnly - ) { - const userConfig: UserConfigType = { - show_subed_only: showSubscribedOnly, - view_style_channel: view, - }; - - await updateUserConfig(userConfig); - } + const channelListResponse = await loadChannelList(currentPage, userConfig.config.show_subed_only); + setChannelListResponse(channelListResponse); })(); - }, [showSubscribedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_channel, view]); - - useEffect(() => { - (async () => { - if ( - refresh || - pagination?.current_page === undefined || - currentPage !== pagination?.current_page - ) { - const channelListResponse = await loadChannelList(currentPage, showSubscribedOnly); - - setChannelListResponse(channelListResponse); - setRefresh(false); - } - })(); - }, [currentPage, showSubscribedOnly, refresh, pagination?.current_page]); + }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]); return ( <> @@ -158,18 +126,19 @@ const Channels = () => {
{ - setShowSubscribedOnly(!showSubscribedOnly); + onChange={async () => { + setPartialConfig({show_subed_only: !userConfig.config.show_subed_only}); + setRefresh(true); }} type="checkbox" - checked={showSubscribedOnly} + checked={userConfig.config.show_subed_only} /> - {!showSubscribedOnly && ( + {!userConfig.config.show_subed_only && ( )} - {showSubscribedOnly && ( + {userConfig.config.show_subed_only && ( @@ -180,7 +149,7 @@ const Channels = () => { { - setView('grid'); + setPartialConfig({view_style_channel: 'grid'}); }} data-origin="channel" data-value="grid" @@ -189,7 +158,7 @@ const Channels = () => { { - setView('list'); + setPartialConfig({view_style_channel: 'list'}); }} data-origin="channel" data-value="list" @@ -199,11 +168,11 @@ const Channels = () => {
{hasChannels &&

Total channels: {channelCount}

} -
+
{!hasChannels &&

No channels found...

} {hasChannels && ( - + )}
diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx index ffd57cc9..fe70b1be 100644 --- a/frontend/src/pages/Download.tsx +++ b/frontend/src/pages/Download.tsx @@ -5,13 +5,12 @@ import iconSubstract from '/img/icon-substract.svg'; import iconGridView from '/img/icon-gridview.svg'; import iconListView from '/img/icon-listview.svg'; import { Fragment, useEffect, useState } from 'react'; -import { useLoaderData, useOutletContext, useSearchParams } from 'react-router-dom'; -import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig'; -import { ConfigType, ViewLayoutType } from './Home'; +import { useOutletContext, useSearchParams } from 'react-router-dom'; +import { ConfigType } from './Home'; import loadDownloadQueue from '../api/loader/loadDownloadQueue'; import { OutletContextType } from './Base'; import Pagination, { PaginationType } from '../components/Pagination'; -import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle'; +import { ViewStyles } from '../configuration/constants/ViewStyle'; import updateDownloadQueue from '../api/actions/updateDownloadQueue'; import updateTaskByName from '../api/actions/updateTaskByName'; import Notifications from '../components/Notifications'; @@ -19,6 +18,7 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop'; import Button from '../components/Button'; import DownloadListItem from '../components/DownloadListItem'; import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs'; +import { useUserConfigStore } from '../stores/UserConfigStore'; type Download = { auto_start: boolean; @@ -44,21 +44,13 @@ export type DownloadResponseType = { paginate?: PaginationType; }; -type DownloadLoaderDataType = { - userConfig: UserMeType; -}; - const Download = () => { const [searchParams, setSearchParams] = useSearchParams(); - const { userConfig } = useLoaderData() as DownloadLoaderDataType; + const { userConfig, setPartialConfig } = useUserConfigStore(); const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const channelFilterFromUrl = searchParams.get('channel'); - const userMeConfig = userConfig.config; - const [view, setView] = useState(userMeConfig.view_style_downloads || 'grid'); - const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3); - const [showIgnored, setShowIgnored] = useState(userMeConfig.show_ignored_only || false); const [refresh, setRefresh] = useState(false); const [showHiddenForm, setShowHiddenForm] = useState(false); const [downloadPending, setDownloadPending] = useState(false); @@ -83,60 +75,28 @@ const Download = () => { ? downloadResponse?.data[0].channel_name : ''; + const view = userConfig.config.view_style_downloads; + const gridItems = userConfig.config.grid_items; + const showIgnored = userConfig.config.show_ignored_only; const isGridView = view === ViewStyles.grid; const gridView = isGridView ? `boxed-${gridItems}` : ''; const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; useEffect(() => { (async () => { - if ( - userMeConfig.show_ignored_only !== showIgnored || - userMeConfig.grid_items !== gridItems || - // @ts-ignore - userMeConfig[ViewStyleNames.downloads] !== view - ) { - const userConfig: UserConfigType = { - show_ignored_only: showIgnored, - [ViewStyleNames.downloads]: view, - grid_items: gridItems, - }; + const videos = await loadDownloadQueue(currentPage, channelFilterFromUrl, showIgnored); + const videoCount = videos?.paginate?.total_hits; - await updateUserConfig(userConfig); - setRefresh(true); + if (videoCount && lastVideoCount !== videoCount) { + setLastVideoCount(videoCount); } - })(); - }, [ - view, - gridItems, - showIgnored, - userMeConfig.show_ignored_only, - userMeConfig.view_style_downloads, - userMeConfig.grid_items, - ]); - useEffect(() => { - (async () => { - if ( - refresh || - pagination?.current_page === undefined || - currentPage !== pagination?.current_page - ) { - const videos = await loadDownloadQueue(currentPage, channelFilterFromUrl, showIgnored); - - const videoCount = videos?.paginate?.total_hits; - - if (videoCount && lastVideoCount !== videoCount) { - setLastVideoCount(videoCount); - } - - setDownloadResponse(videos); - setRefresh(false); - } + setDownloadResponse(videos); + setRefresh(false); })(); - // Do not add showIgnored otherwise it will not update the userconfig first. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refresh, currentPage, downloadPending]); + }, [refresh, showIgnored, currentPage, downloadPending]); useEffect(() => { (async () => { @@ -246,7 +206,8 @@ const Download = () => { { - setShowIgnored(!showIgnored); + setPartialConfig({show_ignored_only: !showIgnored}); + setRefresh(true); }} type="checkbox" checked={showIgnored} @@ -301,7 +262,7 @@ const Download = () => { { - setGridItems(gridItems + 1); + setPartialConfig({grid_items: gridItems + 1}); }} alt="grid plus row" /> @@ -310,7 +271,7 @@ const Download = () => { { - setGridItems(gridItems - 1); + setPartialConfig({grid_items: gridItems - 1}); }} alt="grid minus row" /> @@ -321,14 +282,14 @@ const Download = () => { { - setView('grid'); + setPartialConfig({view_style_downloads: 'grid'}); }} alt="grid view" /> { - setView('list'); + setPartialConfig({view_style_downloads: 'list'}); }} alt="list view" /> @@ -354,8 +315,6 @@ const Download = () => { diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx index b32dff97..8022a657 100644 --- a/frontend/src/pages/ErrorPage.tsx +++ b/frontend/src/pages/ErrorPage.tsx @@ -1,5 +1,6 @@ import { useRouteError } from 'react-router-dom'; -import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours'; +import importColours from '../configuration/colours/getColours'; + // This is not always the correct response type ErrorType = { @@ -9,7 +10,7 @@ type ErrorType = { const ErrorPage = () => { const error = useRouteError() as ErrorType; - importColours(ColourConstant.Dark as ColourVariants); + importColours(); console.error('ErrorPage', error); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index cf8c082d..847c3375 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,11 +1,10 @@ import { useEffect, useState } from 'react'; -import { Link, useLoaderData, useOutletContext, useSearchParams } from 'react-router-dom'; +import { Link, useOutletContext, useSearchParams } from 'react-router-dom'; import Routes from '../configuration/routes/RouteList'; import Pagination from '../components/Pagination'; import loadVideoListByFilter, { VideoListByFilterResponseType, } from '../api/loader/loadVideoListByPage'; -import { UserMeType } from '../api/actions/updateUserConfig'; import VideoList from '../components/VideoList'; import { ChannelType } from './Channels'; import { OutletContextType } from './Base'; @@ -14,6 +13,7 @@ import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle import ScrollToTopOnNavigate from '../components/ScrollToTop'; import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer'; import { SponsorBlockType } from './Video'; +import { useUserConfigStore } from '../stores/UserConfigStore'; export type PlayerType = { watched: boolean; @@ -98,28 +98,18 @@ export type ConfigType = { downloads: DownloadsType; }; -type HomeLoaderDataType = { - userConfig: UserMeType; -}; - -export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'filesize'; +export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize'; export type SortOrderType = 'asc' | 'desc'; export type ViewLayoutType = 'grid' | 'list'; const Home = () => { - const { userConfig } = useLoaderData() as HomeLoaderDataType; + const { userConfig } = useUserConfigStore(); const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const [searchParams] = useSearchParams(); const videoId = searchParams.get('videoId'); const userMeConfig = userConfig.config; - const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false); - const [sortBy, setSortBy] = useState(userMeConfig.sort_by || 'published'); - const [sortOrder, setSortOrder] = useState(userMeConfig.sort_order || 'asc'); - const [view, setView] = useState(userMeConfig.view_style_home || 'grid'); - const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3); - const [showHidden, setShowHidden] = useState(false); const [refreshVideoList, setRefreshVideoList] = useState(false); const [videoResponse, setVideoReponse] = useState(); @@ -133,9 +123,9 @@ const Home = () => { const hasVideos = videoResponse?.data?.length !== 0; const showEmbeddedVideo = videoId !== null; - const isGridView = view === ViewStyles.grid; - const gridView = isGridView ? `boxed-${gridItems}` : ''; - const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; + const isGridView = userMeConfig.view_style_home === ViewStyles.grid; + const gridView = isGridView ? `boxed-${userMeConfig.grid_items}` : ''; + const gridViewGrid = isGridView ? `grid-${userMeConfig.grid_items}` : ''; useEffect(() => { (async () => { @@ -146,9 +136,9 @@ const Home = () => { ) { const videos = await loadVideoListByFilter({ page: currentPage, - watch: hideWatched ? 'unwatched' : undefined, - sort: sortBy, - order: sortOrder, + watch: userMeConfig.hide_watched ? 'unwatched' : undefined, + sort: userMeConfig.sort_by, + order: userMeConfig.sort_order, }); try { @@ -163,9 +153,15 @@ const Home = () => { setRefreshVideoList(false); } })(); - // Do not add sort, order, hideWatched this will not work as expected! // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refreshVideoList, currentPage, pagination?.current_page]); + }, [ + refreshVideoList, + userMeConfig.sort_by, + userMeConfig.sort_order, + userMeConfig.hide_watched, + currentPage, + pagination?.current_page + ]); return ( <> @@ -180,10 +176,10 @@ const Home = () => {

Continue Watching

-
+
@@ -196,27 +192,13 @@ const Home = () => {
-
+
{!hasVideos && ( <>

No videos found...

@@ -231,7 +213,7 @@ const Home = () => { {hasVideos && ( )} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c8ac4729..e3d8e45f 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import Routes from '../configuration/routes/RouteList'; import { useNavigate } from 'react-router-dom'; -import importColours, { ColourConstant, ColourVariants } from '../configuration/colours/getColours'; +import importColours from '../configuration/colours/getColours'; import Button from '../components/Button'; import signIn from '../api/actions/signIn'; @@ -11,7 +11,7 @@ const Login = () => { const [saveLogin, setSaveLogin] = useState(false); const navigate = useNavigate(); - importColours(ColourConstant.Dark as ColourVariants); + importColours(); const form_error = false; diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx index c8148009..06245871 100644 --- a/frontend/src/pages/Playlist.tsx +++ b/frontend/src/pages/Playlist.tsx @@ -1,13 +1,11 @@ import { useEffect, useState } from 'react'; import { Link, - useLoaderData, useNavigate, useOutletContext, useParams, useSearchParams, } from 'react-router-dom'; -import { UserMeType } from '../api/actions/updateUserConfig'; import loadPlaylistById from '../api/loader/loadPlaylistById'; import { OutletContextType } from './Base'; import { ConfigType, VideoType, ViewLayoutType } from './Home'; @@ -30,6 +28,8 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop'; import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer'; import Button from '../components/Button'; import loadVideoListByFilter from '../api/loader/loadVideoListByPage'; +import loadIsAdmin from '../functions/getIsAdmin'; +import { useUserConfigStore } from '../stores/UserConfigStore'; export type PlaylistType = { playlist_active: boolean; @@ -47,10 +47,6 @@ export type PlaylistType = { _score: number; }; -type PlaylistLoaderDataType = { - userConfig: UserMeType; -}; - export type PlaylistResponseType = { data?: PlaylistType; config?: ConfigType; @@ -68,8 +64,9 @@ const Playlist = () => { const [searchParams] = useSearchParams(); const videoId = searchParams.get('videoId'); - const { userConfig } = useLoaderData() as PlaylistLoaderDataType; - const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const { userConfig } = useUserConfigStore(); + const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const isAdmin = loadIsAdmin(); const userMeConfig = userConfig.config; diff --git a/frontend/src/pages/Playlists.tsx b/frontend/src/pages/Playlists.tsx index 934e91dd..a93c320e 100644 --- a/frontend/src/pages/Playlists.tsx +++ b/frontend/src/pages/Playlists.tsx @@ -1,14 +1,13 @@ import { useEffect, useState } from 'react'; -import { useLoaderData, useOutletContext } from 'react-router-dom'; +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 { OutletContextType } from './Base'; -import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig'; import loadPlaylistList from '../api/loader/loadPlaylistList'; -import { ConfigType, ViewLayoutType } from './Home'; +import { ConfigType } from './Home'; import Pagination, { PaginationType } from '../components/Pagination'; import PlaylistList from '../components/PlaylistList'; import { PlaylistType } from './Playlist'; @@ -16,6 +15,8 @@ import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscriptio import createCustomPlaylist from '../api/actions/createCustomPlaylist'; import ScrollToTopOnNavigate from '../components/ScrollToTop'; import Button from '../components/Button'; +import loadIsAdmin from '../functions/getIsAdmin'; +import { useUserConfigStore } from '../stores/UserConfigStore'; export type PlaylistEntryType = { youtube_id: string; @@ -31,18 +32,11 @@ export type PlaylistsResponseType = { paginate?: PaginationType; }; -type PlaylistLoaderDataType = { - userConfig: UserMeType; -}; - const Playlists = () => { - const { userConfig } = useLoaderData() as PlaylistLoaderDataType; - const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const { userConfig, setPartialConfig } = useUserConfigStore(); + const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const isAdmin = loadIsAdmin(); - const userMeConfig = userConfig.config; - - const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false); - const [view, setView] = useState(userMeConfig.view_style_playlist || 'grid'); const [showAddForm, setShowAddForm] = useState(false); const [refresh, setRefresh] = useState(false); const [playlistsToAddText, setPlaylistsToAddText] = useState(''); @@ -55,42 +49,21 @@ const Playlists = () => { const hasPlaylists = playlistResponse?.data?.length !== 0; - useEffect(() => { - (async () => { - if ( - userMeConfig.view_style_playlist !== view || - userMeConfig.show_subed_only !== showSubedOnly - ) { - const userConfig: UserConfigType = { - show_subed_only: showSubedOnly, - view_style_playlist: view, - }; - - await updateUserConfig(userConfig); - setRefresh(true); - } - })(); - }, [showSubedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_playlist, view]); + const view = userConfig.config.view_style_playlist; + const showSubedOnly = userConfig.config.show_subed_only; useEffect(() => { (async () => { - if ( - refresh || - pagination?.current_page === undefined || - currentPage !== pagination?.current_page - ) { - const playlist = await loadPlaylistList({ - page: currentPage, - subscribed: showSubedOnly, - }); + const playlist = await loadPlaylistList({ + page: currentPage, + subscribed: showSubedOnly, + }); - setPlaylistReponse(playlist); - setRefresh(false); - } + setPlaylistReponse(playlist); + setRefresh(false); })(); - // Do not add showSubedOnly, view this will not work as expected! // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refresh, currentPage, pagination?.current_page]); + }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]); return ( <> @@ -170,7 +143,7 @@ const Playlists = () => { { - setShowSubedOnly(!showSubedOnly); + setPartialConfig({show_subed_only: !showSubedOnly}); }} type="checkbox" /> @@ -190,14 +163,14 @@ const Playlists = () => { { - setView('grid'); + setPartialConfig({view_style_playlist: 'grid'}); }} alt="grid view" /> { - setView('list'); + setPartialConfig({view_style_playlist: 'list'}); }} alt="list view" /> @@ -208,7 +181,7 @@ const Playlists = () => { {!hasPlaylists &&

No playlists found...

} {hasPlaylists && ( - + )}
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index af4618ab..463d2484 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -1,7 +1,6 @@ -import { useLoaderData, useSearchParams } from 'react-router-dom'; -import { UserMeType } from '../api/actions/updateUserConfig'; +import { useSearchParams } from 'react-router-dom'; import { useEffect, useState } from 'react'; -import { VideoType, ViewLayoutType } from './Home'; +import { VideoType } from './Home'; import loadSearch from '../api/loader/loadSearch'; import { PlaylistType } from './Playlist'; import { ChannelType } from './Channels'; @@ -12,6 +11,7 @@ import SubtitleList from '../components/SubtitleList'; import { ViewStyles } from '../configuration/constants/ViewStyle'; import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer'; import SearchExampleQueries from '../components/SearchExampleQueries'; +import { useUserConfigStore } from '../stores/UserConfigStore'; const EmptySearchResponse: SearchResultsType = { results: { @@ -35,17 +35,15 @@ type SearchResultsType = { queryType: string; }; -type SearchLoaderDataType = { - userConfig: UserMeType; -}; - const Search = () => { - const { userConfig } = useLoaderData() as SearchLoaderDataType; + const { userConfig } = useUserConfigStore(); const [searchParams] = useSearchParams(); const videoId = searchParams.get('videoId'); const userMeConfig = userConfig.config; - const view = (userMeConfig.view_style_home || ViewStyles.grid) as ViewLayoutType; + const viewVideos = userMeConfig.view_style_home; + const viewChannels = userMeConfig.view_style_channel; + const viewPlaylists = userMeConfig.view_style_playlist; const gridItems = userMeConfig.grid_items || 3; const [searchQuery, setSearchQuery] = useState(''); @@ -72,7 +70,7 @@ const Search = () => { const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery; const isFullTextQuery = queryType === 'full' || isSimpleQuery; - const isGridView = view === ViewStyles.grid; + const isGridView = viewVideos === ViewStyles.grid; const gridView = isGridView ? `boxed-${gridItems}` : ''; const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; @@ -116,8 +114,8 @@ const Search = () => { {hasSearchQuery && isVideoQuery && (

Video Results

-
- +
+
)} @@ -125,10 +123,9 @@ const Search = () => { {hasSearchQuery && isChannelQuery && (

Channel Results

-
+
@@ -138,10 +135,9 @@ const Search = () => { {hasSearchQuery && isPlaylistQuery && (

Playlist Results

-
+
diff --git a/frontend/src/pages/SettingsUser.tsx b/frontend/src/pages/SettingsUser.tsx index a52235a8..463df003 100644 --- a/frontend/src/pages/SettingsUser.tsx +++ b/frontend/src/pages/SettingsUser.tsx @@ -1,46 +1,43 @@ -import { useLoaderData, useNavigate, useOutletContext } from 'react-router-dom'; -import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig'; -import { useEffect, useState } from 'react'; -import loadUserMeConfig from '../api/loader/loadUserConfig'; -import { ColourConstant, ColourVariants } from '../configuration/colours/getColours'; +import { useNavigate } from 'react-router-dom'; +import { ColourVariants } from '../api/actions/updateUserConfig'; +import { ColourConstant } from '../configuration/colours/getColours'; import SettingsNavigation from '../components/SettingsNavigation'; import Notifications from '../components/Notifications'; import Button from '../components/Button'; -import { OutletContextType } from './Base'; - -type SettingsUserLoaderData = { - userConfig: UserMeType; -}; +import loadIsAdmin from '../functions/getIsAdmin'; +import { useUserConfigStore } from '../stores/UserConfigStore'; +import { useEffect, useState } from 'react'; const SettingsUser = () => { - const { isAdmin } = useOutletContext() as OutletContextType; - const { userConfig } = useLoaderData() as SettingsUserLoaderData; + const { userConfig, setPartialConfig } = useUserConfigStore(); + const isAdmin = loadIsAdmin(); const navigate = useNavigate(); - const userMeConfig = userConfig.config; - const { stylesheet, page_size } = userMeConfig; - - const [selectedStylesheet, setSelectedStylesheet] = useState(userMeConfig.stylesheet); - const [selectedPageSize, setSelectedPageSize] = useState(userMeConfig.page_size); - const [refresh, setRefresh] = useState(false); - - const [userConfigResponse, setUserConfigResponse] = useState(); - - const stylesheetOverwritable = - userConfigResponse?.stylesheet || stylesheet || (ColourConstant.Dark as ColourVariants); - const pageSizeOverwritable = userConfigResponse?.page_size || page_size || 12; + const [styleSheet, setStyleSheet] = useState(userConfig.config.stylesheet); + const [styleSheetRefresh, setStyleSheetRefresh] = useState(false); + const [pageSize, setPageSize] = useState(userConfig.config.page_size); useEffect(() => { (async () => { - if (refresh) { - const userConfigResponse = await loadUserMeConfig(); - - setUserConfigResponse(userConfigResponse.config); - setRefresh(false); - navigate(0); - } + setStyleSheet(userConfig.config.stylesheet); + setPageSize(userConfig.config.page_size); })(); - }, [navigate, refresh]); + }, [userConfig.config.page_size, userConfig.config.stylesheet]); + + const handleStyleSheetChange = async (selectedStyleSheet: ColourVariants) => { + setPartialConfig({stylesheet: selectedStyleSheet}); + setStyleSheet(selectedStyleSheet); + setStyleSheetRefresh(true); + } + + const handlePageSizeChange = async () => { + setPartialConfig({page_size: pageSize}); + } + + const handlePageRefresh = () => { + navigate(0); + setStyleSheetRefresh(false); + } return ( <> @@ -52,74 +49,63 @@ const SettingsUser = () => {

User Configurations

-
-
-

Stylesheet

-
-

- Current stylesheet:{' '} - {stylesheetOverwritable} -

- Select your preferred stylesheet. -
- +
+
+

Customize user Interface

+
+
+

Switch your color scheme

+
+
+ + {styleSheetRefresh && ( + + )} +
+
+
+
+

Archive view page size

+
+
+ { + setPageSize(Number(event.target.value)); + }} + /> +
+ {userConfig.config.page_size !== pageSize && ( + <> + + + + )} +
+
-
-

Archive View

-
-

- Current page size: {pageSizeOverwritable} -

- Result of videos showing in archive page -
- - { - setSelectedPageSize(Number(event.target.value)); - }} - > -
-
-
- {isAdmin && ( <> -
-

Users

-

User Management

diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx index c8c2850b..562746b6 100644 --- a/frontend/src/pages/Video.tsx +++ b/frontend/src/pages/Video.tsx @@ -1,4 +1,4 @@ -import { Link, useNavigate, useOutletContext, useParams } from 'react-router-dom'; +import { Link, useNavigate, useParams } from 'react-router-dom'; import loadVideoById from '../api/loader/loadVideoById'; import { Fragment, useEffect, useState } from 'react'; import { ConfigType, VideoType } from './Home'; @@ -35,9 +35,9 @@ import { PlaylistType } from './Playlist'; import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId'; import CommentBox, { CommentsType } from '../components/CommentBox'; import Button from '../components/Button'; -import { OutletContextType } from './Base'; import getApiUrl from '../configuration/getApiUrl'; import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav'; +import loadIsAdmin from '../functions/getIsAdmin'; const isInPlaylist = (videoId: string, playlist: PlaylistType) => { return playlist.playlist_entries.some(entry => { @@ -116,9 +116,9 @@ export type VideoCommentsResponseType = { }; const Video = () => { - const { isAdmin } = useOutletContext() as OutletContextType; const { videoId } = useParams() as VideoParams; const navigate = useNavigate(); + const isAdmin = loadIsAdmin(); const [loading, setLoading] = useState(false); const [videoEnded, setVideoEnded] = useState(false); diff --git a/frontend/src/stores/AuthDataStore.ts b/frontend/src/stores/AuthDataStore.ts new file mode 100644 index 00000000..49f484c9 --- /dev/null +++ b/frontend/src/stores/AuthDataStore.ts @@ -0,0 +1,12 @@ +import { create } from 'zustand'; +import { AuthenticationType } from '../pages/Base'; + +interface AuthState { + auth: AuthenticationType | null; + setAuth: (auth: AuthenticationType) => void; +} + +export const useAuthStore = create((set) => ({ + auth: null, + setAuth: (auth) => set({ auth }), +})); diff --git a/frontend/src/stores/UserConfigStore.ts b/frontend/src/stores/UserConfigStore.ts new file mode 100644 index 00000000..ee00a67a --- /dev/null +++ b/frontend/src/stores/UserConfigStore.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand'; +import updateUserConfig, { UserMeType, UserConfigType } from '../api/actions/updateUserConfig'; + +interface UserConfigState { + userConfig: UserMeType; + setUserConfig: (userConfig: UserMeType) => void; + setPartialConfig: (userConfig: Partial) => void; +} + +export const useUserConfigStore = create((set) => ({ + + userConfig: { + id: 0, + name: '', + is_superuser: false, + is_staff: false, + groups: [], + user_permissions: [], + last_login: '', + config: { + stylesheet: 'dark.css', + page_size: 12, + sort_by: 'published', + sort_order: 'desc', + view_style_home: 'grid', + view_style_channel: 'list', + view_style_downloads: 'list', + view_style_playlist: 'grid', + grid_items: 3, + hide_watched: false, + show_ignored_only: false, + show_subed_only: false, + } + }, + setUserConfig: (userConfig) => set({ userConfig }), + + setPartialConfig: async (userConfig: Partial) => { + const userConfigResponse = await updateUserConfig(userConfig); + set((state) => ({ + userConfig: state.userConfig ? { ...state.userConfig, config: userConfigResponse } : state.userConfig, + })); + } + +}))