Use gql for "sources" - preferences

This commit is contained in:
schroda
2023-09-25 23:32:26 +02:00
parent dcead5a801
commit 78049282a7
8 changed files with 147 additions and 103 deletions

View File

@@ -10,14 +10,11 @@ import { ExpandLess, ExpandMore } from '@mui/icons-material';
import { Collapse, ListItemButton, ListItemText, Stack, Box } from '@mui/material'; import { Collapse, ListItemButton, ListItemText, Stack, Box } from '@mui/material';
import React from 'react'; import React from 'react';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { SourceFilters } from '@/typings'; import { ExtractByKeyValue, SourceFilters } from '@/typings';
import { Options } from '@/components/source/SourceOptions'; import { Options } from '@/components/source/SourceOptions';
// type ExcludeByName<T, K extends string> = T extends { __typename?: K } ? never : T;
type PickByName<T, K extends string> = T extends { __typename?: K } ? T : never;
interface Props { interface Props {
state: PickByName<SourceFilters, 'GroupFilter'>['filters']; state: ExtractByKeyValue<SourceFilters, '__typename', 'GroupFilter'>['filters'];
name: string; name: string;
position: number; position: number;
updateFilterValue: Function; updateFilterValue: Function;

View File

@@ -22,22 +22,29 @@ import { EditTextPreferenceProps } from '@/typings';
export default function EditTextPreference(props: EditTextPreferenceProps) { export default function EditTextPreference(props: EditTextPreferenceProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props; const {
EditTextPreferenceTitle: title,
summary,
dialogTitle,
dialogMessage,
EditTextPreferenceCurrentValue: currentValue,
updateValue,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue); const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue ?? '');
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const handleDialogCancel = () => { const handleDialogCancel = () => {
setDialogOpen(false); setDialogOpen(false);
// reset the dialog // reset the dialog
setInternalCurrentValue(currentValue); setInternalCurrentValue(currentValue ?? '');
}; };
const handleDialogSubmit = () => { const handleDialogSubmit = () => {
setDialogOpen(false); setDialogOpen(false);
updateValue(internalCurrentValue); updateValue('editTextState', internalCurrentValue);
}; };
return ( return (

View File

@@ -85,12 +85,20 @@ function ListDialog(props: IListDialogProps) {
} }
export default function ListPreference(props: ListPreferenceProps) { export default function ListPreference(props: ListPreferenceProps) {
const { title, summary, currentValue, updateValue, entryValues, entries } = props; const {
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue); ListPreferenceTitle: title,
summary,
ListPreferenceCurrentValue: currentValue,
ListPreferenceDefault: defaultValue,
updateValue,
entryValues,
entries,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue ?? '');
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setInternalCurrentValue(currentValue); setInternalCurrentValue(currentValue ?? defaultValue ?? '');
}, [currentValue]); }, [currentValue]);
const findEntryOf = (value: string) => { const findEntryOf = (value: string) => {
@@ -104,6 +112,10 @@ export default function ListPreference(props: ListPreferenceProps) {
}; };
const getSummary = () => { const getSummary = () => {
if (currentValue == null) {
return '';
}
if (summary === '%s') { if (summary === '%s') {
return findEntryOf(currentValue); return findEntryOf(currentValue);
} }
@@ -112,7 +124,7 @@ export default function ListPreference(props: ListPreferenceProps) {
const handleDialogClose = (newValue: string | null) => { const handleDialogClose = (newValue: string | null) => {
if (newValue !== null) { if (newValue !== null) {
updateValue(findEntryValueOf(newValue)); updateValue('listState', findEntryValueOf(newValue));
// appear smooth // appear smooth
setInternalCurrentValue(newValue); setInternalCurrentValue(newValue);
@@ -127,7 +139,7 @@ export default function ListPreference(props: ListPreferenceProps) {
<ListItemText primary={title} secondary={getSummary()} /> <ListItemText primary={title} secondary={getSummary()} />
</ListItemButton> </ListItemButton>
<ListDialog <ListDialog
title={title} title={title ?? ''}
open={dialogOpen} open={dialogOpen}
onClose={handleDialogClose} onClose={handleDialogClose}
value={findEntryOf(internalCurrentValue)} value={findEntryOf(internalCurrentValue)}

View File

@@ -99,32 +99,40 @@ function ListDialog(props: IListDialogProps) {
} }
export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) { export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) {
const { title, summary, currentValue, updateValue, entryValues, entries } = props; const {
const [internalCurrentValue, setInternalCurrentValue] = useState<string[]>(currentValue); MultiSelectListPreferenceTitle: title,
summary,
MultiSelectListPreferenceCurrentValue: currentValue,
MultiSelectListPreferenceDefault: defaultValue,
updateValue,
entryValues,
entries,
} = props;
const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue);
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setInternalCurrentValue(currentValue); setInternalCurrentValue(currentValue);
}, [currentValue]); }, [currentValue]);
const findEntriesOf = (values: string[]) => const findEntriesOf = (values?: string[] | null) =>
values.map((value) => { values?.map((value) => {
const idx = entryValues.indexOf(value); const idx = entryValues.indexOf(value);
return entries[idx]; return entries[idx];
}); }) ?? [];
const findEntryValuesOf = (values: string[]) => const findEntryValuesOf = (values?: string[] | null) =>
values.map((value) => { values?.map((value) => {
const idx = entries.indexOf(value); const idx = entries.indexOf(value);
return entryValues[idx]; return entryValues[idx];
}); }) ?? [];
const getSummary = () => summary; const getSummary = () => summary;
const handleDialogClose = (newValue: string[] | null) => { const handleDialogClose = (newValue: string[] | null) => {
if (newValue !== null) { if (newValue !== null) {
// console.log(newValue); // console.log(newValue);
updateValue(findEntryValuesOf(newValue)); updateValue('multiSelectState', findEntryValuesOf(newValue));
// appear smooth // appear smooth
setInternalCurrentValue(newValue); setInternalCurrentValue(newValue);
@@ -139,7 +147,7 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen
<ListItemText primary={title} secondary={getSummary()} /> <ListItemText primary={title} secondary={getSummary()} />
</ListItemButton> </ListItemButton>
<ListDialog <ListDialog
title={title} title={title ?? ''}
open={dialogOpen} open={dialogOpen}
onClose={handleDialogClose} onClose={handleDialogClose}
selectedValues={findEntriesOf(internalCurrentValue)} selectedValues={findEntriesOf(internalCurrentValue)}

View File

@@ -21,23 +21,48 @@ function getTwoStateType(type: 'Checkbox' | 'Switch') {
return Checkbox; return Checkbox;
} }
const getTwoStateValues = (
props: TwoStatePreferenceProps,
): {
title: string;
defaultValue: boolean;
currentValue?: boolean | null | undefined;
} => {
if (props.type === 'CheckBoxPreference') {
return {
title: props.CheckBoxTitle,
defaultValue: props.CheckBoxDefault,
currentValue: props.CheckBoxCheckBoxCurrentValue,
};
}
return {
title: props.SwitchPreferenceTitle,
defaultValue: props.SwitchPreferenceDefault,
currentValue: props.SwitchPreferenceCurrentValue,
};
};
function TwoSatePreference(props: TwoStatePreferenceProps) { function TwoSatePreference(props: TwoStatePreferenceProps) {
const { title, summary, currentValue, updateValue, type } = props; const { title, defaultValue, currentValue, summary, updateValue, twoStateType } = {
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue); ...props,
...getTwoStateValues(props),
};
const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue);
useEffect(() => { useEffect(() => {
setInternalCurrentValue(currentValue); setInternalCurrentValue(currentValue ?? defaultValue);
}, [currentValue]); }, [currentValue]);
return ( return (
<ListItem> <ListItem>
<ListItemText primary={title} secondary={summary} /> <ListItemText primary={title} secondary={summary} />
<ListItemSecondaryAction> <ListItemSecondaryAction>
{createElement(getTwoStateType(type), { {createElement(getTwoStateType(twoStateType), {
edge: 'end', edge: 'end',
checked: internalCurrentValue, checked: internalCurrentValue,
onChange: () => { onChange: () => {
updateValue(!currentValue); updateValue(twoStateType === 'Switch' ? 'switchState' : 'checkBoxState', !currentValue);
// appear smooth // appear smooth
setInternalCurrentValue(!currentValue); setInternalCurrentValue(!currentValue);
@@ -49,11 +74,11 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
} }
export function CheckBoxPreference(props: CheckBoxPreferenceProps) { export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
return <TwoSatePreference {...props} type="Checkbox" />; return <TwoSatePreference {...props} twoStateType="Checkbox" />;
} }
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) { export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
return <TwoSatePreference {...props} type="Switch" />; return <TwoSatePreference {...props} twoStateType="Switch" />;
} }
export default { CheckBoxPreference, SwitchPreferenceCompat }; export default { CheckBoxPreference, SwitchPreferenceCompat };

View File

@@ -24,13 +24,7 @@ import {
} from '@apollo/client'; } from '@apollo/client';
import { OperationVariables } from '@apollo/client/core'; import { OperationVariables } from '@apollo/client/core';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { import { BackupValidationResult, IChapter, IMangaChapter, PaginatedList } from '@/typings.ts';
BackupValidationResult,
IChapter,
IMangaChapter,
PaginatedList,
SourcePreferences,
} from '@/typings.ts';
import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts'; import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
import storage from '@/util/localStorage.tsx'; import storage from '@/util/localStorage.tsx';
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
@@ -95,6 +89,7 @@ import {
SetGlobalMetadataMutation, SetGlobalMetadataMutation,
SetGlobalMetadataMutationVariables, SetGlobalMetadataMutationVariables,
SetMangaMetadataMutation, SetMangaMetadataMutation,
SourcePreferenceChangeInput,
StartDownloaderMutation, StartDownloaderMutation,
StartDownloaderMutationVariables, StartDownloaderMutationVariables,
StopDownloaderMutation, StopDownloaderMutation,
@@ -123,6 +118,8 @@ import {
UpdateMangaMutation, UpdateMangaMutation,
UpdateMangaMutationVariables, UpdateMangaMutationVariables,
UpdateMangaPatchInput, UpdateMangaPatchInput,
UpdateSourcePreferencesMutation,
UpdateSourcePreferencesMutationVariables,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { GET_GLOBAL_METADATA, GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts'; import { GET_GLOBAL_METADATA, GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts';
import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts'; import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
@@ -142,7 +139,7 @@ import {
} from '@/lib/graphql/mutations/MangaMutation.ts'; } from '@/lib/graphql/mutations/MangaMutation.ts';
import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts'; import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts';
import { GET_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts';
import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation.ts'; import { GET_SOURCE_MANGAS_FETCH, UPDATE_SOURCE_PREFERENCES } from '@/lib/graphql/mutations/SourceMutation.ts';
import { import {
CLEAR_DOWNLOADER, CLEAR_DOWNLOADER,
DELETE_DOWNLOADED_CHAPTER, DELETE_DOWNLOADED_CHAPTER,
@@ -1195,15 +1192,20 @@ export class RequestManager {
); );
} }
public useGetSourcePreferences( public setSourcePreferences(
sourceId: string, source: string,
swrOptions?: SWROptions<SourcePreferences[]>, change: SourcePreferenceChangeInput,
): AbortableSWRResponse<SourcePreferences[]> { options?: MutationOptions<UpdateSourcePreferencesMutation, UpdateSourcePreferencesMutationVariables>,
return this.doRequest(HttpMethod.SWR_GET, `source/${sourceId}/preferences`, { swrOptions }); ): AbortableApolloMutationResponse<UpdateSourcePreferencesMutation> {
} return this.doRequestNew(
GQLMethod.MUTATION,
public setSourcePreferences(sourceId: string, position: number, value: string): AbortableAxiosResponse { UPDATE_SOURCE_PREFERENCES,
return this.doRequest(HttpMethod.POST, `source/${sourceId}/preferences`, { data: { position, value } }); { input: { source, change } },
{
refetchQueries: [GET_SOURCE],
...options,
},
);
} }
public useSourceSearch( public useSourceSearch(

View File

@@ -17,12 +17,13 @@ import { SwitchPreferenceCompat, CheckBoxPreference } from '@/components/sourceC
import ListPreference from '@/components/sourceConfiguration/ListPreference'; import ListPreference from '@/components/sourceConfiguration/ListPreference';
import EditTextPreference from '@/components/sourceConfiguration/EditTextPreference'; import EditTextPreference from '@/components/sourceConfiguration/EditTextPreference';
import MultiSelectListPreference from '@/components/sourceConfiguration/MultiSelectListPreference'; import MultiSelectListPreference from '@/components/sourceConfiguration/MultiSelectListPreference';
import { PreferenceProps } from '@/typings.ts';
function getPrefComponent(type: string) { function getPrefComponent(type: string) {
switch (type) { switch (type) {
case 'CheckBoxPreference': case 'CheckBoxPreference':
return CheckBoxPreference; return CheckBoxPreference;
case 'SwitchPreferenceCompat': case 'SwitchPreference':
return SwitchPreferenceCompat; return SwitchPreferenceCompat;
case 'ListPreference': case 'ListPreference':
return ListPreference; return ListPreference;
@@ -31,7 +32,7 @@ function getPrefComponent(type: string) {
case 'MultiSelectListPreference': case 'MultiSelectListPreference':
return MultiSelectListPreference; return MultiSelectListPreference;
default: default:
return CheckBoxPreference; throw new Error(`Unexpected preference type "${type}"`);
} }
} }
@@ -45,33 +46,26 @@ export default function SourceConfigure() {
}, [t]); }, [t]);
const { sourceId } = useParams<{ sourceId: string }>(); const { sourceId } = useParams<{ sourceId: string }>();
const { data: sourcePreferences = [], mutate } = requestManager.useGetSourcePreferences(sourceId); const { data } = requestManager.useGetSource(sourceId);
const sourcePreferences = data?.source.preferences ?? [];
const convertToString = (position: number, value: any): string => { const updateValue =
switch (sourcePreferences[position].props.defaultValueType) { (position: number): PreferenceProps['updateValue'] =>
case 'Set<String>': (type, value) => {
return JSON.stringify(value); requestManager.setSourcePreferences(sourceId, { position, [type]: value });
default: };
return value.toString();
}
};
const updateValue = (position: number) => (value: any) => {
requestManager
.setSourcePreferences(sourceId, position, convertToString(position, value))
.response.then(() => mutate());
};
return ( return (
<List sx={{ padding: 0 }}> <List sx={{ padding: 0 }}>
{sourcePreferences.map((it, index) => { {sourcePreferences.map((it, index) => {
const props = cloneObject(it.props); const props = cloneObject(it);
props.updateValue = updateValue(index);
props.key = index;
// TypeScript is dumb in detecting extra props // TypeScript is dumb in detecting extra props
// @ts-ignore // @ts-ignore
return createElement(getPrefComponent(it.type), props); return createElement(getPrefComponent(it.type), {
...props,
updateValue: updateValue(index),
});
})} })}
</List> </List>
); );

View File

@@ -10,7 +10,19 @@ import { OverridableComponent } from '@mui/material/OverridableComponent';
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon'; import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
import { ParseKeys } from 'i18next'; import { ParseKeys } from 'i18next';
import { Location } from 'react-router-dom'; import { Location } from 'react-router-dom';
import { ExtensionType, GetSourceQuery, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts'; import {
ExtensionType,
GetSourceQuery,
MangaType,
MetaType,
SourcePreferenceChangeInput,
} from '@/lib/graphql/generated/graphql.ts';
export type ExtractByKeyValue<T, Key extends keyof T, Value extends T[Key]> = T extends
| Record<Key, Value>
| Partial<Record<Key, Value>>
? T
: never;
export type RecursivePartial<T> = { export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[] [P in keyof T]?: T[P] extends (infer U)[]
@@ -253,47 +265,34 @@ export interface IUpdateStatus {
}; };
} }
export type SourcePreferences = GetSourceQuery['source']['preferences'][number];
export interface PreferenceProps { export interface PreferenceProps {
key: string; updateValue: <Key extends keyof Omit<SourcePreferenceChangeInput, 'position'>>(
title: string; type: Key,
summary: string; value: SourcePreferenceChangeInput[Key],
defaultValue: any; ) => void;
currentValue: any; }
defaultValueType: string;
export type TwoStatePreferenceProps = (CheckBoxPreferenceProps | SwitchPreferenceCompatProps) & {
// intetnal props // intetnal props
updateValue: any; twoStateType: 'Switch' | 'Checkbox';
} };
export interface TwoStatePreferenceProps extends PreferenceProps { export type CheckBoxPreferenceProps = PreferenceProps &
// intetnal props ExtractByKeyValue<SourcePreferences, '__typename', 'CheckBoxPreference'>;
type: 'Switch' | 'Checkbox';
}
export interface CheckBoxPreferenceProps extends PreferenceProps {} export type SwitchPreferenceCompatProps = PreferenceProps &
ExtractByKeyValue<SourcePreferences, '__typename', 'SwitchPreference'>;
export interface SwitchPreferenceCompatProps extends PreferenceProps {} export type ListPreferenceProps = PreferenceProps &
ExtractByKeyValue<SourcePreferences, '__typename', 'ListPreference'>;
export interface ListPreferenceProps extends PreferenceProps { export type MultiSelectListPreferenceProps = PreferenceProps &
entries: string[]; ExtractByKeyValue<SourcePreferences, '__typename', 'MultiSelectListPreference'>;
entryValues: string[];
}
export interface MultiSelectListPreferenceProps extends PreferenceProps { export type EditTextPreferenceProps = PreferenceProps &
entries: string[]; ExtractByKeyValue<SourcePreferences, '__typename', 'EditTextPreference'>;
entryValues: string[];
}
export interface EditTextPreferenceProps extends PreferenceProps {
dialogTitle: string;
dialogMessage: string;
text: string;
}
export interface SourcePreferences {
type: string;
props: any;
}
export interface NavbarItem { export interface NavbarItem {
path: string; path: string;