Skip to content

Commit

Permalink
Finish profile editor
Browse files Browse the repository at this point in the history
  • Loading branch information
undyingwraith committed Jan 15, 2025
1 parent 72a716e commit 10e58ba
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 47 deletions.
5 changes: 0 additions & 5 deletions packages/core/src/util.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ const configService: IConfigurationService = {
return JSON.parse(fs.readFileSync(getProfileFolder(name) + '/profile.json', 'utf-8'));
},
setProfile(name: string, profile: IProfile) {
if (!fs.existsSync(getProfileFolder(name))) {
fs.mkdirSync(getProfileFolder(name));
}
fs.writeFileSync(getProfileFolder(name) + '/profile.json', JSON.stringify(profile));
},
removeProfile(name) {
Expand Down
19 changes: 1 addition & 18 deletions packages/interfaces/src/MetaData/Library/IGenericLibrary.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IFileInfo } from '../IFileInfo';

export interface IGenericLibrary<TData extends IFileInfo, TType extends string> {
export interface IGenericLibrary<TType extends string> {
/**
* Name of the library. Must be unique.
*/
Expand All @@ -11,25 +11,8 @@ export interface IGenericLibrary<TData extends IFileInfo, TType extends string>
*/
upstream?: string;

/**
* Last resolved root.
*/
root: string;

/**
* Cached index.
*/
index?: {
cid: string,
values: TData[];
};

/**
* Type of the library.
*/
type: TType;
}

export function isGenericLibrary<TData extends IFileInfo, TType extends string>(item: any): item is IGenericLibrary<TData, TType> {
return item?.name !== undefined && typeof item?.type === 'string';
}
10 changes: 4 additions & 6 deletions packages/interfaces/src/MetaData/Library/ILibrary.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { IMovieMetaData } from './IMovieMetaData';
import { IGenericLibrary, isGenericLibrary } from './IGenericLibrary';
import { ISeriesMetaData } from './ISeriesMetaData';
import { IGenericLibrary } from './IGenericLibrary';

export type IMovieLibrary = IGenericLibrary<IMovieMetaData, 'movie'>;
export type IMovieLibrary = IGenericLibrary<'movie'>;

export type ISeriesLibrary = IGenericLibrary<ISeriesMetaData, 'series'>;
export type ISeriesLibrary = IGenericLibrary<'series'>;

export type IMusicLibrary = IGenericLibrary<any, 'music'>;
export type IMusicLibrary = IGenericLibrary<'music'>;

export type ILibrary = IMovieLibrary | ISeriesLibrary | IMusicLibrary;
42 changes: 42 additions & 0 deletions packages/ui/src/components/atoms/FormList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Button, Typography } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { ReadonlySignal, Signal, useComputed } from '@preact/signals-react';
import { useTranslation } from '@src/hooks';
import React, { ReactNode } from 'react';

interface IFormListProps<TData> {
renderControl: (data: Signal<TData>) => ReactNode;
values: Signal<Signal<TData>[]>;
createItem: () => TData;
label: ReadonlySignal<string>;
}

export function FormList<TData>(props: IFormListProps<TData>) {
const _t = useTranslation();

return (
<Grid container spacing={1}>
<Grid size={8}>
<Typography>{props.label}</Typography>
</Grid>
<Grid size={4}>
<Button
fullWidth={true}
onClick={() => {
props.values.value = [...props.values.value, new Signal(props.createItem())];
}}
>
{_t('Add')}
</Button>
</Grid>
{useComputed(() => props.values.value.map((item, index) => (
<Grid size={12}>
{props.renderControl(item)}
<Button onClick={() => {
props.values.value = props.values.value.toSpliced(index, 1);
}}>{_t('Remove')}</Button>
</Grid>
)))}
</Grid>
);
}
30 changes: 30 additions & 0 deletions packages/ui/src/components/atoms/SelectInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
import { ReadonlySignal, Signal, useComputed } from '@preact/signals-react';
import React from 'react';

