mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-04 22:59:39 +00:00
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:
@@ -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
|
||||||
|
|||||||
36
frontend/package-lock.json
generated
36
frontend/package-lock.json
generated
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
12
frontend/src/stores/AuthDataStore.ts
Normal file
12
frontend/src/stores/AuthDataStore.ts
Normal 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 }),
|
||||||
|
}));
|
||||||
44
frontend/src/stores/UserConfigStore.ts
Normal file
44
frontend/src/stores/UserConfigStore.ts
Normal 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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user