Feat global state (#861)

* add zustand, add auth store

* add user config store, isAdmin from store

* move ColourVariants, fix importColours

* get userConfig from store

* fix name conflict

* home filter bar from config store

* use user store for channel list

* use userConfig store for channel pages

* use userConfig for playlist pages

* fix channel video type navigation

* use userConfig update on downloads page

* fix view style in search

* handle initial user config, take 2

* modify userSettings in global state
This commit is contained in:
Simon
2025-01-06 19:44:05 +07:00
committed by GitHub
parent 0eca242fd6
commit c9607343e6
31 changed files with 443 additions and 721 deletions

View File

@@ -25,7 +25,6 @@ class UserConfigType(TypedDict, total=False):
hide_watched: bool hide_watched: bool
show_ignored_only: bool show_ignored_only: bool
show_subed_only: bool show_subed_only: bool
sponsorblock_id: str
class UserConfig: class UserConfig:
@@ -44,7 +43,6 @@ class UserConfig:
hide_watched=False, hide_watched=False,
show_ignored_only=False, show_ignored_only=False,
show_subed_only=False, show_subed_only=False,
sponsorblock_id=None,
) )
VALID_STYLESHEETS = get_stylesheets() VALID_STYLESHEETS = get_stylesheets()
@@ -134,9 +132,15 @@ class UserConfig:
es_document_path = f"ta_config/_doc/user_{self._user_id}" es_document_path = f"ta_config/_doc/user_{self._user_id}"
response, status = ElasticWrap(es_document_path).get(print_error=False) response, status = ElasticWrap(es_document_path).get(print_error=False)
if status == 200 and "_source" in response.keys(): if status == 200 and "_source" in response.keys():
source = response.get("_source") source = response.get("_source", {})
if "config" in source.keys(): if "config" in source.keys():
return source.get("config") return source.get("config")
# There is no config in ES # 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

View File

@@ -11,7 +11,8 @@
"dompurify": "^3.2.3", "dompurify": "^3.2.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^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": { "devDependencies": {
"@types/react": "^19.0.1", "@types/react": "^19.0.1",
@@ -1213,7 +1214,7 @@
"version": "19.0.1", "version": "19.0.1",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz",
"integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==", "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -1611,7 +1612,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true "devOptional": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
@@ -2918,6 +2919,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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
}
}
} }
} }
} }

View File

@@ -13,7 +13,8 @@
"dompurify": "^3.2.3", "dompurify": "^3.2.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^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": { "devDependencies": {
"@types/react": "^19.0.1", "@types/react": "^19.0.1",

View File

@@ -1,4 +1,3 @@
import { ColourVariants } from '../../configuration/colours/getColours';
import { SortByType, SortOrderType, ViewLayoutType } from '../../pages/Home'; import { SortByType, SortOrderType, ViewLayoutType } from '../../pages/Home';
import getApiUrl from '../../configuration/getApiUrl'; import getApiUrl from '../../configuration/getApiUrl';
import defaultHeaders from '../../configuration/defaultHeaders'; import defaultHeaders from '../../configuration/defaultHeaders';
@@ -16,23 +15,24 @@ export type UserMeType = {
config: UserConfigType; config: UserConfigType;
}; };
export type ColourVariants = 'dark.css' | 'light.css' | 'matrix.css' | 'midnight.css';
export type UserConfigType = { export type UserConfigType = {
stylesheet?: ColourVariants; stylesheet: ColourVariants;
page_size?: number; page_size: number;
sort_by?: SortByType; sort_by: SortByType;
sort_order?: SortOrderType; sort_order: SortOrderType;
view_style_home?: ViewLayoutType; view_style_home: ViewLayoutType;
view_style_channel?: ViewLayoutType; view_style_channel: ViewLayoutType;
view_style_downloads?: ViewLayoutType; view_style_downloads: ViewLayoutType;
view_style_playlist?: ViewLayoutType; view_style_playlist: ViewLayoutType;
grid_items?: number; grid_items: number;
hide_watched?: boolean; hide_watched: boolean;
show_ignored_only?: boolean; show_ignored_only: boolean;
show_subed_only?: boolean; show_subed_only: boolean;
sponsorblock_id?: number;
}; };
const updateUserConfig = async (config: UserConfigType): Promise<UserConfigType> => { const updateUserConfig = async (config: Partial<UserConfigType>): Promise<UserConfigType> => {
const apiUrl = getApiUrl(); const apiUrl = getApiUrl();
const csrfCookie = getCookie('csrftoken'); const csrfCookie = getCookie('csrftoken');

View File

@@ -1,6 +1,5 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ChannelType } from '../pages/Channels'; import { ChannelType } from '../pages/Channels';
import { ViewLayoutType } from '../pages/Home';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import updateChannelSubscription from '../api/actions/updateChannelSubscription'; import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import formatDate from '../functions/formatDates'; import formatDate from '../functions/formatDates';
@@ -8,14 +7,18 @@ import FormattedNumber from './FormattedNumber';
import Button from './Button'; import Button from './Button';
import ChannelIcon from './ChannelIcon'; import ChannelIcon from './ChannelIcon';
import ChannelBanner from './ChannelBanner'; import ChannelBanner from './ChannelBanner';
import { useUserConfigStore } from '../stores/UserConfigStore';
type ChannelListProps = { type ChannelListProps = {
channelList: ChannelType[] | undefined; channelList: ChannelType[] | undefined;
viewLayout: ViewLayoutType;
refreshChannelList: (refresh: boolean) => void; 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) { if (!channelList || channelList.length === 0) {
return <p>No channels found.</p>; return <p>No channels found.</p>;
} }
@@ -61,7 +64,9 @@ const ChannelList = ({ channelList, viewLayout, refreshChannelList }: ChannelLis
title={`Unsubscribe from ${channel.channel_name}`} title={`Unsubscribe from ${channel.channel_name}`}
onClick={async () => { onClick={async () => {
await updateChannelSubscription(channel.channel_id, false); await updateChannelSubscription(channel.channel_id, false);
refreshChannelList(true); setTimeout(() => {
refreshChannelList(true);
}, 1000);
}} }}
/> />
)} )}

View File