interface ISelectInputProps {
label?: ReadonlySignal<string>;
value: Signal<string>;
options: { [key: string]: ReadonlySignal<string>; };
}

export function SelectInput(props: ISelectInputProps) {
return (
<FormControl fullWidth>
<InputLabel>{props.label}</InputLabel>
{useComputed(() => (
<Select
label={props.label?.value}
value={props.value.value}
onChange={(ev) => {
props.value.value = ev.target.value;
}}
>
{Object.entries(props.options).map(([value, label]) => (
<MenuItem value={value}>{label}</MenuItem>
))}
</Select>
))}
</FormControl>
);
}
31 changes: 31 additions & 0 deletions packages/ui/src/components/atoms/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { TextField } from '@mui/material';
import { ReadonlySignal, Signal, useComputed } from '@preact/signals-react';

interface ITextInputProps {
label?: ReadonlySignal<string>;
value: Signal<string>;
ref?: Signal<HTMLInputElement | null>;
multiline?: boolean;
rows?: number;
}

export function TextInput(props: ITextInputProps) {
return useComputed(() => (
<TextField
fullWidth={true}
rows={props.rows}
multiline={props.multiline}
label={props.label?.value}
value={props.value.value}
inputRef={ref => {
if (props.ref) {
props.ref.value = ref;
}
}}
onChange={(ev) => {
props.value.value = ev.target.value;
}}
/>
));
}
6 changes: 5 additions & 1 deletion packages/ui/src/components/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { FormList } from './FormList';
export { Identicon } from './Identicon';
export { ImageView } from './ImageView';
export { Loader } from './Loader';
export { Identicon } from './Identicon';
export { SelectInput } from './SelectInput';
export { Spacer } from './Spacer';
export { TextInput } from './TextInput';
export { ThemeToggle } from './ThemeToggle';
58 changes: 58 additions & 0 deletions packages/ui/src/components/molecules/LibraryEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Grid from '@mui/material/Grid2';
import { Signal, useSignal } from '@preact/signals-react';
import { useTranslation } from '@src/hooks';
import { ILibrary } from 'ipmc-interfaces';
import React, { useEffect } from 'react';
import { SelectInput, TextInput } from '../atoms';

interface ILibraryEditorProps {
value: Signal<ILibrary>;
}

export function LibraryEditor(props: ILibraryEditorProps) {
const _t = useTranslation();

const name = useSignal<string>(props.value.value.name);
const type = useSignal<'movie' | 'series' | 'music'>(props.value.value.type);
const upstream = useSignal<string>(props.value.value.upstream ?? '');

useEffect(() => {
name.subscribe((value) => {
props.value.value = { ...props.value.value, name: value };
});
type.subscribe((value) => {
props.value.value = { ...props.value.value, type: value };
});
upstream.subscribe((value) => {
props.value.value = { ...props.value.value, upstream: value };
});
});

return (
<Grid container spacing={2}>
<Grid size={8}>
<TextInput
value={name}
label={_t('Name')}
/>
</Grid>
<Grid size={4}>
<SelectInput
value={type}
label={_t('Type')}
options={{
'movie': _t('Movies'),
'series': _t('Series'),
'music': _t('Music'),
}}
/>
</Grid>
<Grid size={12}>
<TextInput
value={upstream}
label={_t('Upstream')}
/>
</Grid>
</Grid>
);
}
122 changes: 107 additions & 15 deletions packages/ui/src/components/molecules/ProfileEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,130 @@
import { Button, Card, CardActions, CardContent, CardHeader } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { Signal, useComputed, useSignal } from '@preact/signals-react';
import { IConfigurationService, ILibrary, IProfile, isInternalProfile, isRemoteProfile } from 'ipmc-interfaces';
import React from 'react';
import { useComputed, useSignal } from "@preact/signals-react";
import { Button, Card, CardActions, CardContent, CardHeader, TextField } from "@mui/material";
import { useTranslation } from 'react-i18next';
import { IConfigurationService } from 'ipmc-interfaces';
import { useTranslation } from '../../hooks';
import { FormList, SelectInput, TextInput } from '../atoms';
import { LibraryEditor } from './LibraryEditor';

