diff --git a/.vscode/settings.json b/.vscode/settings.json index eb8c5e8..5c18a64 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "typescript.enablePromptUseWorkspaceTsdk": true, "editor.formatOnSave": true, "editor.detectIndentation": false, - "editor.insertSpaces": false + "editor.insertSpaces": false, + "typescript.format.semicolons": "insert" } diff --git a/packages/core/src/components/molecules/ProfileEditor.tsx b/packages/core/src/components/molecules/ProfileEditor.tsx new file mode 100644 index 0000000..7cfc2f2 --- /dev/null +++ b/packages/core/src/components/molecules/ProfileEditor.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useComputed, useSignal } from "@preact/signals-react"; +import { Button, Card, CardActions, CardContent, CardHeader, TextField } from "@mui/material"; +import { IConfigurationService } from '../../service'; +import { useTranslation } from 'react-i18next'; + +export function ProfileEditor(props: { id: string, configService: IConfigurationService, onCancel: () => void, onSave: () => void }) { + const { configService, id, onCancel, onSave } = props; + const [_t] = useTranslation(); + + const profile = useComputed(() => { + return configService.getProfile(id); + }); + + const name = useSignal(profile.value?.name ?? id); + + function save() { + configService.setProfile(id, { + ...(profile.value ?? {}), + name: name.value, + }); + onSave(); + } + + return ( + + + + { + name.value = ev.target.value; + }} + /> + + + + + + + ); +} diff --git a/packages/core/src/components/molecules/ProfileSelector.tsx b/packages/core/src/components/molecules/ProfileSelector.tsx index e45302a..d796042 100644 --- a/packages/core/src/components/molecules/ProfileSelector.tsx +++ b/packages/core/src/components/molecules/ProfileSelector.tsx @@ -1,17 +1,27 @@ +import { Button, ButtonGroup, Card, CardActions, CardHeader, Dialog, Stack } from "@mui/material"; +import { useComputed, useSignal } from "@preact/signals-react"; import React from "react"; -import { IConfigurationService, IProfile } from "../../service"; -import { CardActions, Button, ButtonGroup, Card, CardContent, Stack, Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { useComputed, useSignal } from "@preact/signals-react"; +import { IConfigurationService, IProfile } from "../../service"; +import { uuid } from '../../util'; +import { ProfileEditor } from "./ProfileEditor"; -export function ProfileSelector(props: { profile?: IProfile, profiles: string[], switchProfile: (name: string) => void, configService: IConfigurationService }) { +export function ProfileSelector(props: { profile?: IProfile, switchProfile: (name: string) => void, configService: IConfigurationService; }) { const { configService } = props; const [_t] = useTranslation(); const editing = useSignal(undefined); + const profiles = useSignal<(IProfile & { id: string; })[]>(loadProfiles()); + + function loadProfiles() { + return configService.getProfiles().map(p => ({ + ...configService.getProfile(p), + id: p, + })); + } const dialog = useComputed(() => { - if (editing.value !== undefined) { + if (editing.value == undefined) { return <>; } return ( @@ -19,33 +29,50 @@ export function ProfileSelector(props: { profile?: IProfile, profiles: string[], open={true} onClose={() => editing.value = undefined} > - {_t('EditProfile')} - - TODO - - - - - + editing.value = undefined} + onSave={() => { + editing.value = undefined; + profiles.value = loadProfiles(); + }} + /> ); }); + const content = useComputed(() => profiles.value.map(p => ( + + + + + + + + + + + ))); + return (<> - {props.profiles.map(p => - {p} - - - - - - - )} + {content}
- +
{dialog} ); -} \ No newline at end of file +} diff --git a/packages/core/src/components/pages/AppContext.tsx b/packages/core/src/components/pages/AppContext.tsx index d4eb291..8828e09 100644 --- a/packages/core/src/components/pages/AppContext.tsx +++ b/packages/core/src/components/pages/AppContext.tsx @@ -44,7 +44,7 @@ export function AppContextProvider(props: PropsWithChildren) { const node = useSignal(undefined); const state = useSignal(LoadState.Idle); - const accentColor = useComputed(() => '#0b3a53'); + const accentColor = useComputed(() => '#6200EE'); const darkMode = useComputed(() => true); const theme = useComputed(() => darkMode.value ? createDarkTheme(accentColor.value) : createLightTheme(accentColor.value)); @@ -82,7 +82,7 @@ export function AppContextProvider(props: PropsWithChildren) { case LoadState.Idle: return ( - + ); case LoadState.Starting: diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 654bdb7..daec6cc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,3 +10,4 @@ export type { IInternalProfile, IRemoteProfile, } from './service'; +export { uuid } from './util'; diff --git a/packages/core/src/service/IConfigurationService.ts b/packages/core/src/service/IConfigurationService.ts index 6a0c8a4..772bf32 100644 --- a/packages/core/src/service/IConfigurationService.ts +++ b/packages/core/src/service/IConfigurationService.ts @@ -7,18 +7,24 @@ export interface IConfigurationService { /** * Gets a list of all profile names. */ - getProfiles(): string[] + getProfiles(): string[]; /** - * returns the specified Profile + * returns the specified Profile. * @param name name of the profile */ getProfile(name: string): IProfile; /** - * Updates the specified Profile + * Updates the specified Profile. * @param name name of the profile * @param profile updated profile */ setProfile(name: string, profile: IProfile): void; + + /** + * Deletes the specified Profile. + * @param name name of the profile + */ + removeProfile(name: string): void; } diff --git a/packages/core/src/translations/de.json b/packages/core/src/translations/de.json index 0816b2c..e3398ae 100644 --- a/packages/core/src/translations/de.json +++ b/packages/core/src/translations/de.json @@ -1,16 +1,18 @@ { "AddProfile": "Profil hinzufügen", "Cancel": "Abbrechen", + "Delete": "Löschen", "Edit": "Bearbeiten", "EditProfile": "Profil bearbeiten", "Home": "Home", "Loading": "Laden...", "Logout": "Abmelden", "Movies": "Filme", + "Name": "Name", "Save": "Speichern", "Search": "Suche", "Start": "Starten", "Starting": "Starten...", "Stopping": "Stoppen...", "SwarmKey": "Schwarm Schlüssel" -} \ No newline at end of file +} diff --git a/packages/core/src/translations/en.json b/packages/core/src/translations/en.json index b077279..76e52f8 100644 --- a/packages/core/src/translations/en.json +++ b/packages/core/src/translations/en.json @@ -1,16 +1,18 @@ { "AddProfile": "Add profile", "Cancel": "Cancel", + "Delete": "Delete", "Edit": "Edit", "EditProfile": "Edit profile", "Home": "Home", "Loading": "Loading...", "Logout": "Logout", "Movies": "Movies", + "Name": "Name", "Save": "Save", "Search": "Search", "Start": "Start", "Starting": "Starting node...", "Stopping": "Stopping node...", "SwarmKey": "Swarm Key" -} \ No newline at end of file +} diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts new file mode 100644 index 0000000..c7c28dc --- /dev/null +++ b/packages/core/src/util.ts @@ -0,0 +1,5 @@ +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 7821c7f..9375f57 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import { IConfigurationService, INodeService, IInternalProfile, IProfile, IIpfsService, IFileInfo } from 'ipmc-core'; function getProfileFolder(name: string): string { - return `./profiles/${name}` + return `./profiles/${name}`; } const nodeService: INodeService = { @@ -94,7 +94,7 @@ const nodeService: INodeService = { const port = connString.substring(connString.lastIndexOf('/') + 1); return { async ls(cid: string) { - const files: IFileInfo[] = [] + const files: IFileInfo[] = []; for await (const file of node.ls(cid)) { files.push({ type: file.type, @@ -113,14 +113,14 @@ const nodeService: INodeService = { async peers() { return (await node.swarm.peers()).map(p => p.addr.toString()); } - } + }; } }; const configService: IConfigurationService = { getProfiles(): string[] { try { - const profiles = fs.readdirSync('./profiles') + const profiles = fs.readdirSync('./profiles'); return profiles; } catch (_) { return []; @@ -132,6 +132,9 @@ const configService: IConfigurationService = { setProfile(name: string, profile: IProfile) { fs.writeFileSync(getProfileFolder(name) + '/profile.json', JSON.stringify(profile)); }, + removeProfile(name) { + fs.rmdirSync(getProfileFolder(name), { recursive: true }); + }, }; // Use `contextBridge` APIs to expose Electron APIs to @@ -144,14 +147,14 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('configService', configService); console.log("exposeInMainWorld"); } catch (error) { - console.error(error) + console.error(error); } } else { console.log("window"); // @ts-ignore (define in dts) //window.electron = electronAPI // @ts-ignore (define in dts) - window.configService = configService + window.configService = configService; // @ts-ignore (define in dts) - window.nodeService = nodeService + window.nodeService = nodeService; } diff --git a/packages/webui/src/App.tsx b/packages/webui/src/App.tsx index 2393972..fc0d3b9 100644 --- a/packages/webui/src/App.tsx +++ b/packages/webui/src/App.tsx @@ -34,6 +34,12 @@ export function App() { window.localStorage.setItem('profiles', JSON.stringify([...profiles, name])); } }, + removeProfile(name) { + window.localStorage.removeItem('profile_' + name); + const value = window.localStorage.getItem('profiles'); + const profiles: string[] = value ? JSON.parse(value) : []; + window.localStorage.setItem('profiles', JSON.stringify(profiles.filter(p => p !== name))); + }, }} nodeService={{ async create(profile) { @@ -105,7 +111,7 @@ export function App() { const port = connString.substring(connString.lastIndexOf('/') + 1); return { async ls(cid: string) { - const files: IFileInfo[] = [] + const files: IFileInfo[] = []; for await (const file of node.ls(cid)) { files.push({ type: file.type, @@ -124,8 +130,8 @@ export function App() { peers() { return node.swarm.peers().then(r => r.map(p => p.addr.toString())); } - } + }; }, }} - /> + />; };