@@ -7,15 +7,19 @@ import deleteDownloadById from '../api/actions/deleteDownloadById';
import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById'; import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById';
import { useState } from 'react'; import { useState } from 'react';
import getApiUrl from '../configuration/getApiUrl'; import getApiUrl from '../configuration/getApiUrl';
import { useUserConfigStore } from '../stores/UserConfigStore';
type DownloadListItemProps = { type DownloadListItemProps = {
view: string;
download: Download; download: Download;
showIgnored: boolean;
setRefresh: (status: boolean) => void; 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); const [hideDownload, setHideDownload] = useState(false);
return ( return (

View File

@@ -1,79 +1,28 @@
import { useEffect } from 'react'; import { useState } from 'react';
import { useRevalidator } from 'react-router-dom';
import iconSort from '/img/icon-sort.svg'; import iconSort from '/img/icon-sort.svg';
import iconAdd from '/img/icon-add.svg'; import iconAdd from '/img/icon-add.svg';
import iconSubstract from '/img/icon-substract.svg'; import iconSubstract from '/img/icon-substract.svg';
import iconGridView from '/img/icon-gridview.svg'; import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg'; import iconListView from '/img/icon-listview.svg';
import { SortByType, SortOrderType, ViewLayoutType } from '../pages/Home'; import { SortByType, SortOrderType } from '../pages/Home';
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig'; import { useUserConfigStore } from '../stores/UserConfigStore';
import { ViewStyles } from '../configuration/constants/ViewStyle';
type FilterbarProps = { type FilterbarProps = {
hideToggleText: string; hideToggleText: string;
showHidden?: boolean;
hideWatched?: boolean;
isGridView?: boolean;
view: ViewLayoutType;
viewStyleName: string; 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; setRefresh?: (status: boolean) => void;
}; };
const Filterbar = ({ const Filterbar = ({
hideToggleText, hideToggleText,
showHidden,
hideWatched,
isGridView,
view,
viewStyleName, viewStyleName,
gridItems,
sortBy,
sortOrder,
userMeConfig,
setShowHidden,
setHideWatched,
setView,
setSortBy,
setSortOrder,
setGridItems,
setRefresh, setRefresh,
}: FilterbarProps) => { }: FilterbarProps) => {
const revalidator = useRevalidator();
useEffect(() => { const { userConfig, setPartialConfig } = useUserConfigStore();
(async () => { const [showHidden, setShowHidden] = useState(false);
if ( const isGridView = userConfig.config.view_style_home === ViewStyles.grid
(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]);
return ( return (
<div className="view-controls three"> <div className="view-controls three">
@@ -83,22 +32,23 @@ const Filterbar = ({
<input <input
id="hide_watched" id="hide_watched"
type="checkbox" type="checkbox"
checked={hideWatched} checked={userConfig.config.hide_watched}
onChange={() => { onChange={() => {
setHideWatched?.(!hideWatched); setRefresh?.(true);
setPartialConfig({hide_watched: !userConfig.config.hide_watched})
}} }}
/> />
{!hideWatched && ( {userConfig.config.hide_watched ? (
<label htmlFor="" className="onbtn">
On
</label>
) : (
<label htmlFor="" className="ofbtn"> <label htmlFor="" className="ofbtn">
Off Off
</label> </label>
)} )}
{hideWatched && (
<label htmlFor="" className="onbtn">
On
</label>
)}
</div> </div>
</div> </div>
@@ -109,9 +59,10 @@ const Filterbar = ({
<select <select
name="sort_by" name="sort_by"
id="sort" id="sort"
value={sortBy} value={userConfig.config.sort_by}
onChange={event => { onChange={event => {
setSortBy?.(event.target.value as SortByType); setRefresh?.(true);
setPartialConfig({sort_by: event.target.value as SortByType});
}} }}
> >
<option value="published">date published</option> <option value="published">date published</option>
@@ -119,14 +70,15 @@ const Filterbar = ({
<option value="views">views</option> <option value="views">views</option>
<option value="likes">likes</option> <option value="likes">likes</option>
<option value="duration">duration</option> <option value="duration">duration</option>
<option value="filesize">file size</option> <option value="mediasize">media size</option>
</select> </select>
<select <select
name="sort_order" name="sort_order"
id="sort-order" id="sort-order"
value={sortOrder} value={userConfig.config.sort_order}
onChange={event => { onChange={event => {
setSortOrder?.(event.target.value as SortOrderType); setRefresh?.(true);
setPartialConfig({sort_order: event.target.value as SortOrderType})
}} }}
> >
<option value="asc">asc</option> <option value="asc">asc</option>
@@ -148,22 +100,22 @@ const Filterbar = ({
/> />
)} )}
{isGridView && ( {userConfig.config.grid_items !== undefined && isGridView && (
<div className="grid-count"> <div className="grid-count">
{gridItems < 7 && ( {userConfig.config.grid_items < 7 && (
<img <img
src={iconAdd} src={iconAdd}
onClick={() => { onClick={() => {
setGridItems(gridItems + 1); setPartialConfig({grid_items: userConfig.config.grid_items + 1});
}} }}
alt="grid plus row" alt="grid plus row"
/> />
)} )}
{gridItems > 3 && ( {userConfig.config.grid_items > 3 && (
<img <img
src={iconSubstract} src={iconSubstract}
onClick={() => { onClick={() => {
setGridItems(gridItems - 1); setPartialConfig({grid_items: userConfig.config.grid_items - 1});
}} }}
alt="grid minus row" alt="grid minus row"
/> />
@@ -173,14 +125,14 @@ const Filterbar = ({
<img <img
src={iconGridView} src={iconGridView}
onClick={() => { onClick={() => {
setView('grid'); setPartialConfig({[viewStyleName]: 'grid'});
}} }}
alt="grid view" alt="grid view"
/> />
<img <img
src={iconListView} src={iconListView}
onClick={() => { onClick={() => {
setView('list'); setPartialConfig({[viewStyleName]: 'list'});
}} }}
alt="list view" alt="list view"
/> />

View File

@@ -1,18 +1,12 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import { useAuthStore } from '../stores/AuthDataStore';
export type TaUpdateType = { const Footer = () => {
version?: string;
is_breaking?: boolean;
};
interface Props {
version: string;
taUpdate?: TaUpdateType;
}
const Footer = ({ version, taUpdate }: Props) => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const { auth } = useAuthStore();
const version = auth?.version
const taUpdate = auth?.ta_update
return ( return (
<div className="footer"> <div className="footer">

View File

@@ -5,12 +5,11 @@ import iconExit from '/img/icon-exit.svg';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import NavigationItem from './NavigationItem'; import NavigationItem from './NavigationItem';
import logOut from '../api/actions/logOut'; import logOut from '../api/actions/logOut';
import loadIsAdmin from '../functions/getIsAdmin';
interface NavigationProps { const Navigation = () => {
isAdmin: boolean;
}
const Navigation = ({ isAdmin }: NavigationProps) => { const isAdmin = loadIsAdmin();
const navigate = useNavigate(); const navigate = useNavigate();
const handleLogout = async (event: { preventDefault: () => void }) => { const handleLogout = async (event: { preventDefault: () => void }) => {
event.preventDefault(); event.preventDefault();

View File

@@ -1,19 +1,22 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import { ViewLayoutType } from '../pages/Home';
import { PlaylistType } from '../pages/Playlist'; import { PlaylistType } from '../pages/Playlist';
import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription'; import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
import formatDate from '../functions/formatDates'; import formatDate from '../functions/formatDates';
import Button from './Button'; import Button from './Button';
import PlaylistThumbnail from './PlaylistThumbnail'; import PlaylistThumbnail from './PlaylistThumbnail';
import { useUserConfigStore } from '../stores/UserConfigStore';
type PlaylistListProps = { type PlaylistListProps = {
playlistList: PlaylistType[] | undefined; playlistList: PlaylistType[] | undefined;
viewLayout: ViewLayoutType;
setRefresh: (status: boolean) => void; setRefresh: (status: boolean) => void;
}; };
const PlaylistList = ({ playlistList, viewLayout, setRefresh }: PlaylistListProps) => { const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
const { userConfig } = useUserConfigStore();
const viewLayout = userConfig.config.view_style_playlist;
if (!playlistList || playlistList.length === 0) { if (!playlistList || playlistList.length === 0) {
return <p>No playlists found.</p>; return <p>No playlists found.</p>;
} }

View File

@@ -1,9 +1,9 @@
import { Link, useOutletContext } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import { OutletContextType } from '../pages/Base'; import loadIsAdmin from '../functions/getIsAdmin';
const SettingsNavigation = () => { const SettingsNavigation = () => {
const { isAdmin } = useOutletContext() as OutletContextType; const isAdmin = loadIsAdmin();
return ( return (
<> <>

View File

@@ -1,3 +1,5 @@
import { useUserConfigStore } from '../../stores/UserConfigStore';
export const ColourConstant = { export const ColourConstant = {
Dark: 'dark.css', Dark: 'dark.css',
Light: 'light.css', Light: 'light.css',
@@ -5,9 +7,11 @@ export const ColourConstant = {
Midnight: 'midnight.css', Midnight: 'midnight.css',
}; };
export type ColourVariants = 'dark.css' | 'light.css' | 'matrix.css' | 'midnight.css'; const importColours = () => {
const { userConfig } = useUserConfigStore();
const stylesheet = userConfig?.config.stylesheet
const importColours = (stylesheet: ColourVariants | undefined) => {
switch (stylesheet) { switch (stylesheet) {
case ColourConstant.Dark: case ColourConstant.Dark:
return import('./components/Dark'); return import('./components/Dark');

View File

@@ -1,7 +1,8 @@
import { UserMeType } from '../api/actions/updateUserConfig'; import { useUserConfigStore } from '../stores/UserConfigStore';
const loadIsAdmin = (config: UserMeType) => { const loadIsAdmin = () => {
const isAdmin = config.is_staff || config.is_superuser; const { userConfig } = useUserConfigStore()
const isAdmin = userConfig?.is_staff || userConfig?.is_superuser;
return isAdmin; return isAdmin;
}; };

View File

@@ -50,243 +50,77 @@ const router = createBrowserRouter(
{ {
index: true, index: true,
element: <Home />, element: <Home />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.Video(':videoId'), path: Routes.Video(':videoId'),
element: <Video />, element: <Video />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
}, },
{ {
path: Routes.Channels, path: Routes.Channels,
element: <Channels />, element: <Channels />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.Channel(':channelId'), path: Routes.Channel(':channelId'),
element: <ChannelBase />, element: <ChannelBase />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
children: [ children: [
{ {
index: true, index: true,
path: Routes.ChannelVideo(':channelId'), path: Routes.ChannelVideo(':channelId'),
element: <ChannelVideo videoType="videos" />, element: <ChannelVideo videoType="videos" />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.ChannelStream(':channelId'), path: Routes.ChannelStream(':channelId'),
element: <ChannelVideo videoType="streams" />, element: <ChannelVideo videoType="streams" />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.ChannelShorts(':channelId'), path: Routes.ChannelShorts(':channelId'),
element: <ChannelVideo videoType="shorts" />, element: <ChannelVideo videoType="shorts" />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.ChannelPlaylist(':channelId'), path: Routes.ChannelPlaylist(':channelId'),
element: <ChannelPlaylist />, element: <ChannelPlaylist />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.ChannelAbout(':channelId'), path: Routes.ChannelAbout(':channelId'),
element: <ChannelAbout />, element: <ChannelAbout />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
}, },
], ],
}, },
{ {
path: Routes.Playlists, path: Routes.Playlists,
element: <Playlists />, element: <Playlists />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.Playlist(':playlistId'), path: Routes.Playlist(':playlistId'),
element: <Playlist />, element: <Playlist />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.Downloads, path: Routes.Downloads,
element: <Download />, element: <Download />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.Search, path: Routes.Search,
element: <Search />, element: <Search />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.SettingsDashboard, path: Routes.SettingsDashboard,
element: <SettingsDashboard />, element: <SettingsDashboard />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
}, },
{ {
path: Routes.SettingsActions, path: Routes.SettingsActions,
element: <SettingsActions />, element: <SettingsActions />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
}, },
{ {
path: Routes.SettingsApplication, path: Routes.SettingsApplication,
element: <SettingsApplication />, element: <SettingsApplication />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
}, },
{ {
path: Routes.SettingsScheduling, path: Routes.SettingsScheduling,
element: <SettingsScheduling />, element: <SettingsScheduling />,
loader: async () => {
const authResponse = await loadAuth();
if (authResponse.status === 403) {
return redirect(Routes.Login);
}
return {};
},
}, },
{ {
path: Routes.SettingsUser, path: Routes.SettingsUser,
element: <SettingsUser />, element: <SettingsUser />,
loader: async () => {
const auth = await loadAuth();
if (auth.status === 403) {
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
return { userConfig };
},
}, },
{ {
path: Routes.About, path: Routes.About,

View File

@@ -1,10 +1,16 @@
import { Outlet, useLoaderData, useLocation, useSearchParams } from 'react-router-dom'; import { Outlet, useLoaderData, useLocation, useSearchParams } from 'react-router-dom';
import Footer, { TaUpdateType } from '../components/Footer'; import Footer from '../components/Footer';
import importColours from '../configuration/colours/getColours'; import importColours from '../configuration/colours/getColours';
import { UserMeType } from '../api/actions/updateUserConfig'; import { UserMeType } from '../api/actions/updateUserConfig';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Navigation from '../components/Navigation'; import Navigation from '../components/Navigation';
import loadIsAdmin from '../functions/getIsAdmin'; import { useAuthStore } from '../stores/AuthDataStore';
import { useUserConfigStore } from '../stores/UserConfigStore';
export type TaUpdateType = {
version?: string;
is_breaking?: boolean;
};
export type AuthenticationType = { export type AuthenticationType = {
response: string; response: string;
@@ -19,16 +25,16 @@ type BaseLoaderData = {
}; };
export type OutletContextType = { export type OutletContextType = {
isAdmin: boolean;
currentPage: number; currentPage: number;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
}; };
const Base = () => { const Base = () => {
const { setAuth } = useAuthStore();
const { setUserConfig } = useUserConfigStore()
const { userConfig, auth } = useLoaderData() as BaseLoaderData; const { userConfig, auth } = useLoaderData() as BaseLoaderData;
const location = useLocation();
const userMeConfig = userConfig.config; const location = useLocation();
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
@@ -37,9 +43,10 @@ const Base = () => {
const [currentPage, setCurrentPage] = useState(currentPageFromUrl); const [currentPage, setCurrentPage] = useState(currentPageFromUrl);
const [, setSearchParams] = useSearchParams(); const [, setSearchParams] = useSearchParams();
const isAdmin = loadIsAdmin(userConfig); useEffect(() => {
const version = auth.version; setAuth(auth);
const taUpdate = auth.ta_update; setUserConfig(userConfig);
}, [])
useEffect(() => { useEffect(() => {
if (currentPageFromUrl !== currentPage) { if (currentPageFromUrl !== currentPage) {
@@ -72,16 +79,16 @@ const Base = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]); }, [currentPage]);
importColours(userMeConfig.stylesheet); importColours();
return ( return (
<> <>
<div className="main-content"> <div className="main-content">
<Navigation isAdmin={isAdmin} /> <Navigation />
{/** Outlet: https://reactrouter.com/en/main/components/outlet */} {/** Outlet: https://reactrouter.com/en/main/components/outlet */}
<Outlet context={{ isAdmin, currentPage, setCurrentPage }} /> <Outlet context={{ currentPage, setCurrentPage }} />
</div> </div>
<Footer version={version} taUpdate={taUpdate} /> <Footer />
</> </>
); );
}; };

View File

@@ -12,9 +12,9 @@ import PaginationDummy from '../components/PaginationDummy';
import FormattedNumber from '../components/FormattedNumber'; import FormattedNumber from '../components/FormattedNumber';
import Button from '../components/Button'; import Button from '../components/Button';
import updateChannelOverwrites from '../api/actions/updateChannelOverwrite'; import updateChannelOverwrites from '../api/actions/updateChannelOverwrite';
import loadIsAdmin from '../functions/getIsAdmin';
export type ChannelBaseOutletContextType = { export type ChannelBaseOutletContextType = {
isAdmin: boolean;
currentPage: number; currentPage: number;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
startNotification: boolean; startNotification: boolean;
@@ -22,7 +22,6 @@ export type ChannelBaseOutletContextType = {
}; };
export type OutletContextType = { export type OutletContextType = {
isAdmin: boolean;
currentPage: number; currentPage: number;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
}; };
@@ -33,8 +32,9 @@ type ChannelAboutParams = {
const ChannelAbout = () => { const ChannelAbout = () => {
const { channelId } = useParams() as ChannelAboutParams; const { channelId } = useParams() as ChannelAboutParams;
const { isAdmin, setStartNotification } = useOutletContext() as ChannelBaseOutletContextType; const { setStartNotification } = useOutletContext() as ChannelBaseOutletContextType;
const navigate = useNavigate(); const navigate = useNavigate();
const isAdmin = loadIsAdmin();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [descriptionExpanded, setDescriptionExpanded] = useState(false); const [descriptionExpanded, setDescriptionExpanded] = useState(false);
@@ -121,7 +121,6 @@ const ChannelAbout = () => {
channelSubscribed={channel.channel_subscribed} channelSubscribed={channel.channel_subscribed}
channelThumbUrl={channel.channel_thumb_url} channelThumbUrl={channel.channel_thumb_url}
showSubscribeButton={true} showSubscribeButton={true}
isUserAdmin={isAdmin}
setRefresh={setRefresh} setRefresh={setRefresh}
/> />

View File

@@ -8,6 +8,7 @@ import { useEffect, useState } from 'react';
import ChannelBanner from '../components/ChannelBanner'; import ChannelBanner from '../components/ChannelBanner';
import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav'; import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
import loadChannelById from '../api/loader/loadChannelById'; import loadChannelById from '../api/loader/loadChannelById';
import loadIsAdmin from '../functions/getIsAdmin';
type ChannelParams = { type ChannelParams = {
channelId: string; channelId: string;
@@ -20,7 +21,8 @@ export type ChannelResponseType = {
const ChannelBase = () => { const ChannelBase = () => {
const { channelId } = useParams() as ChannelParams; const { channelId } = useParams() as ChannelParams;
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = loadIsAdmin();
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>(); const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [channelNav, setChannelNav] = useState<ChannelNavResponseType>(); const [channelNav, setChannelNav] = useState<ChannelNavResponseType>();
@@ -90,7 +92,6 @@ const ChannelBase = () => {
<Outlet <Outlet
context={{ context={{
isAdmin,
currentPage, currentPage,
setCurrentPage, setCurrentPage,
startNotification, startNotification,

View File

@@ -1,8 +1,6 @@
import { useLoaderData, useOutletContext, useParams } from 'react-router-dom'; import { useOutletContext, useParams } from 'react-router-dom';
import Notifications from '../components/Notifications'; import Notifications from '../components/Notifications';
import PlaylistList from '../components/PlaylistList'; import PlaylistList from '../components/PlaylistList';
import { ViewLayoutType } from './Home';
import { ViewStyles } from '../configuration/constants/ViewStyle';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
import Pagination from '../components/Pagination'; import Pagination from '../components/Pagination';
@@ -11,22 +9,13 @@ import loadPlaylistList from '../api/loader/loadPlaylistList';
import { PlaylistsResponseType } from './Playlists'; import { PlaylistsResponseType } from './Playlists';
import iconGridView from '/img/icon-gridview.svg'; import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg'; import iconListView from '/img/icon-listview.svg';
import { UserMeType } from '../api/actions/updateUserConfig'; import { useUserConfigStore } from '../stores/UserConfigStore';
type ChannelPlaylistLoaderDataType = {
userConfig: UserMeType;
};
const ChannelPlaylist = () => { const ChannelPlaylist = () => {
const { channelId } = useParams(); const { channelId } = useParams();
const { userConfig } = useLoaderData() as ChannelPlaylistLoaderDataType; const { userConfig, setPartialConfig } = useUserConfigStore();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const userMeConfig = userConfig.config;
const [showSubedOnly, setShowSubedOnly] = useState(userMeConfig.show_subed_only || false);
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_playlist || 'grid');
const [gridItems] = useState(userMeConfig.grid_items || 3);
const [refreshPlaylists, setRefreshPlaylists] = useState(false); const [refreshPlaylists, setRefreshPlaylists] = useState(false);
const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>(); const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>();
@@ -34,9 +23,8 @@ const ChannelPlaylist = () => {
const playlistList = playlistsResponse?.data; const playlistList = playlistsResponse?.data;
const pagination = playlistsResponse?.paginate; const pagination = playlistsResponse?.paginate;
const isGridView = view === ViewStyles.grid; const view = userConfig.config.view_style_playlist;
const gridView = isGridView ? `boxed-${gridItems}` : ''; const showSubedOnly = userConfig.config.show_subed_only;
const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -54,7 +42,7 @@ const ChannelPlaylist = () => {
<> <>
<title>TA | Channel: Playlists</title> <title>TA | Channel: Playlists</title>
<ScrollToTopOnNavigate /> <ScrollToTopOnNavigate />
<div className={`boxed-content ${gridView}`}> <div className='boxed-content'>
<Notifications pageName="channel" includeReindex={true} /> <Notifications pageName="channel" includeReindex={true} />
<div className="view-controls"> <div className="view-controls">
@@ -64,7 +52,8 @@ const ChannelPlaylist = () => {
<input <input
checked={showSubedOnly} checked={showSubedOnly}
onChange={() => { onChange={() => {
setShowSubedOnly(!showSubedOnly); setPartialConfig({show_subed_only: !showSubedOnly});
setRefreshPlaylists(true);
}} }}
type="checkbox" type="checkbox"
/> />
@@ -84,14 +73,14 @@ const ChannelPlaylist = () => {
<img <img
src={iconGridView} src={iconGridView}
onClick={() => { onClick={() => {
setView('grid'); setPartialConfig({view_style_playlist: 'grid'});
}} }}
alt="grid view" alt="grid view"
/> />
<img <img
src={iconListView} src={iconListView}
onClick={() => { onClick={() => {
setView('list'); setPartialConfig({view_style_playlist: 'list'});
}} }}
alt="list view" alt="list view"
/> />
@@ -99,11 +88,10 @@ const ChannelPlaylist = () => {
</div> </div>
</div> </div>
<div className={`boxed-content ${gridView}`}> <div className={`boxed-content`}>
<div className={`playlist-list ${view} ${gridViewGrid}`}> <div className={`playlist-list ${view}`}>
<PlaylistList <PlaylistList
playlistList={playlistList} playlistList={playlistList}
viewLayout={view}
setRefresh={setRefreshPlaylists} setRefresh={setRefreshPlaylists}
/> />
</div> </div>

View File

@@ -1,14 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Link, Link,
useLoaderData,
useOutletContext, useOutletContext,
useParams, useParams,
useSearchParams, useSearchParams,
} from 'react-router-dom'; } from 'react-router-dom';
import { SortByType, SortOrderType, ViewLayoutType } from './Home';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
import { UserMeType } from '../api/actions/updateUserConfig';
import VideoList from '../components/VideoList'; import VideoList from '../components/VideoList';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import Pagination from '../components/Pagination'; import Pagination from '../components/Pagination';
@@ -27,33 +24,23 @@ import loadVideoListByFilter, {
} from '../api/loader/loadVideoListByPage'; } from '../api/loader/loadVideoListByPage';
import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs'; import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
import humanFileSize from '../functions/humanFileSize'; import humanFileSize from '../functions/humanFileSize';
import { useUserConfigStore } from '../stores/UserConfigStore';
type ChannelParams = { type ChannelParams = {
channelId: string; channelId: string;
}; };
type ChannelVideoLoaderType = {
userConfig: UserMeType;
};
type ChannelVideoProps = { type ChannelVideoProps = {
videoType: VideoTypes; videoType: VideoTypes;
}; };
const ChannelVideo = ({ videoType }: ChannelVideoProps) => { const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
const { channelId } = useParams() as ChannelParams; const { channelId } = useParams() as ChannelParams;
const { userConfig } = useLoaderData() as ChannelVideoLoaderType; const { userConfig } = useUserConfigStore();
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId'); const videoId = searchParams.get('videoId');
const userMeConfig = userConfig.config;
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
const [sortBy, setSortBy] = useState<SortByType>(userMeConfig.sort_by || 'published');
const [sortOrder, setSortOrder] = useState<SortOrderType>(userMeConfig.sort_order || 'asc');
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>(); const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
@@ -67,37 +54,40 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
const hasVideos = videoResponse?.data?.length !== 0; const hasVideos = videoResponse?.data?.length !== 0;
const showEmbeddedVideo = videoId !== null; const showEmbeddedVideo = videoId !== null;
const view = userConfig.config.view_style_home
const isGridView = view === ViewStyles.grid; const isGridView = view === ViewStyles.grid;
const gridView = isGridView ? `boxed-${gridItems}` : ''; const gridView = isGridView ? `boxed-${userConfig.config.grid_items}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; const gridViewGrid = isGridView ? `grid-${userConfig.config.grid_items}` : '';
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if ( const channelResponse = await loadChannelById(channelId);
refresh || const videos = await loadVideoListByFilter({
pagination?.current_page === undefined || channel: channelId,
currentPage !== pagination?.current_page page: currentPage,
) { watch: userConfig.config.hide_watched ? 'unwatched' : undefined,
const channelResponse = await loadChannelById(channelId); sort: userConfig.config.sort_by,
const videos = await loadVideoListByFilter({ order: userConfig.config.sort_order,
channel: channelId, type: videoType,
page: currentPage, });
watch: hideWatched ? 'unwatched' : undefined, const channelAggs = await loadChannelAggs(channelId);
sort: sortBy,
order: sortOrder,
type: videoType,
});
const channelAggs = await loadChannelAggs(channelId);
setChannelResponse(channelResponse); setChannelResponse(channelResponse);
setVideoReponse(videos); setVideoReponse(videos);
setVideoAggsResponse(channelAggs); setVideoAggsResponse(channelAggs);
setRefresh(false); setRefresh(false);
}
})(); })();
// Do not add sort, order, hideWatched this will not work as expected!
// eslint-disable-next-line react-hooks/exhaustive-deps // 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) { if (!channel) {
return ( return (
@@ -121,7 +111,6 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
channelSubscribed={channel.channel_subscribed} channelSubscribed={channel.channel_subscribed}
channelThumbUrl={channel.channel_thumb_url} channelThumbUrl={channel.channel_thumb_url}
showSubscribeButton={true} showSubscribeButton={true}
isUserAdmin={isAdmin}
setRefresh={setRefresh} setRefresh={setRefresh}
/> />
<div className="info-box-item"> <div className="info-box-item">
@@ -172,18 +161,6 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
<div className={`boxed-content ${gridView}`}> <div className={`boxed-content ${gridView}`}>
<Filterbar <Filterbar
hideToggleText={'Hide watched videos:'} hideToggleText={'Hide watched videos:'}
view={view}
isGridView={isGridView}
hideWatched={hideWatched}
gridItems={gridItems}
sortBy={sortBy}
sortOrder={sortOrder}
userMeConfig={userMeConfig}
setSortBy={setSortBy}
setSortOrder={setSortOrder}
setHideWatched={setHideWatched}
setView={setView}
setGridItems={setGridItems}
viewStyleName={ViewStyleNames.home} viewStyleName={ViewStyleNames.home}
setRefresh={setRefresh} setRefresh={setRefresh}
/> />

View File

@@ -1,18 +1,19 @@
import { useLoaderData, useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import loadChannelList from '../api/loader/loadChannelList'; import loadChannelList from '../api/loader/loadChannelList';
import iconGridView from '/img/icon-gridview.svg'; import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg'; import iconListView from '/img/icon-listview.svg';
import iconAdd from '/img/icon-add.svg'; import iconAdd from '/img/icon-add.svg';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Pagination, { PaginationType } from '../components/Pagination'; import Pagination, { PaginationType } from '../components/Pagination';
import { ConfigType, ViewLayoutType } from './Home'; import { ConfigType } from './Home';
import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
import ChannelList from '../components/ChannelList'; import ChannelList from '../components/ChannelList';
import ScrollToTopOnNavigate from '../components/ScrollToTop'; import ScrollToTopOnNavigate from '../components/ScrollToTop';
import Notifications from '../components/Notifications'; import Notifications from '../components/Notifications';
import Button from '../components/Button'; import Button from '../components/Button';
import updateChannelSubscription from '../api/actions/updateChannelSubscription'; import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import loadIsAdmin from '../functions/getIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
type ChannelOverwritesType = { type ChannelOverwritesType = {
download_format?: string; download_format?: string;
@@ -46,21 +47,12 @@ type ChannelsListResponse = {
config?: ConfigType; config?: ConfigType;
}; };
type ChannelsLoaderDataType = {
userConfig: UserMeType;
};
const Channels = () => { const Channels = () => {
const { userConfig } = useLoaderData() as ChannelsLoaderDataType; const { userConfig, setPartialConfig } = useUserConfigStore();
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = loadIsAdmin();
const userMeConfig = userConfig.config;
const [channelListResponse, setChannelListResponse] = useState<ChannelsListResponse>(); const [channelListResponse, setChannelListResponse] = useState<ChannelsListResponse>();
const [showSubscribedOnly, setShowSubscribedOnly] = useState(
userMeConfig.show_subed_only || false,
);
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_channel || 'grid');
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(false);
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState(''); const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
@@ -72,34 +64,10 @@ const Channels = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if ( const channelListResponse = await loadChannelList(currentPage, userConfig.config.show_subed_only);
userMeConfig.view_style_channel !== view || setChannelListResponse(channelListResponse);
userMeConfig.show_subed_only !== showSubscribedOnly
) {
const userConfig: UserConfigType = {
show_subed_only: showSubscribedOnly,
view_style_channel: view,
};
await updateUserConfig(userConfig);
}
})(); })();
}, [showSubscribedOnly, userMeConfig.show_subed_only, userMeConfig.view_style_channel, view]); }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
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]);
return ( return (
<> <>
@@ -158,18 +126,19 @@ const Channels = () => {
<div className="toggleBox"> <div className="toggleBox">
<input <input
id="show_subed_only" id="show_subed_only"
onChange={() => { onChange={async () => {
setShowSubscribedOnly(!showSubscribedOnly); setPartialConfig({show_subed_only: !userConfig.config.show_subed_only});
setRefresh(true);
}} }}
type="checkbox" type="checkbox"
checked={showSubscribedOnly} checked={userConfig.config.show_subed_only}
/> />
{!showSubscribedOnly && ( {!userConfig.config.show_subed_only && (
<label htmlFor="" className="ofbtn"> <label htmlFor="" className="ofbtn">
Off Off
</label> </label>
)} )}
{showSubscribedOnly && ( {userConfig.config.show_subed_only && (
<label htmlFor="" className="onbtn"> <label htmlFor="" className="onbtn">
On On
</label> </label>
@@ -180,7 +149,7 @@ const Channels = () => {
<img <img
src={iconGridView} src={iconGridView}
onClick={() => { onClick={() => {
setView('grid'); setPartialConfig({view_style_channel: 'grid'});
}} }}
data-origin="channel" data-origin="channel"
data-value="grid" data-value="grid"
@@ -189,7 +158,7 @@ const Channels = () => {
<img <img
src={iconListView} src={iconListView}
onClick={() => { onClick={() => {
setView('list'); setPartialConfig({view_style_channel: 'list'});
}} }}
data-origin="channel" data-origin="channel"
data-value="list" data-value="list"
@@ -199,11 +168,11 @@ const Channels = () => {
</div> </div>
{hasChannels && <h2>Total channels: {channelCount}</h2>} {hasChannels && <h2>Total channels: {channelCount}</h2>}
<div className={`channel-list ${view}`}> <div className={`channel-list ${userConfig.config.view_style_channel}`}>
{!hasChannels && <h2>No channels found...</h2>} {!hasChannels && <h2>No channels found...</h2>}
{hasChannels && ( {hasChannels && (
<ChannelList channelList={channels} viewLayout={view} refreshChannelList={setRefresh} /> <ChannelList channelList={channels} refreshChannelList={setRefresh} />
)} )}
</div> </div>

View File

@@ -5,13 +5,12 @@ import iconSubstract from '/img/icon-substract.svg';
import iconGridView from '/img/icon-gridview.svg'; import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg'; import iconListView from '/img/icon-listview.svg';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { useLoaderData, useOutletContext, useSearchParams } from 'react-router-dom'; import { useOutletContext, useSearchParams } from 'react-router-dom';
import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig'; import { ConfigType } from './Home';
import { ConfigType, ViewLayoutType } from './Home';
import loadDownloadQueue from '../api/loader/loadDownloadQueue'; import loadDownloadQueue from '../api/loader/loadDownloadQueue';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
import Pagination, { PaginationType } from '../components/Pagination'; 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 updateDownloadQueue from '../api/actions/updateDownloadQueue';
import updateTaskByName from '../api/actions/updateTaskByName'; import updateTaskByName from '../api/actions/updateTaskByName';
import Notifications from '../components/Notifications'; import Notifications from '../components/Notifications';
@@ -19,6 +18,7 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop';
import Button from '../components/Button'; import Button from '../components/Button';
import DownloadListItem from '../components/DownloadListItem'; import DownloadListItem from '../components/DownloadListItem';
import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs'; import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs';
import { useUserConfigStore } from '../stores/UserConfigStore';
type Download = { type Download = {
auto_start: boolean; auto_start: boolean;
@@ -44,21 +44,13 @@ export type DownloadResponseType = {
paginate?: PaginationType; paginate?: PaginationType;
}; };
type DownloadLoaderDataType = {
userConfig: UserMeType;
};
const Download = () => { const Download = () => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const { userConfig } = useLoaderData() as DownloadLoaderDataType; const { userConfig, setPartialConfig } = useUserConfigStore();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const channelFilterFromUrl = searchParams.get('channel'); const channelFilterFromUrl = searchParams.get('channel');
const userMeConfig = userConfig.config;
const [view, setView] = useState<ViewLayoutType>(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 [refresh, setRefresh] = useState(false);
const [showHiddenForm, setShowHiddenForm] = useState(false); const [showHiddenForm, setShowHiddenForm] = useState(false);
const [downloadPending, setDownloadPending] = useState(false); const [downloadPending, setDownloadPending] = useState(false);
@@ -83,60 +75,28 @@ const Download = () => {
? downloadResponse?.data[0].channel_name ? 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 isGridView = view === ViewStyles.grid;
const gridView = isGridView ? `boxed-${gridItems}` : ''; const gridView = isGridView ? `boxed-${gridItems}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if ( const videos = await loadDownloadQueue(currentPage, channelFilterFromUrl, showIgnored);
userMeConfig.show_ignored_only !== showIgnored || const videoCount = videos?.paginate?.total_hits;
userMeConfig.grid_items !== gridItems ||
// @ts-ignore
userMeConfig[ViewStyleNames.downloads] !== view
) {
const userConfig: UserConfigType = {
show_ignored_only: showIgnored,
[ViewStyleNames.downloads]: view,
grid_items: gridItems,
};
await updateUserConfig(userConfig); if (videoCount && lastVideoCount !== videoCount) {
setRefresh(true); setLastVideoCount(videoCount);
} }
})();
}, [
view,
gridItems,
showIgnored,
userMeConfig.show_ignored_only,
userMeConfig.view_style_downloads,
userMeConfig.grid_items,
]);
useEffect(() => { setDownloadResponse(videos);
(async () => { setRefresh(false);
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);
}
})(); })();
// Do not add showIgnored otherwise it will not update the userconfig first.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [refresh, currentPage, downloadPending]); }, [refresh, showIgnored, currentPage, downloadPending]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -246,7 +206,8 @@ const Download = () => {
<input <input
id="showIgnored" id="showIgnored"
onChange={() => { onChange={() => {
setShowIgnored(!showIgnored); setPartialConfig({show_ignored_only: !showIgnored});
setRefresh(true);
}} }}
type="checkbox" type="checkbox"
checked={showIgnored} checked={showIgnored}
@@ -301,7 +262,7 @@ const Download = () => {
<img <img
src={iconAdd} src={iconAdd}
onClick={() => { onClick={() => {
setGridItems(gridItems + 1); setPartialConfig({grid_items: gridItems + 1});
}} }}
alt="grid plus row" alt="grid plus row"
/> />
@@ -310,7 +271,7 @@ const Download = () => {
<img <img
src={iconSubstract} src={iconSubstract}
onClick={() => { onClick={() => {
setGridItems(gridItems - 1); setPartialConfig({grid_items: gridItems - 1});
}} }}
alt="grid minus row" alt="grid minus row"
/> />
@@ -321,14 +282,14 @@ const Download = () => {
<img <img
src={iconGridView} src={iconGridView}
onClick={() => { onClick={() => {
setView('grid'); setPartialConfig({view_style_downloads: 'grid'});
}} }}
alt="grid view" alt="grid view"
/> />
<img <img
src={iconListView} src={iconListView}
onClick={() => { onClick={() => {
setView('list'); setPartialConfig({view_style_downloads: 'list'});
}} }}
alt="list view" alt="list view"
/> />
@@ -354,8 +315,6 @@ const Download = () => {
<Fragment key={`${download.channel_id}_${download.timestamp}`}> <Fragment key={`${download.channel_id}_${download.timestamp}`}>
<DownloadListItem <DownloadListItem
download={download} download={download}
view={view}
showIgnored={showIgnored}
setRefresh={setRefresh} setRefresh={setRefresh}
/> />
</Fragment> </Fragment>

View File

@@ -1,5 +1,6 @@
import { useRouteError } from 'react-router-dom'; 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 // This is not always the correct response
type ErrorType = { type ErrorType = {
@@ -9,7 +10,7 @@ type ErrorType = {
const ErrorPage = () => { const ErrorPage = () => {
const error = useRouteError() as ErrorType; const error = useRouteError() as ErrorType;
importColours(ColourConstant.Dark as ColourVariants); importColours();
console.error('ErrorPage', error); console.error('ErrorPage', error);

View File

@@ -1,11 +1,10 @@
import { useEffect, useState } from 'react'; 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 Routes from '../configuration/routes/RouteList';
import Pagination from '../components/Pagination'; import Pagination from '../components/Pagination';
import loadVideoListByFilter, { import loadVideoListByFilter, {
VideoListByFilterResponseType, VideoListByFilterResponseType,
} from '../api/loader/loadVideoListByPage'; } from '../api/loader/loadVideoListByPage';
import { UserMeType } from '../api/actions/updateUserConfig';
import VideoList from '../components/VideoList'; import VideoList from '../components/VideoList';
import { ChannelType } from './Channels'; import { ChannelType } from './Channels';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
@@ -14,6 +13,7 @@ import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle
import ScrollToTopOnNavigate from '../components/ScrollToTop'; import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer'; import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import { SponsorBlockType } from './Video'; import { SponsorBlockType } from './Video';
import { useUserConfigStore } from '../stores/UserConfigStore';
export type PlayerType = { export type PlayerType = {
watched: boolean; watched: boolean;
@@ -98,28 +98,18 @@ export type ConfigType = {
downloads: DownloadsType; downloads: DownloadsType;
}; };
type HomeLoaderDataType = { export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
userConfig: UserMeType;
};
export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'filesize';
export type SortOrderType = 'asc' | 'desc'; export type SortOrderType = 'asc' | 'desc';
export type ViewLayoutType = 'grid' | 'list'; export type ViewLayoutType = 'grid' | 'list';
const Home = () => { const Home = () => {
const { userConfig } = useLoaderData() as HomeLoaderDataType; const { userConfig } = useUserConfigStore();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId'); const videoId = searchParams.get('videoId');
const userMeConfig = userConfig.config; const userMeConfig = userConfig.config;
const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
const [sortBy, setSortBy] = useState<SortByType>(userMeConfig.sort_by || 'published');
const [sortOrder, setSortOrder] = useState<SortOrderType>(userMeConfig.sort_order || 'asc');
const [view, setView] = useState<ViewLayoutType>(userMeConfig.view_style_home || 'grid');
const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
const [showHidden, setShowHidden] = useState(false);
const [refreshVideoList, setRefreshVideoList] = useState(false); const [refreshVideoList, setRefreshVideoList] = useState(false);
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>(); const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
@@ -133,9 +123,9 @@ const Home = () => {
const hasVideos = videoResponse?.data?.length !== 0; const hasVideos = videoResponse?.data?.length !== 0;
const showEmbeddedVideo = videoId !== null; const showEmbeddedVideo = videoId !== null;
const isGridView = view === ViewStyles.grid; const isGridView = userMeConfig.view_style_home === ViewStyles.grid;
const gridView = isGridView ? `boxed-${gridItems}` : ''; const gridView = isGridView ? `boxed-${userMeConfig.grid_items}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; const gridViewGrid = isGridView ? `grid-${userMeConfig.grid_items}` : '';
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -146,9 +136,9 @@ const Home = () => {
) { ) {
const videos = await loadVideoListByFilter({ const videos = await loadVideoListByFilter({
page: currentPage, page: currentPage,
watch: hideWatched ? 'unwatched' : undefined, watch: userMeConfig.hide_watched ? 'unwatched' : undefined,
sort: sortBy, sort: userMeConfig.sort_by,
order: sortOrder, order: userMeConfig.sort_order,
}); });
try { try {
@@ -163,9 +153,15 @@ const Home = () => {
setRefreshVideoList(false); setRefreshVideoList(false);
} }
})(); })();
// Do not add sort, order, hideWatched this will not work as expected!
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 ( return (
<> <>
@@ -180,10 +176,10 @@ const Home = () => {
<div className="title-bar"> <div className="title-bar">
<h1>Continue Watching</h1> <h1>Continue Watching</h1>
</div> </div>
<div className={`video-list ${view} ${gridViewGrid}`}> <div className={`video-list ${userMeConfig.view_style_home} ${gridViewGrid}`}>
<VideoList <VideoList
videoList={continueVideos} videoList={continueVideos}
viewLayout={view} viewLayout={userMeConfig.view_style_home}
refreshVideoList={setRefreshVideoList} refreshVideoList={setRefreshVideoList}
/> />
</div> </div>
@@ -196,27 +192,13 @@ const Home = () => {
<Filterbar <Filterbar
hideToggleText="Hide watched:" hideToggleText="Hide watched:"
showHidden={showHidden}
hideWatched={hideWatched}
isGridView={isGridView}
view={view}
gridItems={gridItems}
sortBy={sortBy}
sortOrder={sortOrder}
userMeConfig={userMeConfig}
setShowHidden={setShowHidden}
setHideWatched={setHideWatched}
setView={setView}
setSortBy={setSortBy}
setSortOrder={setSortOrder}
setGridItems={setGridItems}
viewStyleName={ViewStyleNames.home} viewStyleName={ViewStyleNames.home}
setRefresh={setRefreshVideoList} setRefresh={setRefreshVideoList}
/> />
</div> </div>
<div className={`boxed-content ${gridView}`}> <div className={`boxed-content ${gridView}`}>
<div className={`video-list ${view} ${gridViewGrid}`}> <div className={`video-list ${userMeConfig.view_style_home} ${gridViewGrid}`}>
{!hasVideos && ( {!hasVideos && (
<> <>
<h2>No videos found...</h2> <h2>No videos found...</h2>
@@ -231,7 +213,7 @@ const Home = () => {
{hasVideos && ( {hasVideos && (
<VideoList <VideoList
videoList={videoList} videoList={videoList}
viewLayout={view} viewLayout={userMeConfig.view_style_home}
refreshVideoList={setRefreshVideoList} refreshVideoList={setRefreshVideoList}
/> />
)} )}

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import Routes from '../configuration/routes/RouteList'; import Routes from '../configuration/routes/RouteList';
import { useNavigate } from 'react-router-dom'; 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 Button from '../components/Button';
import signIn from '../api/actions/signIn'; import signIn from '../api/actions/signIn';
@@ -11,7 +11,7 @@ const Login = () => {
const [saveLogin, setSaveLogin] = useState(false); const [saveLogin, setSaveLogin] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
importColours(ColourConstant.Dark as ColourVariants); importColours();
const form_error = false; const form_error = false;

View File

@@ -1,13 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Link, Link,
useLoaderData,
useNavigate, useNavigate,
useOutletContext, useOutletContext,
useParams, useParams,
useSearchParams, useSearchParams,
} from 'react-router-dom'; } from 'react-router-dom';
import { UserMeType } from '../api/actions/updateUserConfig';
import loadPlaylistById from '../api/loader/loadPlaylistById'; import loadPlaylistById from '../api/loader/loadPlaylistById';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
import { ConfigType, VideoType, ViewLayoutType } from './Home'; import { ConfigType, VideoType, ViewLayoutType } from './Home';
@@ -30,6 +28,8 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer'; import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import Button from '../components/Button'; import Button from '../components/Button';
import loadVideoListByFilter from '../api/loader/loadVideoListByPage'; import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
import loadIsAdmin from '../functions/getIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
export type PlaylistType = { export type PlaylistType = {
playlist_active: boolean; playlist_active: boolean;
@@ -47,10 +47,6 @@ export type PlaylistType = {
_score: number; _score: number;
}; };
type PlaylistLoaderDataType = {
userConfig: UserMeType;
};
export type PlaylistResponseType = { export type PlaylistResponseType = {
data?: PlaylistType; data?: PlaylistType;
config?: ConfigType; config?: ConfigType;
@@ -68,8 +64,9 @@ const Playlist = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId'); const videoId = searchParams.get('videoId');
const { userConfig } = useLoaderData() as PlaylistLoaderDataType; const { userConfig } = useUserConfigStore();
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = loadIsAdmin();
const userMeConfig = userConfig.config; const userMeConfig = userConfig.config;

View File

@@ -1,14 +1,13 @@
import { useEffect, useState } from 'react'; 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 iconAdd from '/img/icon-add.svg';
import iconGridView from '/img/icon-gridview.svg'; import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg'; import iconListView from '/img/icon-listview.svg';
import { OutletContextType } from './Base'; import { OutletContextType } from './Base';
import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig';
import loadPlaylistList from '../api/loader/loadPlaylistList'; import loadPlaylistList from '../api/loader/loadPlaylistList';
import { ConfigType, ViewLayoutType } from './Home'; import { ConfigType } from './Home';
import Pagination, { PaginationType } from '../components/Pagination'; import Pagination, { PaginationType } from '../components/Pagination';
import PlaylistList from '../components/PlaylistList'; import PlaylistList from '../components/PlaylistList';
import { PlaylistType } from './Playlist'; import { PlaylistType } from './Playlist';
@@ -16,6 +15,8 @@ import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscriptio
import createCustomPlaylist from '../api/actions/createCustomPlaylist'; import createCustomPlaylist from '../api/actions/createCustomPlaylist';
import ScrollToTopOnNavigate from '../components/ScrollToTop'; import ScrollToTopOnNavigate from '../components/ScrollToTop';
import Button from '../components/Button'; import Button from '../components/Button';
import loadIsAdmin from '../functions/getIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
export type PlaylistEntryType = { export type PlaylistEntryType = {
youtube_id: string; youtube_id: string;
@@ -31,18 +32,11 @@ export type PlaylistsResponseType = {
paginate?: PaginationType; paginate?: PaginationType;
}; };
type PlaylistLoaderDataType = {
userConfig: UserMeType;
};
const Playlists = () => { const Playlists = () => {
const { userConfig } = useLoaderData() as PlaylistLoaderDataType; const { userConfig, setPartialConfig } = useUserConfigStore();
const { isAdmin, currentPage, setCurrentPage } = useOutletContext() as OutletContextType; 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<ViewLayoutType>(userMeConfig.view_style_playlist || 'grid');
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(false);
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [playlistsToAddText, setPlaylistsToAddText] = useState(''); const [playlistsToAddText, setPlaylistsToAddText] = useState('');
@@ -55,42 +49,21 @@ const Playlists = () => {
const hasPlaylists = playlistResponse?.data?.length !== 0; const hasPlaylists = playlistResponse?.data?.length !== 0;
useEffect(() => { const view = userConfig.config.view_style_playlist;
(async () => { const showSubedOnly = userConfig.config.show_subed_only;
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]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if ( const playlist = await loadPlaylistList({
refresh || page: currentPage,
pagination?.current_page === undefined || subscribed: showSubedOnly,
currentPage !== pagination?.current_page });
) {
const playlist = await loadPlaylistList({
page: currentPage,
subscribed: showSubedOnly,
});
setPlaylistReponse(playlist); setPlaylistReponse(playlist);
setRefresh(false); setRefresh(false);
}
})(); })();
// Do not add showSubedOnly, view this will not work as expected!
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [refresh, currentPage, pagination?.current_page]); }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
return ( return (
<> <>
@@ -170,7 +143,7 @@ const Playlists = () => {
<input <input
checked={showSubedOnly} checked={showSubedOnly}
onChange={() => { onChange={() => {
setShowSubedOnly(!showSubedOnly); setPartialConfig({show_subed_only: !showSubedOnly});
}} }}
type="checkbox" type="checkbox"
/> />
@@ -190,14 +163,14 @@ const Playlists = () => {
<img <img
src={iconGridView} src={iconGridView}
onClick={() => { onClick={() => {
setView('grid'); setPartialConfig({view_style_playlist: 'grid'});
}} }}
alt="grid view" alt="grid view"
/> />
<img <img
src={iconListView} src={iconListView}
onClick={() => { onClick={() => {
setView('list'); setPartialConfig({view_style_playlist: 'list'});
}} }}
alt="list view" alt="list view"
/> />
@@ -208,7 +181,7 @@ const Playlists = () => {
{!hasPlaylists && <h2>No playlists found...</h2>} {!hasPlaylists && <h2>No playlists found...</h2>}
{hasPlaylists && ( {hasPlaylists && (
<PlaylistList playlistList={playlistList} viewLayout={view} setRefresh={setRefresh} /> <PlaylistList playlistList={playlistList} setRefresh={setRefresh} />
)} )}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,6 @@
import { useLoaderData, useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { UserMeType } from '../api/actions/updateUserConfig';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { VideoType, ViewLayoutType } from './Home'; import { VideoType } from './Home';
import loadSearch from '../api/loader/loadSearch'; import loadSearch from '../api/loader/loadSearch';
import { PlaylistType } from './Playlist'; import { PlaylistType } from './Playlist';
import { ChannelType } from './Channels'; import { ChannelType } from './Channels';
@@ -12,6 +11,7 @@ import SubtitleList from '../components/SubtitleList';
import { ViewStyles } from '../configuration/constants/ViewStyle'; import { ViewStyles } from '../configuration/constants/ViewStyle';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer'; import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import SearchExampleQueries from '../components/SearchExampleQueries'; import SearchExampleQueries from '../components/SearchExampleQueries';
import { useUserConfigStore } from '../stores/UserConfigStore';
const EmptySearchResponse: SearchResultsType = { const EmptySearchResponse: SearchResultsType = {
results: { results: {
@@ -35,17 +35,15 @@ type SearchResultsType = {
queryType: string; queryType: string;
}; };
type SearchLoaderDataType = {
userConfig: UserMeType;
};
const Search = () => { const Search = () => {
const { userConfig } = useLoaderData() as SearchLoaderDataType; const { userConfig } = useUserConfigStore();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId'); const videoId = searchParams.get('videoId');
const userMeConfig = userConfig.config; 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 gridItems = userMeConfig.grid_items || 3;
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -72,7 +70,7 @@ const Search = () => {
const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery; const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
const isFullTextQuery = queryType === 'full' || isSimpleQuery; const isFullTextQuery = queryType === 'full' || isSimpleQuery;
const isGridView = view === ViewStyles.grid; const isGridView = viewVideos === ViewStyles.grid;
const gridView = isGridView ? `boxed-${gridItems}` : ''; const gridView = isGridView ? `boxed-${gridItems}` : '';
const gridViewGrid = isGridView ? `grid-${gridItems}` : ''; const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
@@ -116,8 +114,8 @@ const Search = () => {
{hasSearchQuery && isVideoQuery && ( {hasSearchQuery && isVideoQuery && (
<div className="multi-search-result"> <div className="multi-search-result">
<h2>Video Results</h2> <h2>Video Results</h2>
<div id="video-results" className={`video-list ${view} ${gridViewGrid}`}> <div id="video-results" className={`video-list ${viewVideos} ${gridViewGrid}`}>
<VideoList videoList={videoList} viewLayout={view} refreshVideoList={setRefresh} /> <VideoList videoList={videoList} viewLayout={viewVideos} refreshVideoList={setRefresh} />
</div> </div>
</div> </div>
)} )}
@@ -125,10 +123,9 @@ const Search = () => {
{hasSearchQuery && isChannelQuery && ( {hasSearchQuery && isChannelQuery && (
<div className="multi-search-result"> <div className="multi-search-result">
<h2>Channel Results</h2> <h2>Channel Results</h2>
<div id="channel-results" className={`channel-list ${view} ${gridViewGrid}`}> <div id="channel-results" className={`channel-list ${viewChannels} ${gridViewGrid}`}>
<ChannelList <ChannelList
channelList={channelList} channelList={channelList}
viewLayout={view}
refreshChannelList={setRefresh} refreshChannelList={setRefresh}
/> />
</div> </div>
@@ -138,10 +135,9 @@ const Search = () => {
{hasSearchQuery && isPlaylistQuery && ( {hasSearchQuery && isPlaylistQuery && (
<div className="multi-search-result"> <div className="multi-search-result">
<h2>Playlist Results</h2> <h2>Playlist Results</h2>
<div id="playlist-results" className={`playlist-list ${view} ${gridViewGrid}`}> <div id="playlist-results" className={`playlist-list ${viewPlaylists} ${gridViewGrid}`}>
<PlaylistList <PlaylistList
playlistList={playlistList} playlistList={playlistList}
viewLayout={view}
setRefresh={setRefresh} setRefresh={setRefresh}
/> />
</div> </div>

View File

@@ -1,46 +1,43 @@
import { useLoaderData, useNavigate, useOutletContext } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import updateUserConfig, { UserConfigType, UserMeType } from '../api/actions/updateUserConfig'; import { ColourVariants } from '../api/actions/updateUserConfig';
import { useEffect, useState } from 'react'; import { ColourConstant } from '../configuration/colours/getColours';
import loadUserMeConfig from '../api/loader/loadUserConfig';
import { ColourConstant, ColourVariants } from '../configuration/colours/getColours';
import SettingsNavigation from '../components/SettingsNavigation'; import SettingsNavigation from '../components/SettingsNavigation';
import Notifications from '../components/Notifications'; import Notifications from '../components/Notifications';
import Button from '../components/Button'; import Button from '../components/Button';
import { OutletContextType } from './Base'; import loadIsAdmin from '../functions/getIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
type SettingsUserLoaderData = { import { useEffect, useState } from 'react';
userConfig: UserMeType;
};
const SettingsUser = () => { const SettingsUser = () => {
const { isAdmin } = useOutletContext() as OutletContextType; const { userConfig, setPartialConfig } = useUserConfigStore();
const { userConfig } = useLoaderData() as SettingsUserLoaderData; const isAdmin = loadIsAdmin();
const navigate = useNavigate(); const navigate = useNavigate();
const userMeConfig = userConfig.config; const [styleSheet, setStyleSheet] = useState<ColourVariants>(userConfig.config.stylesheet);
const { stylesheet, page_size } = userMeConfig; const [styleSheetRefresh, setStyleSheetRefresh] = useState(false);
const [pageSize, setPageSize] = useState<number>(userConfig.config.page_size);
const [selectedStylesheet, setSelectedStylesheet] = useState(userMeConfig.stylesheet);
const [selectedPageSize, setSelectedPageSize] = useState(userMeConfig.page_size);
const [refresh, setRefresh] = useState(false);
const [userConfigResponse, setUserConfigResponse] = useState<UserConfigType>();
const stylesheetOverwritable =
userConfigResponse?.stylesheet || stylesheet || (ColourConstant.Dark as ColourVariants);
const pageSizeOverwritable = userConfigResponse?.page_size || page_size || 12;
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (refresh) { setStyleSheet(userConfig.config.stylesheet);
const userConfigResponse = await loadUserMeConfig(); setPageSize(userConfig.config.page_size);
setUserConfigResponse(userConfigResponse.config);
setRefresh(false);
navigate(0);
}
})(); })();
}, [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 ( return (
<> <>
@@ -52,74 +49,63 @@ const SettingsUser = () => {
<div className="title-bar"> <div className="title-bar">
<h1>User Configurations</h1> <h1>User Configurations</h1>
</div> </div>
<div> <div className='info-box'>
<div className="settings-group"> <div className='info-box-item'>
<h2>Stylesheet</h2> <h2>Customize user Interface</h2>
<div className="settings-item"> <div className='settings-box-wrapper'>
<p> <div>
Current stylesheet:{' '} <p>Switch your color scheme</p>
<span className="settings-current">{stylesheetOverwritable}</span> </div>
</p> <div>
<i>Select your preferred stylesheet.</i> <select
<br /> name="stylesheet"
<select id="id_stylesheet"
name="stylesheet" value={styleSheet}
id="id_stylesheet" onChange={event => {
value={selectedStylesheet} handleStyleSheetChange(event.target.value as ColourVariants);
onChange={event => { }}
setSelectedStylesheet(event.target.value as ColourVariants); >
}} {Object.entries(ColourConstant).map(([key, value]) => {
> return (
<option value="">-- change stylesheet --</option> <option key={key} value={value}>
{Object.entries(ColourConstant).map(([key, value]) => { {key}
return ( </option>
<option key={key} value={value}> );
{key} })}
</option> </select>
); {styleSheetRefresh && (
})} <button onClick={handlePageRefresh}>Refresh</button>
</select> )}
</div>
</div>
<div className='settings-box-wrapper'>
<div>
<p>Archive view page size</p>
</div>
<div>
<input
type="number"
name="page_size"
id="id_page_size"
value={pageSize || 12}
onChange={event => {
setPageSize(Number(event.target.value));
}}
/>
<div className='button-box'>
{userConfig.config.page_size !== pageSize && (
<>
<button onClick={handlePageSizeChange}>Update</button>
<button onClick={() => setPageSize(userConfig.config.page_size)}>Cancel</button>
</>
)}
</div>
</div>
</div> </div>
</div> </div>
<div className="settings-group">
<h2>Archive View</h2>
<div className="settings-item">
<p>
Current page size: <span className="settings-current">{pageSizeOverwritable}</span>
</p>
<i>Result of videos showing in archive page</i>
<br />
<input
type="number"
name="page_size"
id="id_page_size"
value={selectedPageSize}
onChange={event => {
setSelectedPageSize(Number(event.target.value));
}}
></input>
</div>
</div>
<Button
name="user-settings"
label="Update User Configurations"
onClick={async () => {
await updateUserConfig({
page_size: selectedPageSize,
stylesheet: selectedStylesheet,
});
setRefresh(true);
}}
/>
</div> </div>
{isAdmin && ( {isAdmin && (
<> <>
<div className="title-bar">
<h1>Users</h1>
</div>
<div className="settings-group"> <div className="settings-group">
<h2>User Management</h2> <h2>User Management</h2>
<p> <p>

View File

@@ -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 loadVideoById from '../api/loader/loadVideoById';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { ConfigType, VideoType } from './Home'; import { ConfigType, VideoType } from './Home';
@@ -35,9 +35,9 @@ import { PlaylistType } from './Playlist';
import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId'; import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
import CommentBox, { CommentsType } from '../components/CommentBox'; import CommentBox, { CommentsType } from '../components/CommentBox';
import Button from '../components/Button'; import Button from '../components/Button';
import { OutletContextType } from './Base';
import getApiUrl from '../configuration/getApiUrl'; import getApiUrl from '../configuration/getApiUrl';
import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav'; import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav';
import loadIsAdmin from '../functions/getIsAdmin';
const isInPlaylist = (videoId: string, playlist: PlaylistType) => { const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
return playlist.playlist_entries.some(entry => { return playlist.playlist_entries.some(entry => {
@@ -116,9 +116,9 @@ export type VideoCommentsResponseType = {
}; };
const Video = () => { const Video = () => {
const { isAdmin } = useOutletContext() as OutletContextType;
const { videoId } = useParams() as VideoParams; const { videoId } = useParams() as VideoParams;
const navigate = useNavigate(); const navigate = useNavigate();
const isAdmin = loadIsAdmin();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [videoEnded, setVideoEnded] = useState(false); const [videoEnded, setVideoEnded] = useState(false);

View File

@@ -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<AuthState>((set) => ({
auth: null,
setAuth: (auth) => set({ auth }),
}));

View File

@@ -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<UserConfigType>) => void;
}
export const useUserConfigStore = create<UserConfigState>((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<UserConfigType>) => {
const userConfigResponse = await updateUserConfig(userConfig);
set((state) => ({
userConfig: state.userConfig ? { ...state.userConfig, config: userConfigResponse } : state.userConfig,
}));
}
}))