export function ProfileEditor(props: { id: string, configService: IConfigurationService, onCancel: () => void, onSave: () => void; }) {
const { configService, id, onCancel, onSave } = props;
const [_t] = useTranslation();
const _t = useTranslation();

const profile = useComputed(() => {
return configService.getProfile(id);
const profile = useComputed<IProfile>(() => {
try {
return configService.getProfile(id);
} catch (ex) {
return {
id: props.id,
libraries: [],
name: '',
type: 'internal',
} as IProfile;
}
});

const name = useSignal<string>(profile.value?.name ?? id);
const type = useSignal<'internal' | 'remote'>(profile.value?.type ?? 'internal');
const apiUrl = useSignal<string>(isRemoteProfile(profile.value) ? profile.value.url ?? '' : '');
const swarmKey = useSignal<string>(isInternalProfile(profile.value) ? profile.value.swarmKey ?? '' : '');
const port = useSignal<string>(isInternalProfile(profile.value) ? profile.value.port?.toString() ?? '' : '');
const bootstrap = useSignal<Signal<string>[]>(isInternalProfile(profile.value) ? profile.value.bootstrap?.map(i => new Signal(i)) ?? [] : []);
const libraries = useSignal<Signal<ILibrary>[]>(profile.value.libraries.map(i => new Signal(i)));

function save() {
configService.setProfile(id, {
...(profile.value ?? {}),
name: name.value,
type: type.value,
...(type.value === 'internal' ? {
swarmKey: swarmKey.value === '' ? undefined : swarmKey.value,
port: port.value === '' ? undefined : parseInt(port.value),
bootstrap: bootstrap.value.map(s => s.value),
} : {
apiUrl: apiUrl.value === '' ? undefined : apiUrl.value,
}),
libraries: libraries.value.map(l => l.value),
});
onSave();
}

return (
<Card>
<Card sx={{ maxHeight: '100%', overflow: 'auto' }}>
<CardHeader title={_t('EditProfile')} />
<CardContent>
<TextField
label={_t('Name')}
value={name}
onChange={(ev) => {
name.value = ev.target.value;
}}
/>
<Grid container spacing={2}>
<Grid size={8}>
<TextInput
label={_t('Name')}
value={name}
/>
</Grid>
<Grid size={4}>
<SelectInput
value={type}
label={_t('ProfileType')}
options={{
'internal': _t('Internal'),
'remote': _t('Remote'),
}}
/>
</Grid>
{useComputed(() => type.value === 'internal' ? (<>
<Grid size={12}>
<TextInput
label={_t('SwarmKey')}
value={swarmKey}
key={'swarmKey'}
multiline={true}
rows={3}
/>
</Grid>
<Grid size={12}>
<TextInput
label={_t('Port')}
value={port}
key={'port'}
/>
</Grid>
<Grid size={12}>
<FormList
label={_t('Bootstrap')}
values={bootstrap}
renderControl={(item) => (
<TextInput
value={item}
/>
)}
createItem={() => ''}
/>
</Grid>
</>) : (<>
<Grid size={12}>
<TextInput
label={_t('ApiUrl')}
value={apiUrl}
key={'apiUrl'}
/>
</Grid>
</>))}
<Grid size={12}>
<FormList
label={_t('Libraries')}
values={libraries}
renderControl={(item) => (
<LibraryEditor
value={item}
/>
)}
createItem={() => ({
upstream: '',
name: '',
type: 'movie',
} as ILibrary)}
/>
</Grid>
</Grid>
</CardContent>
<CardActions>
<Button onClick={() => onCancel()}>{_t('Cancel')}</Button>
Expand Down
Loading

0 comments on commit 10e58ba

Please sign in to comment.