diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts deleted file mode 100644 index c7c28dc..0000000 --- a/packages/core/src/util.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function uuid() { - return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => - (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) - ); -} diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 31fe754..4eb91bc 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -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) { diff --git a/packages/interfaces/src/MetaData/Library/IGenericLibrary.ts b/packages/interfaces/src/MetaData/Library/IGenericLibrary.ts index 7ff082b..efe712d 100644 --- a/packages/interfaces/src/MetaData/Library/IGenericLibrary.ts +++ b/packages/interfaces/src/MetaData/Library/IGenericLibrary.ts @@ -1,6 +1,6 @@ import { IFileInfo } from '../IFileInfo'; -export interface IGenericLibrary { +export interface IGenericLibrary { /** * Name of the library. Must be unique. */ @@ -11,25 +11,8 @@ export interface IGenericLibrary */ upstream?: string; - /** - * Last resolved root. - */ - root: string; - - /** - * Cached index. - */ - index?: { - cid: string, - values: TData[]; - }; - /** * Type of the library. */ type: TType; } - -export function isGenericLibrary(item: any): item is IGenericLibrary { - return item?.name !== undefined && typeof item?.type === 'string'; -} diff --git a/packages/interfaces/src/MetaData/Library/ILibrary.ts b/packages/interfaces/src/MetaData/Library/ILibrary.ts index bd7de17..4d16c55 100644 --- a/packages/interfaces/src/MetaData/Library/ILibrary.ts +++ b/packages/interfaces/src/MetaData/Library/ILibrary.ts @@ -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; +export type IMovieLibrary = IGenericLibrary<'movie'>; -export type ISeriesLibrary = IGenericLibrary; +export type ISeriesLibrary = IGenericLibrary<'series'>; -export type IMusicLibrary = IGenericLibrary; +export type IMusicLibrary = IGenericLibrary<'music'>; export type ILibrary = IMovieLibrary | ISeriesLibrary | IMusicLibrary; diff --git a/packages/ui/src/components/atoms/FormList.tsx b/packages/ui/src/components/atoms/FormList.tsx new file mode 100644 index 0000000..35470f1 --- /dev/null +++ b/packages/ui/src/components/atoms/FormList.tsx @@ -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 { + renderControl: (data: Signal) => ReactNode; + values: Signal[]>; + createItem: () => TData; + label: ReadonlySignal; +} + +export function FormList(props: IFormListProps) { + const _t = useTranslation(); + + return ( + + + {props.label} + + + + + {useComputed(() => props.values.value.map((item, index) => ( + + {props.renderControl(item)} + + + )))} + + ); +} diff --git a/packages/ui/src/components/atoms/SelectInput.tsx b/packages/ui/src/components/atoms/SelectInput.tsx new file mode 100644 index 0000000..c744567 --- /dev/null +++ b/packages/ui/src/components/atoms/SelectInput.tsx @@ -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; + value: Signal; + options: { [key: string]: ReadonlySignal; }; +} + +export function SelectInput(props: ISelectInputProps) { + return ( + + {props.label} + {useComputed(() => ( + + ))} + + ); +} diff --git a/packages/ui/src/components/atoms/TextInput.tsx b/packages/ui/src/components/atoms/TextInput.tsx new file mode 100644 index 0000000..472d761 --- /dev/null +++ b/packages/ui/src/components/atoms/TextInput.tsx @@ -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; + value: Signal; + ref?: Signal; + multiline?: boolean; + rows?: number; +} + +export function TextInput(props: ITextInputProps) { + return useComputed(() => ( + { + if (props.ref) { + props.ref.value = ref; + } + }} + onChange={(ev) => { + props.value.value = ev.target.value; + }} + /> + )); +} diff --git a/packages/ui/src/components/atoms/index.ts b/packages/ui/src/components/atoms/index.ts index 535a465..966c6f4 100644 --- a/packages/ui/src/components/atoms/index.ts +++ b/packages/ui/src/components/atoms/index.ts @@ -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'; diff --git a/packages/ui/src/components/molecules/LibraryEditor.tsx b/packages/ui/src/components/molecules/LibraryEditor.tsx new file mode 100644 index 0000000..32867e8 --- /dev/null +++ b/packages/ui/src/components/molecules/LibraryEditor.tsx @@ -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; +} + +export function LibraryEditor(props: ILibraryEditorProps) { + const _t = useTranslation(); + + const name = useSignal(props.value.value.name); + const type = useSignal<'movie' | 'series' | 'music'>(props.value.value.type); + const upstream = useSignal(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 ( + + + + + + + + + + + + ); +} diff --git a/packages/ui/src/components/molecules/ProfileEditor.tsx b/packages/ui/src/components/molecules/ProfileEditor.tsx index fb6781a..02df0d7 100644 --- a/packages/ui/src/components/molecules/ProfileEditor.tsx +++ b/packages/ui/src/components/molecules/ProfileEditor.tsx @@ -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(() => { + try { + return configService.getProfile(id); + } catch (ex) { + return { + id: props.id, + libraries: [], + name: '', + type: 'internal', + } as IProfile; + } }); const name = useSignal(profile.value?.name ?? id); + const type = useSignal<'internal' | 'remote'>(profile.value?.type ?? 'internal'); + const apiUrl = useSignal(isRemoteProfile(profile.value) ? profile.value.url ?? '' : ''); + const swarmKey = useSignal(isInternalProfile(profile.value) ? profile.value.swarmKey ?? '' : ''); + const port = useSignal(isInternalProfile(profile.value) ? profile.value.port?.toString() ?? '' : ''); + const bootstrap = useSignal[]>(isInternalProfile(profile.value) ? profile.value.bootstrap?.map(i => new Signal(i)) ?? [] : []); + const libraries = useSignal[]>(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 ( - + - { - name.value = ev.target.value; - }} - /> + + + + + + + + {useComputed(() => type.value === 'internal' ? (<> + + + + + + + + ( + + )} + createItem={() => ''} + /> + + ) : (<> + + + + ))} + + ( + + )} + createItem={() => ({ + upstream: '', + name: '', + type: 'movie', + } as ILibrary)} + /> + + diff --git a/packages/ui/src/translations/de.json b/packages/ui/src/translations/de.json index 587fb5a..5f4c802 100644 --- a/packages/ui/src/translations/de.json +++ b/packages/ui/src/translations/de.json @@ -1,24 +1,37 @@ { "ActiveTasks": "Aktive aufgaben", + "Add": "Hinzufügen", "AddProfile": "Profil hinzufügen", + "ApiUrl": "Api url", "Back": "Zurück", + "Bootstrap": "Bootstrap", "Cancel": "Abbrechen", "Delete": "Löschen", "Edit": "Bearbeiten", "EditProfile": "Profil bearbeiten", "Home": "Home", + "Internal": "Internal", + "Libraries": "Bibliotheken", "Loading": "Laden...", "Logout": "Abmelden", "Movies": "Filme", + "Music": "Musik", "Name": "Name", "NoItems": "Keine Einträge", "NoNodes": "Keine verbundene Geräte", "Open": "Öffnen", "Play": "Abspielen", + "Port": "Port", + "ProfileType": "Profil Typ", + "Remote": "Remote", + "Remove": "Entfernen", "Save": "Speichern", "Search": "Suche", + "Series": "Serien", "Start": "Starten", "Starting": "Starten...", "Stopping": "Stoppen...", - "SwarmKey": "Schwarm Schlüssel" + "SwarmKey": "Schwarm Schlüssel", + "Type": "Typ", + "Upstream": "Upstream" } diff --git a/packages/ui/src/translations/en.json b/packages/ui/src/translations/en.json index bba302d..2d93eb1 100644 --- a/packages/ui/src/translations/en.json +++ b/packages/ui/src/translations/en.json @@ -1,24 +1,37 @@ { "ActiveTasks": "Currently running", + "Add": "Add", "AddProfile": "Add profile", + "ApiUrl": "Api url", "Back": "Back", + "Bootstrap": "Bootstrap", "Cancel": "Cancel", "Delete": "Delete", "Edit": "Edit", "EditProfile": "Edit profile", "Home": "Home", + "Internal": "Internal", + "Libraries": "Libraries", "Loading": "Loading...", "Logout": "Logout", "Movies": "Movies", + "Music": "Music", "Name": "Name", "NoItems": "No items", "NoNodes": "No connected nodes", "Open": "Open", "Play": "Play", + "Port": "Port", + "ProfileType": "Profile type", + "Remote": "Remote", + "Remove": "Remove", "Save": "Save", "Search": "Search", + "Series": "Series", "Start": "Start", "Starting": "Starting node...", "Stopping": "Stopping node...", - "SwarmKey": "Swarm Key" + "SwarmKey": "Swarm Key", + "Type": "Type", + "Upstream": "Upstream" }