Skip to content

Commit

Permalink
Add dashboard settings (#681)
Browse files Browse the repository at this point in the history
* Update actions modal

* Update queue processor panel to 7.5

* Update DashboardTitleToggle to 7.5

* Fix ShokoPanel loader

* Add dashboard settings (WIP)

* Add remaining dashboard settings

* Fix linter warning

* Fix fallback text in recently imported, remove `useMemo` for `isLoading`

* Add Missing Widget Options.

* Change Dependency Array value.

---------

Co-authored-by: ElementalCrisis <[email protected]>
  • Loading branch information
harshithmohan and ElementalCrisis authored Nov 19, 2023
1 parent d3b6763 commit 88c0d81
Show file tree
Hide file tree
Showing 17 changed files with 545 additions and 125 deletions.
277 changes: 277 additions & 0 deletions src/components/Dashboard/DashboardSettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import cx from 'classnames';
import { cloneDeep, toNumber } from 'lodash';

import Button from '@/components/Input/Button';
import Checkbox from '@/components/Input/Checkbox';
import InputSmall from '@/components/Input/InputSmall';
import ModalPanel from '@/components/Panels/ModalPanel';
import { useGetSettingsQuery, usePatchSettingsMutation } from '@/core/rtkQuery/splitV3Api/settingsApi';
import { setLayoutEditMode } from '@/core/slices/mainpage';
import { initialSettings } from '@/pages/settings/SettingsPage';

type Props = {
onClose: () => void;
show: boolean;
};

const Title = ({ onClose }: { onClose: () => void }) => {
const dispatch = useDispatch();

return (
<div className="flex items-center justify-between text-xl font-semibold">
Display Settings
<Button
onClick={() => {
dispatch(setLayoutEditMode(true));
onClose();
}}
buttonType="primary"
className="px-2 py-1 text-sm"
>
Enable Edit Mode
</Button>
</div>
);
};

const DashboardSettingsModal = ({ onClose, show }: Props) => {
const dispatch = useDispatch();

const [newSettings, setNewSettings] = useState(initialSettings);
const [activeTab, setActiveTab] = useState('widgets');

const settingsQuery = useGetSettingsQuery();
const settings = useMemo(() => settingsQuery?.data ?? initialSettings, [settingsQuery]);
const [patchSettings] = usePatchSettingsMutation();

useEffect(() => {
setNewSettings(settings);
}, [dispatch, settings]);

const {
combineContinueWatching,
hideCollectionStats,
hideContinueWatching,
hideImportFolders,
hideMediaType,
hideNextUp,
hideQueueProcessor,
hideR18Content,
hideRecentlyImported,
hideRecommendedAnime,
hideShokoNews,
hideUnrecognizedFiles,
hideUpcomingAnime,
recentlyImportedEpisodesCount,
recentlyImportedSeriesCount,
shokoNewsPostsCount,
} = newSettings.WebUI_Settings.dashboard;

const updateSetting = (key: keyof typeof settings.WebUI_Settings.dashboard, value: boolean | number) => {
const tempSettings = cloneDeep(newSettings);
if (
key === 'recentlyImportedEpisodesCount' || key === 'recentlyImportedSeriesCount' || key === 'shokoNewsPostsCount'
) {
tempSettings.WebUI_Settings.dashboard[key] = value as number;
} else {
tempSettings.WebUI_Settings.dashboard[key] = value as boolean;
}
setNewSettings(tempSettings);
};

const handleSave = async () => {
try {
await patchSettings({ oldSettings: settings, newSettings }).unwrap();
onClose();
} catch (error) { /* empty */ }
};

const handleCancel = () => {
setNewSettings(initialSettings);
onClose();
};

return (
<ModalPanel
show={show}
onRequestClose={onClose}
title={<Title onClose={onClose} />}
size="sm"
className="w-[45rem]"
titleLeft
noPadding
>
<div className="flex h-[25rem] flex-row gap-y-8">
<div className="flex w-[14rem] shrink-0 flex-col gap-y-8 border-r border-panel-border p-8 font-semibold">
<div className="flex flex-col gap-y-4">
<div
className={cx('cursor-pointer', activeTab === 'widgets' && 'text-panel-text-primary')}
key="widgets"
onClick={() => setActiveTab('widgets')}
>
Available Widgets
</div>
<div
className={cx('cursor-pointer', activeTab === 'options' && 'text-panel-text-primary')}
key="options"
onClick={() => setActiveTab('options')}
>
Display Options
</div>
</div>
</div>

<div className="flex w-full flex-col gap-y-8 p-8">
{activeTab === 'widgets' && (
<div className="overflow-y-scroll pr-4">
<div className="flex flex-col gap-y-2 ">
<Checkbox
justify
label="Hide Queue Processor"
id="hide-queue-processor"
isChecked={hideQueueProcessor}
onChange={event => updateSetting('hideQueueProcessor', event.target.checked)}
/>
<Checkbox
justify
label="Hide Unrecognized Files"
id="hide-unrecognized-files"
isChecked={hideUnrecognizedFiles}
onChange={event => updateSetting('hideUnrecognizedFiles', event.target.checked)}
/>
<Checkbox
justify
label="Hide Recently Imported"
id="hide-recently-imported"
isChecked={hideRecentlyImported}
onChange={event => updateSetting('hideRecentlyImported', event.target.checked)}
/>
<Checkbox
justify
label="Hide Collection Statistics"
id="hide-collection-stats"
isChecked={hideCollectionStats}
onChange={event => updateSetting('hideCollectionStats', event.target.checked)}
/>
<Checkbox
justify
label="Hide Media Type"
id="hide-media-type"
isChecked={hideMediaType}
onChange={event => updateSetting('hideMediaType', event.target.checked)}
/>
<Checkbox
justify
label="Hide Import Folders"
id="hide-import-folders"
isChecked={hideImportFolders}
onChange={event => updateSetting('hideImportFolders', event.target.checked)}
/>
<Checkbox
justify
label="Hide Shoko News"
id="hide-shoko-news"
isChecked={hideShokoNews}
onChange={event => updateSetting('hideShokoNews', event.target.checked)}
/>
<Checkbox
justify
label="Hide Continue Watching"
id="hide-continue-watching"
isChecked={hideContinueWatching}
disabled={combineContinueWatching && true}
onChange={event => updateSetting('hideContinueWatching', event.target.checked)}
/>
<Checkbox
justify
label="Hide Next Up"
id="hide-next-up"
isChecked={hideNextUp}
disabled={combineContinueWatching && true}
onChange={event => updateSetting('hideNextUp', event.target.checked)}
/>
<Checkbox
justify
label="Hide Upcoming Anime"
id="hide-upcoming-anime"
isChecked={hideUpcomingAnime}
onChange={event => updateSetting('hideUpcomingAnime', event.target.checked)}
/>
<Checkbox
justify
label="Hide Recommended Anime"
id="hide-recommended-anime"
isChecked={hideRecommendedAnime}
onChange={event => updateSetting('hideRecommendedAnime', event.target.checked)}
/>
</div>
</div>
)}

{activeTab === 'options' && (
<div className="flex flex-col gap-y-2">
<Checkbox
justify
label="Combine Continue Watching & Next Up"
id="combine-continue-watching"
isChecked={combineContinueWatching}
onChange={event => updateSetting('combineContinueWatching', event.target.checked)}
/>
<Checkbox
justify
label="Hide R18 Content"
id="hide-r18-content"
isChecked={hideR18Content}
onChange={event => updateSetting('hideR18Content', event.target.checked)}
/>
<div className="flex items-center justify-between">
Shoko News Posts
<InputSmall
id="shoko-news-posts"
type="number"
value={shokoNewsPostsCount}
onChange={event => updateSetting('shokoNewsPostsCount', toNumber(event.target.value))}
className="w-12 px-2 py-0.5 text-center"
/>
</div>
<div className="flex items-center justify-between">
Recently Imported Episodes
<InputSmall
id="recently-imported-files"
type="number"
value={recentlyImportedEpisodesCount}
onChange={event => updateSetting('recentlyImportedEpisodesCount', toNumber(event.target.value))}
className="w-12 px-2 py-0.5 text-center"
/>
</div>
<div className="flex items-center justify-between">
Recently Imported Series
<InputSmall
id="recently-imported-series"
type="number"
value={recentlyImportedSeriesCount}
onChange={event => updateSetting('recentlyImportedSeriesCount', toNumber(event.target.value))}
className="w-12 px-2 py-0.5 text-center"
/>
</div>
</div>
)}
<div className="flex justify-end gap-x-3 border-t border-panel-border pt-8 font-semibold">
<Button onClick={handleCancel} buttonType="secondary" className="px-6 py-2">Cancel</Button>
<Button
onClick={handleSave}
buttonType="primary"
className="px-6 py-2"
>
Save
</Button>
</div>
</div>
</div>
</ModalPanel>
);
};

export default DashboardSettingsModal;
10 changes: 7 additions & 3 deletions src/components/Input/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ type Props = {
id: string;
label?: string;
isChecked: boolean;
disabled?: boolean;
intermediate?: boolean;
className?: string;
labelRight?: boolean;
justify?: boolean;
onChange: React.ChangeEventHandler<HTMLInputElement>;
};

function Checkbox({ className, id, intermediate, isChecked, justify, label, labelRight, onChange }: Props) {
function Checkbox(
{ className, disabled = false, id, intermediate, isChecked, justify, label, labelRight, onChange }: Props,
) {
const [focused, setFocused] = useState(false);

return (
Expand All @@ -27,13 +30,14 @@ function Checkbox({ className, id, intermediate, isChecked, justify, label, labe
`${className}`,
'cursor-pointer flex items-center transition ease-in-out',
focused && 'ring-2 ring-panel-icon-action ring-inset',
disabled && 'opacity-50 cursor-auto',
])}
>
<input
id={id}
type="checkbox"
checked={isChecked}
onChange={onChange}
checked={!disabled && isChecked}
onChange={disabled ? undefined : onChange}
className="absolute h-0 w-0 overflow-hidden whitespace-nowrap border-0 p-0"
style={{
clip: 'rect(0 0 0 0)',
Expand Down
20 changes: 15 additions & 5 deletions src/components/Layout/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import semver from 'semver';
import { siDiscord } from 'simple-icons';
import { useEventCallback } from 'usehooks-ts';

import DashboardSettingsModal from '@/components/Dashboard/DashboardSettingsModal';
import ActionsModal from '@/components/Dialogs/ActionsModal';
import QueueModal from '@/components/Dialogs/QueueModal';
import Button from '@/components/Input/Button';
Expand All @@ -33,7 +34,7 @@ import toast from '@/components/Toast';
import { useCheckNetworkConnectivityMutation, useGetSettingsQuery } from '@/core/rtkQuery/splitV3Api/settingsApi';
import { useGetCurrentUserQuery } from '@/core/rtkQuery/splitV3Api/userApi';
import { useGetWebuiUpdateCheckQuery, usePostWebuiUpdateMutation } from '@/core/rtkQuery/splitV3Api/webuiApi';
import { setLayoutEditMode, setQueueModalOpen } from '@/core/slices/mainpage';
import { setQueueModalOpen } from '@/core/slices/mainpage';
import { NetworkAvailability } from '@/core/types/signalr';
import { initialSettings } from '@/pages/settings/SettingsPage';

Expand Down Expand Up @@ -99,6 +100,7 @@ function TopNav() {

const [showUtilitiesMenu, setShowUtilitiesMenu] = useState(false);
const [showActionsModal, setShowActionsModal] = useState(false);
const [showDashboardSettingsModal, setShowDashboardSettingsModal] = useState(false);

const isOffline = useMemo(
() => !(networkStatus === NetworkAvailability.Internet || networkStatus === NetworkAvailability.PartialInternet),
Expand All @@ -107,6 +109,7 @@ function TopNav() {

const closeModalsAndSubmenus = () => {
setShowActionsModal(false);
setShowDashboardSettingsModal(false);
setShowUtilitiesMenu(false);
};

Expand Down Expand Up @@ -219,14 +222,20 @@ function TopNav() {
id="utilities"
text="Utilities"
icon={mdiTools}
onClick={() => setShowUtilitiesMenu(prev => !prev)}
onClick={() => {
closeModalsAndSubmenus();
setShowUtilitiesMenu(prev => !prev);
}}
isHighlighted={showUtilitiesMenu}
/>
<MenuItem
id="actions"
text="Actions"
icon={mdiFormatListBulletedSquare}
onClick={() => setShowActionsModal(true)}
onClick={() => {
closeModalsAndSubmenus();
setShowActionsModal(true);
}}
isHighlighted={showActionsModal}
/>
{renderLinkMenuItem('log', 'Log', mdiTextBoxOutline)}
Expand All @@ -239,10 +248,10 @@ function TopNav() {
text="Dashboard Settings"
icon={mdiTabletDashboard}
onClick={() => {
dispatch(setLayoutEditMode(true));
closeModalsAndSubmenus();
setShowDashboardSettingsModal(true);
}}
isHighlighted={layoutEditMode}
isHighlighted={layoutEditMode || showDashboardSettingsModal}
/>
)}
{((checkWebuiUpdate.isSuccess && semver.gt(checkWebuiUpdate.data.Version, VITE_APPVERSION))
Expand Down Expand Up @@ -312,6 +321,7 @@ function TopNav() {
</AnimateHeight>
</div>
<ActionsModal show={showActionsModal} onClose={() => setShowActionsModal(false)} />
<DashboardSettingsModal show={showDashboardSettingsModal} onClose={() => setShowDashboardSettingsModal(false)} />
<QueueModal show={showQueueModal} onClose={handleQueueModalClose} />
</>
);
Expand Down
Loading

0 comments on commit 88c0d81

Please sign in to comment.