From f8e7d02daf46b2051adddc134dcffa46d2dd184c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 24 Oct 2022 22:19:52 -0700 Subject: [PATCH] Update frontend base --- .../servers/components/AddServerModal.tsx | 85 ------- .../servers/components/EditServerModal.tsx | 63 ----- .../servers/components/ServerList.module.scss | 8 - .../servers/components/ServerList.tsx | 75 ------ .../components/add-server-credential-form.tsx | 105 +++++++++ .../servers/components/add-server-form.tsx | 95 ++++++++ .../components/add-server-url-form.tsx | 59 +++++ .../servers/components/edit-server-form.tsx | 102 ++++++++ .../servers/components/server-list-item.tsx | 222 ++++++++++++++++++ .../servers/components/server-list.tsx | 65 +++++ .../servers/components/server-section.tsx | 27 +++ src/renderer/features/servers/index.ts | 9 +- .../mutations/use-create-server-url.ts | 35 +++ .../servers/mutations/use-create-server.ts | 36 +++ .../mutations/use-delete-server-url.ts | 60 +++++ .../mutations/use-disable-server-url.ts | 55 +++++ .../mutations/use-enable-server-url.ts | 55 +++++ .../servers/mutations/use-update-server.ts | 69 ++++++ .../servers/queries/use-server-list.ts | 67 ++++++ .../features/servers/queries/useServers.ts | 68 ------ .../features/servers/routes/ServersRoute.tsx | 11 - .../validate-server.ts} | 21 +- .../features/titlebar/components/Titlebar.tsx | 79 +++++-- .../titlebar/components/TitlebarButton.tsx | 0 .../features/titlebar/components/app-menu.tsx | 107 +++++++++ .../{UserMenu.tsx => user-menu.tsx} | 5 - src/renderer/features/titlebar/index.ts | 3 +- .../features/users/queries/use-user-detail.ts | 16 ++ ...WindowControls.tsx => window-controls.tsx} | 61 ++--- .../features/window-controls/index.ts | 2 +- .../{auth/AuthLayout.tsx => auth-layout.tsx} | 4 +- src/renderer/layouts/default-layout.tsx | 181 ++++++++++++++ .../layouts/default/DefaultLayout.tsx | 90 ------- .../default/utils/constrainSidebarWidth.ts | 11 - src/renderer/layouts/index.ts | 4 +- 35 files changed, 1464 insertions(+), 491 deletions(-) delete mode 100644 src/renderer/features/servers/components/AddServerModal.tsx delete mode 100644 src/renderer/features/servers/components/EditServerModal.tsx delete mode 100644 src/renderer/features/servers/components/ServerList.module.scss delete mode 100644 src/renderer/features/servers/components/ServerList.tsx create mode 100644 src/renderer/features/servers/components/add-server-credential-form.tsx create mode 100644 src/renderer/features/servers/components/add-server-form.tsx create mode 100644 src/renderer/features/servers/components/add-server-url-form.tsx create mode 100644 src/renderer/features/servers/components/edit-server-form.tsx create mode 100644 src/renderer/features/servers/components/server-list-item.tsx create mode 100644 src/renderer/features/servers/components/server-list.tsx create mode 100644 src/renderer/features/servers/components/server-section.tsx create mode 100644 src/renderer/features/servers/mutations/use-create-server-url.ts create mode 100644 src/renderer/features/servers/mutations/use-create-server.ts create mode 100644 src/renderer/features/servers/mutations/use-delete-server-url.ts create mode 100644 src/renderer/features/servers/mutations/use-disable-server-url.ts create mode 100644 src/renderer/features/servers/mutations/use-enable-server-url.ts create mode 100644 src/renderer/features/servers/mutations/use-update-server.ts create mode 100644 src/renderer/features/servers/queries/use-server-list.ts delete mode 100644 src/renderer/features/servers/queries/useServers.ts delete mode 100644 src/renderer/features/servers/routes/ServersRoute.tsx rename src/renderer/features/servers/{queries/useCreateServer.ts => utils/validate-server.ts} (73%) delete mode 100644 src/renderer/features/titlebar/components/TitlebarButton.tsx create mode 100644 src/renderer/features/titlebar/components/app-menu.tsx rename src/renderer/features/titlebar/components/{UserMenu.tsx => user-menu.tsx} (88%) create mode 100644 src/renderer/features/users/queries/use-user-detail.ts rename src/renderer/features/window-controls/components/{WindowControls.tsx => window-controls.tsx} (51%) rename src/renderer/layouts/{auth/AuthLayout.tsx => auth-layout.tsx} (85%) create mode 100644 src/renderer/layouts/default-layout.tsx delete mode 100644 src/renderer/layouts/default/DefaultLayout.tsx delete mode 100644 src/renderer/layouts/default/utils/constrainSidebarWidth.ts diff --git a/src/renderer/features/servers/components/AddServerModal.tsx b/src/renderer/features/servers/components/AddServerModal.tsx deleted file mode 100644 index d1500fc7b..000000000 --- a/src/renderer/features/servers/components/AddServerModal.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { - Button, - Checkbox, - Modal, - ModalProps, - PasswordInput, - SegmentedControl, - Stack, - TextInput, -} from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useTranslation } from 'react-i18next'; -import { useCreateServer, validateServer } from '../queries/useCreateServer'; - -export const AddServerModal = ({ ...rest }: ModalProps) => { - const { t } = useTranslation(); - - const form = useForm({ - initialValues: { - legacyAuth: false, - name: '', - password: '', - serverType: 'jellyfin', - url: 'http://', - username: '', - }, - }); - - const createServerMutation = useCreateServer(); - - return ( - -
{ - const res = await validateServer(values); - - if (res?.token) { - createServerMutation.mutate({ - ...values, - remoteUserId: res.userId, - token: res.token, - }); - } - })} - > - - - - - - - {form.getInputProps('serverType').value === 'subsonic' && ( - - )} - - -
-
- ); -}; diff --git a/src/renderer/features/servers/components/EditServerModal.tsx b/src/renderer/features/servers/components/EditServerModal.tsx deleted file mode 100644 index 0de734125..000000000 --- a/src/renderer/features/servers/components/EditServerModal.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { - Button, - Checkbox, - ModalProps, - PasswordInput, - SegmentedControl, - Stack, - TextInput, -} from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useTranslation } from 'react-i18next'; -import { ServerResponse } from '../../../api/types'; - -interface EditServerModalProps extends ModalProps { - server: ServerResponse | undefined; -} - -export const EditServerModal = ({ server }: EditServerModalProps) => { - const { t } = useTranslation(); - - const form = useForm({ - initialValues: { - legacyAuth: false, - name: server?.name, - password: '', - serverType: server?.serverType, - url: server?.url, - username: server?.username, - }, - }); - - return ( -
{})}> - - - - - - - {form.getInputProps('serverType').value === 'subsonic' && ( - - )} - - -
- ); -}; diff --git a/src/renderer/features/servers/components/ServerList.module.scss b/src/renderer/features/servers/components/ServerList.module.scss deleted file mode 100644 index 0a89ed7ca..000000000 --- a/src/renderer/features/servers/components/ServerList.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.item { - display: flex; - justify-content: space-between; - max-width: 50vw; - margin: 1rem; - padding: 1rem; - outline: 1px #fff solid; -} diff --git a/src/renderer/features/servers/components/ServerList.tsx b/src/renderer/features/servers/components/ServerList.tsx deleted file mode 100644 index eba606715..000000000 --- a/src/renderer/features/servers/components/ServerList.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Button, Text } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; -import { ServerResponse } from '../../../api/types'; -import { useServers } from '../queries/useServers'; -import { EditServerModal } from './EditServerModal'; -import styles from './ServerList.module.scss'; - -export const ServerList = () => { - const { data: servers } = useServers(); - const [editServerModal, editServerHandlers] = useDisclosure(false); - const [opened, setOpened] = useState(false); - - const [selectedServer, setSelectedServer] = useState< - ServerResponse | undefined - >(); - - const handleClickServer = (server: ServerResponse) => { - setSelectedServer(server); - editServerHandlers.open(); - }; - - const handleKeyDownServer = ( - e: React.KeyboardEvent, - server: ServerResponse - ) => { - if (e.key === ' ' || e.key === 'Enter') { - setSelectedServer(server); - editServerHandlers.open(); - } - }; - - const handleCloseModal = () => { - setOpened(false); - editServerHandlers.close(); - }; - - useEffect(() => { - if (editServerModal === true) { - setTimeout(() => setOpened(true)); - } else { - setTimeout(() => setSelectedServer(undefined), 100); - } - }, [editServerModal]); - - return ( - <> - {servers && - servers.data.map((server) => ( - <> -
handleClickServer(server)} - onKeyDown={(e) => handleKeyDownServer(e, server)} - > -
- {server.name} - Hello -
- -
- {selectedServer && ( - - )} - - ))} - - ); -}; diff --git a/src/renderer/features/servers/components/add-server-credential-form.tsx b/src/renderer/features/servers/components/add-server-credential-form.tsx new file mode 100644 index 000000000..f83a62912 --- /dev/null +++ b/src/renderer/features/servers/components/add-server-credential-form.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react'; +import { Checkbox, Stack, Group } from '@mantine/core'; +import { useForm, zodResolver } from '@mantine/form'; +import { useFocusTrap } from '@mantine/hooks'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { remoteServerLogin } from '@/renderer/api/shared.api'; +import { Server, ServerType } from '@/renderer/api/types'; +import { Button, PasswordInput, TextInput, toast } from '@/renderer/components'; +import { useAuthStore } from '@/renderer/store'; +import { randomString } from '@/renderer/utils'; + +interface AddServerCredentialFormProps { + onCancel: () => void; + server: Server; +} + +const schema = z.object({ + legacyAuth: z.boolean().optional(), + password: z.string().min(1), + username: z.string().min(1), +}); + +export const AddServerCredentialForm = ({ + onCancel, + server, +}: AddServerCredentialFormProps) => { + const { t } = useTranslation(); + const [isSubmitting, setIsSubmitting] = useState(false); + const focusTrapRef = useFocusTrap(true); + const addServerCredential = useAuthStore( + (state) => state.addServerCredential + ); + + const form = useForm({ + initialValues: { + legacy: false, + password: '', + url: 'http://', + username: '', + }, + validate: zodResolver(schema), + }); + + const handleSubmit = form.onSubmit(async (values) => { + setIsSubmitting(true); + const auth = await remoteServerLogin({ + ...values, + type: server.type, + url: server.url, + }); + + if (auth.type === 'error') { + setIsSubmitting(false); + return toast.show({ + message: auth.message, + title: 'Failed to add credential', + type: 'error', + }); + } + + addServerCredential({ + enabled: false, + id: randomString(), + serverId: server.id, + token: auth.token, + username: auth.username!, + }); + + setIsSubmitting(false); + return onCancel(); + }); + + return ( +
+ + + + {server.type === ServerType.SUBSONIC && ( + + )} + + + + + +
+ ); +}; diff --git a/src/renderer/features/servers/components/add-server-form.tsx b/src/renderer/features/servers/components/add-server-form.tsx new file mode 100644 index 000000000..f4e71e644 --- /dev/null +++ b/src/renderer/features/servers/components/add-server-form.tsx @@ -0,0 +1,95 @@ +import { Checkbox, Stack, Group } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useFocusTrap } from '@mantine/hooks'; +import { closeAllModals } from '@mantine/modals'; +import { useTranslation } from 'react-i18next'; +import { ServerType } from '@/renderer/api/types'; +import { + Button, + PasswordInput, + TextInput, + SegmentedControl, +} from '@/renderer/components'; +import { useCreateServer } from '@/renderer/features/servers/mutations/use-create-server'; + +const SERVER_TYPES = [ + { label: 'Jellyfin', value: ServerType.JELLYFIN }, + { label: 'Navidrome', value: ServerType.NAVIDROME }, + { label: 'Subsonic', value: ServerType.SUBSONIC }, +]; + +interface AddServerFormProps { + onCancel: () => void; +} + +export const AddServerForm = ({ onCancel }: AddServerFormProps) => { + const { t } = useTranslation(); + const focusTrapRef = useFocusTrap(true); + + const form = useForm({ + initialValues: { + legacyAuth: false, + name: '', + password: '', + type: ServerType.JELLYFIN, + url: 'http://', + username: '', + }, + }); + + const createServerMutation = useCreateServer(); + + return ( +
{ + createServerMutation.mutate( + { body: values }, + { onSuccess: () => closeAllModals() } + ); + })} + > + + + + + + + {form.values.type === ServerType.SUBSONIC && ( + + )} + + + + + +
+ ); +}; diff --git a/src/renderer/features/servers/components/add-server-url-form.tsx b/src/renderer/features/servers/components/add-server-url-form.tsx new file mode 100644 index 000000000..94e26cd94 --- /dev/null +++ b/src/renderer/features/servers/components/add-server-url-form.tsx @@ -0,0 +1,59 @@ +import { Stack, Group } from '@mantine/core'; +import { useForm, zodResolver } from '@mantine/form'; +import { useFocusTrap } from '@mantine/hooks'; +import { z } from 'zod'; +import { Button, TextInput } from '@/renderer/components'; +import { useCreateServerUrl } from '@/renderer/features/servers/mutations/use-create-server-url'; + +interface AddServerUrlFormProps { + onCancel: () => void; + serverId: string; +} + +const schema = z.object({ + url: z.string().url({ message: 'Invalid URL' }), +}); + +export const AddServerUrlForm = ({ + serverId, + onCancel, +}: AddServerUrlFormProps) => { + const focusTrapRef = useFocusTrap(true); + + const form = useForm({ + initialValues: { + url: 'http://', + }, + validate: zodResolver(schema), + }); + + const mutation = useCreateServerUrl(); + + const handleSubmit = form.onSubmit((values) => { + mutation.mutate( + { body: values, query: { serverId } }, + { onSuccess: () => onCancel() } + ); + }); + + return ( +
+ + + + + + + +
+ ); +}; diff --git a/src/renderer/features/servers/components/edit-server-form.tsx b/src/renderer/features/servers/components/edit-server-form.tsx new file mode 100644 index 000000000..1df5f7c45 --- /dev/null +++ b/src/renderer/features/servers/components/edit-server-form.tsx @@ -0,0 +1,102 @@ +import { Checkbox, Stack, Group } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useTranslation } from 'react-i18next'; +import { RiInformationLine } from 'react-icons/ri'; +import { Server, ServerType } from '@/renderer/api/types'; +import { Button, PasswordInput, TextInput } from '@/renderer/components'; +import { useUpdateServer } from '@/renderer/features/servers/mutations/use-update-server'; + +interface EditServerFormProps { + onCancel: () => void; + server: Server; +} + +export const EditServerForm = ({ server, onCancel }: EditServerFormProps) => { + const { t } = useTranslation(); + const updateServer = useUpdateServer(); + + const serverDetailsForm = useForm({ + initialValues: { + legacyAuth: false, + name: server?.name, + password: '', + type: server?.type, + url: server?.url, + username: server?.username, + }, + }); + + const isSubsonic = serverDetailsForm.values.type === ServerType.SUBSONIC; + + const handleSubmit = serverDetailsForm.onSubmit(async (values) => { + updateServer.mutate( + { + body: { + name: values.name, + password: values.password, + type: values.type, + url: values.url, + username: values.username, + }, + query: { serverId: server.id }, + }, + { onSuccess: onCancel } + ); + }); + + return ( +
+ + + ) + } + {...serverDetailsForm.getInputProps('name')} + /> + + ) + } + {...serverDetailsForm.getInputProps('url')} + /> + + ) + } + {...serverDetailsForm.getInputProps('username')} + /> + + {isSubsonic && ( + + )} + + + + + +
+ ); +}; diff --git a/src/renderer/features/servers/components/server-list-item.tsx b/src/renderer/features/servers/components/server-list-item.tsx new file mode 100644 index 000000000..499b7954e --- /dev/null +++ b/src/renderer/features/servers/components/server-list-item.tsx @@ -0,0 +1,222 @@ +import { Stack, Group } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { RiDeleteBin2Fill, RiEdit2Fill } from 'react-icons/ri'; +import { Server } from '@/renderer/api/types'; +import { Button, Text } from '@/renderer/components'; +import { AddServerCredentialForm } from '@/renderer/features/servers/components/add-server-credential-form'; +import { AddServerUrlForm } from '@/renderer/features/servers/components/add-server-url-form'; +import { EditServerForm } from '@/renderer/features/servers/components/edit-server-form'; +import { ServerSection } from '@/renderer/features/servers/components/server-section'; +import { useDeleteServerUrl } from '@/renderer/features/servers/mutations/use-delete-server-url'; +import { useDisableServerUrl } from '@/renderer/features/servers/mutations/use-disable-server-url'; +import { useEnableServerUrl } from '@/renderer/features/servers/mutations/use-enable-server-url'; +import { usePermissions } from '@/renderer/features/shared'; +import { useAuthStore } from '@/renderer/store'; + +interface ServerListItemProps { + server: Server; +} + +export const ServerListItem = ({ server }: ServerListItemProps) => { + const [edit, editHandlers] = useDisclosure(false); + const [addUrl, addUrlHandlers] = useDisclosure(false); + const [addCredential, addCredentialHandlers] = useDisclosure(false); + + const permissions = usePermissions(); + const enableServerUrl = useEnableServerUrl(); + const disableServerUrl = useDisableServerUrl(); + const deleteServerUrl = useDeleteServerUrl(); + const serverCredentials = useAuthStore((state) => state.serverCredentials); + + const enableServerCredential = useAuthStore( + (state) => state.enableServerCredential + ); + + const disableServerCredential = useAuthStore( + (state) => state.disableServerCredential + ); + + const deleteServerCredential = useAuthStore( + (state) => state.deleteServerCredential + ); + + const handleToggleCredential = (credentialId: string, enabled: boolean) => { + if (enabled) { + return disableServerCredential({ id: credentialId }); + } + + return enableServerCredential({ id: credentialId }); + }; + + const handleDeleteCredential = (credentialId: string) => { + deleteServerCredential({ id: credentialId }); + }; + + const handleToggleUrl = (urlId: string, enabled: boolean) => { + if (enabled) { + return disableServerUrl.mutate({ query: { serverId: server.id, urlId } }); + } + + return enableServerUrl.mutate({ query: { serverId: server.id, urlId } }); + }; + + const handleDeleteUrl = (urlId: string) => { + deleteServerUrl.mutate({ + query: { + serverId: server.id, + urlId, + }, + }); + }; + + return ( + <> + + + {edit ? ( + editHandlers.toggle()} + /> + ) : ( + + + + URL + Username + + + {server.url} + {server.username} + + + + {permissions.editServer && ( + + )} + + + )} + + + {addUrl ? ( + addUrlHandlers.close()} + /> + ) : ( + <> + + {server.serverUrls?.map((serverUrl) => ( + + {serverUrl.url} + + + {permissions.deleteServerUrl && ( + + )} + + + ))} + + {permissions.createServerUrl && ( + + )} + + )} + + + {addCredential ? ( + addCredentialHandlers.close()} + /> + ) : ( + <> + + {serverCredentials?.map((credential) => ( + + {credential.username} + + + {permissions.deleteServerCredential && ( + + )} + + + ))} + + {permissions.createServerCredential && ( + + )} + + )} + + + {permissions.deleteServer && ( + + )} + + + + ); +}; diff --git a/src/renderer/features/servers/components/server-list.tsx b/src/renderer/features/servers/components/server-list.tsx new file mode 100644 index 000000000..05cdca32b --- /dev/null +++ b/src/renderer/features/servers/components/server-list.tsx @@ -0,0 +1,65 @@ +import { Accordion, Group } from '@mantine/core'; +import { RiRefreshLine, RiRestartLine, RiServerFill } from 'react-icons/ri'; +import { Button, Text } from '@/renderer/components'; +import { ServerListItem } from '@/renderer/features/servers/components/server-list-item'; +import { useServerList } from '@/renderer/features/servers/queries/use-server-list'; +import { Font } from '@/renderer/styles'; +import { titleCase } from '@/renderer/utils'; + +export const ServerList = () => { + const { data: servers } = useServerList(); + + const handleQuickScan = ( + e: React.MouseEvent + ) => { + e.stopPropagation(); + }; + + const handleFullScan = ( + e: React.MouseEvent + ) => { + e.stopPropagation(); + }; + + return ( + <> + + {servers?.data?.map((s) => ( + + }> + + + {titleCase(s.type)} - {s.name} + + + + + + + + + + + + ))} + + + ); +}; diff --git a/src/renderer/features/servers/components/server-section.tsx b/src/renderer/features/servers/components/server-section.tsx new file mode 100644 index 000000000..63e1ae058 --- /dev/null +++ b/src/renderer/features/servers/components/server-section.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { Text } from '@/renderer/components'; +import { Font } from '@/renderer/styles'; + +interface ServerSectionProps { + children: React.ReactNode; + title: string | React.ReactNode; +} + +const Container = styled.div``; + +const Section = styled.div` + padding: 1rem; + border: 1px solid var(--generic-border-color); +`; + +export const ServerSection = ({ title, children }: ServerSectionProps) => { + return ( + + + {title} + +
{children}
+
+ ); +}; diff --git a/src/renderer/features/servers/index.ts b/src/renderer/features/servers/index.ts index c393fff69..13bb92ce8 100644 --- a/src/renderer/features/servers/index.ts +++ b/src/renderer/features/servers/index.ts @@ -1,5 +1,4 @@ -export * from './routes/ServersRoute'; -export * from './queries/useCreateServer'; -export * from './queries/useServers'; -export * from './components/AddServerModal'; -export * from './components/ServerList'; +export * from './mutations/use-create-server'; +export * from './components/add-server-form'; +export * from './components/server-list'; +export * from './queries/use-server-list'; diff --git a/src/renderer/features/servers/mutations/use-create-server-url.ts b/src/renderer/features/servers/mutations/use-create-server-url.ts new file mode 100644 index 000000000..1ed4b9c22 --- /dev/null +++ b/src/renderer/features/servers/mutations/use-create-server-url.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ServerListResponse, UrlResponse } from '@/renderer/api/servers.api'; +import { ApiError } from '@/renderer/api/types'; +import { toast } from '@/renderer/components'; + +export const useCreateServerUrl = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + UrlResponse, + AxiosError, + { + body: { url: string }; + query: { serverId: string }; + }, + { previous: ServerListResponse | undefined } + >({ + mutationFn: ({ query, body }) => api.servers.createUrl(query, body), + onError: (err) => { + toast.show({ + message: `${err.response?.data?.error?.message || err.message}`, + title: 'Failed to add server URL', + type: 'error', + }); + }, + onSettled: () => { + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/use-create-server.ts b/src/renderer/features/servers/mutations/use-create-server.ts new file mode 100644 index 000000000..b7dd4fb57 --- /dev/null +++ b/src/renderer/features/servers/mutations/use-create-server.ts @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { CreateServerBody, ServerResponse } from '@/renderer/api/servers.api'; +import { ApiError } from '@/renderer/api/types'; +import { toast } from '@/renderer/components'; + +export const useCreateServer = () => { + const queryClient = useQueryClient(); + return useMutation< + ServerResponse, + AxiosError, + { body: CreateServerBody }, + null + >({ + mutationFn: ({ body }) => api.servers.createServer(body), + onError: (err: any) => { + toast.show({ + message: `${err.response?.data?.error?.message || err.message}`, + title: 'Failed to add server', + type: 'error', + }); + }, + onSettled: () => { + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + onSuccess: (data) => { + toast.show({ + message: `${data.data.name} was added successfully`, + title: 'Server added', + type: 'success', + }); + }, + }); +}; diff --git a/src/renderer/features/servers/mutations/use-delete-server-url.ts b/src/renderer/features/servers/mutations/use-delete-server-url.ts new file mode 100644 index 000000000..500bc6302 --- /dev/null +++ b/src/renderer/features/servers/mutations/use-delete-server-url.ts @@ -0,0 +1,60 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ServerListResponse } from '@/renderer/api/servers.api'; +import { ApiError, NullResponse } from '@/renderer/api/types'; +import { toast } from '@/renderer/components'; + +export const useDeleteServerUrl = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { query: { serverId: string; urlId: string } }, + { previous: ServerListResponse | undefined } + >({ + mutationFn: ({ query }) => api.servers.deleteUrl(query), + onError: (err, _variables, context) => { + toast.show({ + message: `${err.response?.data?.error?.message || err.message}`, + title: 'Failed to delete server URL', + type: 'error', + }); + + if (!context?.previous) return; + queryClient.setQueryData(queryKeys.servers.list(), context.previous); + }, + onMutate: async (variables) => { + const queryKey = queryKeys.servers.list(); + + await queryClient.cancelQueries(queryKey); + const previous = queryClient.getQueryData(queryKey); + + if (!previous) return undefined; + + const data = previous.data.map((server) => { + if (server.id === variables.query.serverId) { + return { + ...server, + serverUrls: server.serverUrls?.filter( + (url) => url.id !== variables.query.urlId + ), + }; + } + + return server; + }); + + queryClient.setQueryData(queryKey, { ...previous, data }); + + return { previous }; + }, + onSettled: () => { + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/use-disable-server-url.ts b/src/renderer/features/servers/mutations/use-disable-server-url.ts new file mode 100644 index 000000000..de004d5a7 --- /dev/null +++ b/src/renderer/features/servers/mutations/use-disable-server-url.ts @@ -0,0 +1,55 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ServerListResponse } from '@/renderer/api/servers.api'; +import { ApiError, NullResponse } from '@/renderer/api/types'; + +export const useDisableServerUrl = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { query: { serverId: string; urlId: string } }, + { previous: ServerListResponse | undefined } + >({ + mutationFn: ({ query }) => api.servers.disableUrl(query), + onError: (_err, _variables, context) => { + if (!context?.previous) return; + queryClient.setQueryData(queryKeys.servers.list(), context.previous); + }, + onMutate: async (variables) => { + const queryKey = queryKeys.servers.list(); + + await queryClient.cancelQueries(queryKey); + const previous = queryClient.getQueryData(queryKey); + + if (!previous) return undefined; + + const data = previous.data.map((server) => { + if (server.id === variables.query.serverId) { + return { + ...server, + serverUrls: server.serverUrls?.map((url) => + url.id === variables.query.urlId + ? { ...url, enabled: false } + : url + ), + }; + } + + return server; + }); + + queryClient.setQueryData(queryKey, { ...previous, data }); + + return { previous }; + }, + onSettled: () => { + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/use-enable-server-url.ts b/src/renderer/features/servers/mutations/use-enable-server-url.ts new file mode 100644 index 000000000..659effa41 --- /dev/null +++ b/src/renderer/features/servers/mutations/use-enable-server-url.ts @@ -0,0 +1,55 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ServerListResponse } from '@/renderer/api/servers.api'; +import { ApiError, NullResponse } from '@/renderer/api/types'; + +export const useEnableServerUrl = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { query: { serverId: string; urlId: string } }, + { previous: ServerListResponse | undefined } + >({ + mutationFn: ({ query }) => api.servers.enableUrl(query), + onError: (_err, _variables, context) => { + if (!context?.previous) return; + queryClient.setQueryData(queryKeys.servers.list(), context.previous); + }, + onMutate: async (variables) => { + const queryKey = queryKeys.servers.list(); + + await queryClient.cancelQueries(queryKey); + const previous = queryClient.getQueryData(queryKey); + + if (!previous) return undefined; + + const data = previous.data.map((server) => { + if (server.id === variables.query.serverId) { + return { + ...server, + serverUrls: server.serverUrls?.map((url) => + url.id === variables.query.urlId + ? { ...url, enabled: true } + : { ...url, enabled: false } + ), + }; + } + + return server; + }); + + queryClient.setQueryData(queryKey, { ...previous, data }); + + return { previous }; + }, + onSettled: () => { + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/use-update-server.ts b/src/renderer/features/servers/mutations/use-update-server.ts new file mode 100644 index 000000000..a47dd25af --- /dev/null +++ b/src/renderer/features/servers/mutations/use-update-server.ts @@ -0,0 +1,69 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { + ServerResponse, + ServerListResponse, + CreateServerBody, +} from '@/renderer/api/servers.api'; +import { ApiError } from '@/renderer/api/types'; +import { toast } from '@/renderer/components'; + +export const useUpdateServer = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + ServerResponse, + AxiosError, + { body: Partial; query: { serverId: string } }, + { previous: ServerListResponse | undefined } + >({ + mutationFn: ({ query, body }) => api.servers.updateServer(query, body), + onError: (err, _variables, context) => { + toast.show({ + message: `${err.response?.data.error.message}`, + type: 'error', + }); + + if (context?.previous) { + queryClient.setQueryData(queryKeys.servers.list(), context.previous); + } + }, + onMutate: async (variables) => { + const queryKey = queryKeys.servers.list(); + + await queryClient.cancelQueries(queryKey); + const previous = queryClient.getQueryData(queryKey); + + if (!previous) return undefined; + const data = previous.data.map((server) => { + if (server.id === variables.query.serverId) { + return { + ...server, + name: variables.body.name, + username: variables.body.username, + }; + } + + return server; + }); + + queryClient.setQueryData(queryKey, { ...previous, data }); + + return { previous }; + }, + onSettled: () => { + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + onSuccess: (data) => { + toast.show({ + message: `Server "${data.data.name}" updated`, + type: 'success', + }); + queryClient.invalidateQueries(queryKeys.servers.list()); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/queries/use-server-list.ts b/src/renderer/features/servers/queries/use-server-list.ts new file mode 100644 index 000000000..549bcddab --- /dev/null +++ b/src/renderer/features/servers/queries/use-server-list.ts @@ -0,0 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ServerListResponse } from '@/renderer/api/servers.api'; +import { QueryOptions } from '@/renderer/lib/react-query'; + +export const useServerList = (options?: QueryOptions) => { + // return useQuery({ + // // onSuccess: (servers) => { + // // const { serverUrl } = JSON.parse( + // // localStorage.getItem('authentication') || '{}' + // // ); + // // const storedServersKey = `servers_${md5(serverUrl)}`; + // // const serversFromLocalStorage = localStorage.getItem(storedServersKey); + // // // If a custom account/token is set for a server, use that instead of the default one + // // if (serversFromLocalStorage) { + // // const existingServers = JSON.parse(serversFromLocalStorage); + // // // The 'locked' property determines whether or not to skip updating the server auth + // // const skipped = existingServers.filter( + // // (server: ServerFolderAuth) => server.locked + // // ); + // // const store = servers?.data?.flatMap((server) => + // // server.serverFolders?.map((serverFolder: ServerFolder) => { + // // if (skipped.includes(serverFolder.id)) { + // // return existingServers.find( + // // (s: ServerFolderAuth) => s.id === serverFolder.id + // // ); + // // } + // // return { + // // id: serverFolder.id, + // // locked: false, + // // serverId: server.id, + // // token: server.token, + // // type: server.type, + // // url: server.url, + // // userId: server.remoteUserId, + // // username: server.username, + // // }; + // // }) + // // ); + // // return localStorage.setItem(storedServersKey, JSON.stringify(store)); + // // } + // // const store = servers?.data?.flatMap((server) => + // // server.serverFolders?.map((serverFolder: ServerFolder) => ({ + // // id: serverFolder.id, + // // locked: false, + // // serverId: server.id, + // // token: server.token, + // // type: server.type, + // // url: server.url, + // // userId: server.remoteUserId, + // // username: server.username, + // // })) + // // ); + // // return localStorage.setItem(storedServersKey, JSON.stringify(store)); + // // }, + // queryFn: () => api.servers.getServerList(), + // queryKey: queryKeys.server.list, + // ...options, + // }); + + return useQuery({ + queryFn: () => api.servers.getServerList(), + queryKey: queryKeys.servers.list(), + ...options, + }); +}; diff --git a/src/renderer/features/servers/queries/useServers.ts b/src/renderer/features/servers/queries/useServers.ts deleted file mode 100644 index 5f301eeea..000000000 --- a/src/renderer/features/servers/queries/useServers.ts +++ /dev/null @@ -1,68 +0,0 @@ -import md5 from 'md5'; -import { useQuery } from 'react-query'; -import { ServerFolderAuth } from '../../../../types'; -import { queryKeys } from '../../../api/queryKeys'; -import { serversApi } from '../../../api/serversApi'; -import { ServerFolderResponse } from '../../../api/types'; - -export const useServers = () => { - return useQuery({ - onSuccess: (servers) => { - const { serverUrl } = JSON.parse( - localStorage.getItem('authentication') || '{}' - ); - const storedServersKey = `servers_${md5(serverUrl)}`; - const serversFromLocalStorage = localStorage.getItem(storedServersKey); - - // If a custom account/token is set for a server, use that instead of the default one - if (serversFromLocalStorage) { - const existingServers = JSON.parse(serversFromLocalStorage); - - // The 'locked' property determines whether or not to skip updating the server auth - const skipped = existingServers.filter( - (server: ServerFolderAuth) => server.locked - ); - - const store = servers?.data?.flatMap((server) => - server.serverFolder?.map((serverFolder: ServerFolderResponse) => { - if (skipped.includes(serverFolder.id)) { - return existingServers.find( - (s: ServerFolderAuth) => s.id === serverFolder.id - ); - } - - return { - id: serverFolder.id, - locked: false, - serverId: server.id, - token: server.token, - type: server.serverType, - url: server.url, - userId: server.remoteUserId, - username: server.username, - }; - }) - ); - - return localStorage.setItem(storedServersKey, JSON.stringify(store)); - } - - const store = servers?.data?.flatMap((server) => - server.serverFolder?.map((serverFolder: ServerFolderResponse) => ({ - id: serverFolder.id, - locked: false, - serverId: server.id, - token: server.token, - type: server.serverType, - url: server.url, - userId: server.remoteUserId, - username: server.username, - })) - ); - - return localStorage.setItem(storedServersKey, JSON.stringify(store)); - }, - queryFn: () => serversApi.getServers(), - queryKey: queryKeys.servers, - }); -}; diff --git a/src/renderer/features/servers/routes/ServersRoute.tsx b/src/renderer/features/servers/routes/ServersRoute.tsx deleted file mode 100644 index 51c3c63d4..000000000 --- a/src/renderer/features/servers/routes/ServersRoute.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Title } from '@mantine/core'; -import { ServerList } from '../components/ServerList'; - -export const ServersRoute = () => { - return ( -
- Servers - -
- ); -}; diff --git a/src/renderer/features/servers/queries/useCreateServer.ts b/src/renderer/features/servers/utils/validate-server.ts similarity index 73% rename from src/renderer/features/servers/queries/useCreateServer.ts rename to src/renderer/features/servers/utils/validate-server.ts index 563fb0eda..a35d39213 100644 --- a/src/renderer/features/servers/queries/useCreateServer.ts +++ b/src/renderer/features/servers/utils/validate-server.ts @@ -1,21 +1,20 @@ import axios from 'axios'; import md5 from 'md5'; -import { useMutation } from 'react-query'; -import { serversApi } from '../../../api/serversApi'; -import { randomString } from '../../../utils'; +import { ServerType } from '@/renderer/api/types'; +import { randomString } from '@/renderer/utils'; -export const validateServer = async (options: { +export const validateServerCredential = async (options: { legacyAuth: boolean; password: string; - serverType: string; + type: ServerType; url: string; username: string; }) => { - const { serverType, url, username, password, legacyAuth } = options; + const { type, url, username, password, legacyAuth } = options; const cleanServerUrl = url.replace(/\/$/, ''); try { - if (serverType === 'subsonic') { + if (type === ServerType.SUBSONIC) { let testConnection; let token; if (legacyAuth) { @@ -60,11 +59,3 @@ export const validateServer = async (options: { return null; }; - -export const useCreateServer = () => { - return useMutation({ - mutationFn: serversApi.createServer, - onError: (e) => console.log(e), - onSuccess: (e) => console.log(e), - }); -}; diff --git a/src/renderer/features/titlebar/components/Titlebar.tsx b/src/renderer/features/titlebar/components/Titlebar.tsx index 6422283ab..2a1b2fe10 100644 --- a/src/renderer/features/titlebar/components/Titlebar.tsx +++ b/src/renderer/features/titlebar/components/Titlebar.tsx @@ -1,9 +1,12 @@ import { ReactNode } from 'react'; +import styled from '@emotion/styled'; import { Group } from '@mantine/core'; +import { FiActivity } from 'react-icons/fi'; import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; -import { IconButton } from '../../../components'; +import { AppMenu } from '@/renderer/features/titlebar/components/app-menu'; +import { useAuthStore } from '@/renderer/store'; +import { Button } from '../../../components'; import { WindowControls } from '../../window-controls'; interface TitlebarProps { @@ -18,11 +21,6 @@ const TitlebarContainer = styled.div` justify-content: space-between; width: 100%; height: 100%; - -webkit-app-region: drag; -`; - -const Left = styled.div` - height: 100%; button { -webkit-app-region: no-drag; @@ -33,35 +31,74 @@ const Left = styled.div` } `; -const Right = styled.div` +const Left = styled.div` + flex: 1/3; + height: 100%; +`; + +const Center = styled.div` + flex: 1/3; + height: 100%; +`; + +const Right = styled.div` + flex: 1/3; height: 100%; - -webkit-app-region: no-drag; `; export const Titlebar = ({ children }: TitlebarProps) => { const navigate = useNavigate(); + const isAuthenticated = useAuthStore((state) => !!state.accessToken); return ( <> - } - size={25} - onClick={() => navigate(-1)} - /> - } - size={25} - onClick={() => navigate(1)} - /> + {isAuthenticated && ( + <> + + + + )} +
{children} - + + {isAuthenticated && ( + <> + + + + )} + + @@ -69,5 +106,5 @@ export const Titlebar = ({ children }: TitlebarProps) => { }; Titlebar.defaultProps = { - children: <>, + children: undefined, }; diff --git a/src/renderer/features/titlebar/components/TitlebarButton.tsx b/src/renderer/features/titlebar/components/TitlebarButton.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx new file mode 100644 index 000000000..6a0711b27 --- /dev/null +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -0,0 +1,107 @@ +import { openModal, closeAllModals } from '@mantine/modals'; +import { useTranslation } from 'react-i18next'; +import { RiArrowLeftLine, RiLogoutBoxLine, RiMenu3Fill } from 'react-icons/ri'; +import { useNavigate } from 'react-router'; +import { Button, DropdownMenu } from '@/renderer/components'; +import { + AddServerForm, + ServerList, + useServerList, +} from '@/renderer/features/servers'; +import { useAuthStore } from '@/renderer/store'; + +export const AppMenu = () => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const logout = useAuthStore((state) => state.logout); + const currentServer = useAuthStore((state) => state.currentServer); + const setCurrentServer = useAuthStore((state) => state.setCurrentServer); + const { data: servers } = useServerList(); + + const serverList = + servers?.data?.map((s) => ({ id: s.id, label: `${s.name} - ${s.url}` })) ?? + []; + + const handleLogout = () => { + logout(); + localStorage.removeItem('authentication'); + navigate('/login'); + }; + + const handleAddServerModal = () => { + openModal({ + centered: true, + children: , + title: t('modal.add_server.title'), + }); + }; + + const handleManageServersModal = () => { + openModal({ + centered: true, + children: , + title: t('modal.manage_servers.title'), + }); + }; + + const handleSetCurrentServer = (serverId: string) => { + const server = servers?.data.find((s) => s.id === serverId); + if (!server) return; + setCurrentServer(server); + }; + + return ( + + + + + + Server switcher + {serverList.map((s) => ( + : undefined + } + sx={{ + color: + s.id === currentServer?.id ? 'var(--primary-color)' : undefined, + }} + onClick={() => handleSetCurrentServer(s.id)} + > + {s.label} + + ))} + + {t('global.menu.search_label')} + + {t('global.menu.configure_label')} + + + + {t('global.menu.label_add_server_label')} + + + {t('global.menu.label_manage_servers_label')} + + + {t('global.menu.label_manage_users_label')} + + + } + onClick={handleLogout} + > + {t('global.menu.log_out_label')} + + + + ); +}; diff --git a/src/renderer/features/titlebar/components/UserMenu.tsx b/src/renderer/features/titlebar/components/user-menu.tsx similarity index 88% rename from src/renderer/features/titlebar/components/UserMenu.tsx rename to src/renderer/features/titlebar/components/user-menu.tsx index e12657ef2..b8904dd88 100644 --- a/src/renderer/features/titlebar/components/UserMenu.tsx +++ b/src/renderer/features/titlebar/components/user-menu.tsx @@ -3,7 +3,6 @@ import { useDisclosure } from '@mantine/hooks'; import { RiLogoutBoxLine, RiServerFill, RiSettings3Fill } from 'react-icons/ri'; import { useNavigate } from 'react-router'; import { useAuthStore } from '../../../store'; -import { AddServerModal } from '../../servers'; export const UserMenu = () => { const navigate = useNavigate(); @@ -37,10 +36,6 @@ export const UserMenu = () => { - addServerHandlers.close()} - /> ); }; diff --git a/src/renderer/features/titlebar/index.ts b/src/renderer/features/titlebar/index.ts index 41453a435..490eb5c35 100644 --- a/src/renderer/features/titlebar/index.ts +++ b/src/renderer/features/titlebar/index.ts @@ -1 +1,2 @@ -export * from './components/Titlebar'; +export * from './components/titlebar'; +export * from './components/user-menu'; diff --git a/src/renderer/features/users/queries/use-user-detail.ts b/src/renderer/features/users/queries/use-user-detail.ts new file mode 100644 index 000000000..53ce9d8b1 --- /dev/null +++ b/src/renderer/features/users/queries/use-user-detail.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; + +export const useUserDetail = (options: { userId: string }) => { + const { data, error, isLoading } = useQuery({ + queryFn: () => api.users.getUserDetail({ userId: options.userId }), + queryKey: queryKeys.users.detail(options.userId), + }); + + return { + data, + error, + isLoading, + }; +}; diff --git a/src/renderer/features/window-controls/components/WindowControls.tsx b/src/renderer/features/window-controls/components/window-controls.tsx similarity index 51% rename from src/renderer/features/window-controls/components/WindowControls.tsx rename to src/renderer/features/window-controls/components/window-controls.tsx index 684527dc9..ccb727779 100644 --- a/src/renderer/features/window-controls/components/WindowControls.tsx +++ b/src/renderer/features/window-controls/components/window-controls.tsx @@ -1,18 +1,16 @@ import { useState } from 'react'; +import styled from '@emotion/styled'; import isElectron from 'is-electron'; -import styled from 'styled-components'; -import windowsClose from '../assets/close-w-10.png'; -import windowsMax from '../assets/max-w-10.png'; -import windowsMin from '../assets/min-w-10.png'; +import { + RiCheckboxBlankLine, + RiCloseLine, + RiSubtractLine, +} from 'react-icons/ri'; interface WindowControlsProps { style?: 'macos' | 'windows' | 'linux'; } -const WindowControlsContainer = styled.div` - height: 100%; -`; - const WindowsButtonGroup = styled.div` display: flex; width: 130px; @@ -20,14 +18,23 @@ const WindowsButtonGroup = styled.div` -webkit-app-region: no-drag; `; -const WindowsButton = styled.div` - flex-grow: 1; - height: 100%; - text-align: center; +export const WindowsButton = styled.div<{ $exit?: boolean }>` + display: flex; + flex: 1; + align-items: center; + justify-content: center; -webkit-app-region: no-drag; + width: 50px; + height: 30px; + + img { + width: 35%; + height: 50%; + } &:hover { - background: rgba(125, 125, 125, 30%); + background: ${({ $exit }) => + $exit ? 'rgba(200, 50, 0, 30%)' : `rgba(125, 125, 125, 30%)`}; } `; @@ -56,27 +63,25 @@ export const WindowControls = ({ style }: WindowControlsProps) => { const handleClose = () => close(); return ( - + <> {isElectron() && ( <> {style === 'windows' && ( - <> - - - minimize - - - maximize - - - exit - - - + + + + + + + + + + + )} )} - + ); }; diff --git a/src/renderer/features/window-controls/index.ts b/src/renderer/features/window-controls/index.ts index 29cfe43c7..b1b8447ad 100644 --- a/src/renderer/features/window-controls/index.ts +++ b/src/renderer/features/window-controls/index.ts @@ -1 +1 @@ -export * from './components/WindowControls'; +export * from './components/window-controls'; diff --git a/src/renderer/layouts/auth/AuthLayout.tsx b/src/renderer/layouts/auth-layout.tsx similarity index 85% rename from src/renderer/layouts/auth/AuthLayout.tsx rename to src/renderer/layouts/auth-layout.tsx index f8154e733..5b5fb258e 100644 --- a/src/renderer/layouts/auth/AuthLayout.tsx +++ b/src/renderer/layouts/auth-layout.tsx @@ -1,6 +1,6 @@ +import styled from '@emotion/styled'; import { Outlet } from 'react-router-dom'; -import styled from 'styled-components'; -import { Titlebar } from '../../features/titlebar'; +import { Titlebar } from '../features/titlebar'; const WindowsTitlebarContainer = styled.div` position: absolute; diff --git a/src/renderer/layouts/default-layout.tsx b/src/renderer/layouts/default-layout.tsx new file mode 100644 index 000000000..141075e98 --- /dev/null +++ b/src/renderer/layouts/default-layout.tsx @@ -0,0 +1,181 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import styled from '@emotion/styled'; +import { Menu, Button } from '@mantine/core'; +import { Outlet } from 'react-router'; +import { Titlebar } from '@/renderer/features/titlebar'; +import { constrainSidebarWidth } from '@/renderer/utils'; +import { Playerbar } from '../features/player'; +import { Sidebar } from '../features/sidebar'; + +const Layout = styled.div` + display: grid; + grid-template-areas: + 'header' + 'main' + 'player'; + grid-template-rows: 30px 1fr 90px; + grid-template-columns: 1fr; + gap: 0; + height: 100%; +`; + +const TitlebarContainer = styled.header` + grid-area: header; + background: var(--titlebar-bg); + -webkit-app-region: drag; +`; + +const MainContainer = styled.main<{ leftSidebarWidth: string }>` + display: grid; + grid-area: main; + grid-template-areas: 'sidebar .'; + grid-template-rows: 1fr; + grid-template-columns: ${(props) => props.leftSidebarWidth} 1fr; + gap: 0; + background: var(--main-bg); +`; + +const SidebarContainer = styled.div` + position: relative; + grid-area: sidebar; + background: var(--sidebar-bg); +`; + +const PlayerbarContainer = styled.footer` + grid-area: player; + background: var(--playerbar-bg); +`; + +const ResizeHandle = styled.div<{ + isResizing: boolean; + placement: 'top' | 'left' | 'bottom' | 'right'; +}>` + position: absolute; + width: 3px; + height: 100%; + right: 0; + background-color: var(--sidebar-handle-bg); + /* border-top: ${({ placement }) => + placement === 'top' && '1px var(--sidebar-handle-bg) solid'}; + border-right: ${({ placement }) => + placement === 'right' && '1px var(--sidebar-handle-bg) solid'}; + border-bottom: ${({ placement }) => + placement === 'bottom' && '1px var(--sidebar-handle-bg) solid'}; + border-left: ${({ placement }) => + placement === 'left' && '1px var(--sidebar-handle-bg) solid'}; */ + opacity: ${(props) => (props.isResizing ? 1 : 0)}; + cursor: ew-resize; + + &:hover { + opacity: 1; + } +`; + +export const DefaultLayout = () => { + const [leftSidebarWidth, setLeftSidebarWidth] = useState('170px'); + + const sidebarRef = useRef(null); + const [isResizing, setIsResizing] = useState(false); + + const startResizing = useCallback(() => { + setIsResizing(true); + }, []); + + const stopResizing = useCallback(() => { + setIsResizing(false); + }, []); + + const resize = useCallback( + (mouseMoveEvent) => { + if (isResizing) { + setLeftSidebarWidth( + `${constrainSidebarWidth(mouseMoveEvent.clientX)}px` + ); + } + }, + [isResizing] + ); + + useEffect(() => { + window.addEventListener('mousemove', resize); + window.addEventListener('mouseup', stopResizing); + return () => { + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResizing); + }; + }, [resize, stopResizing]); + + return ( + <> + {/* + + + + + ( + + )} + maximumSize={400} + minimumSize={175} + size={175} + > + + + ( + + )} + maximumSize={800} + size={300} + > + + + + + + + + + + + + + + */} + + + + + + + + + + { + e.preventDefault(); + startResizing(); + }} + /> + + + + + + + + + + ); +}; diff --git a/src/renderer/layouts/default/DefaultLayout.tsx b/src/renderer/layouts/default/DefaultLayout.tsx deleted file mode 100644 index d4ca45d32..000000000 --- a/src/renderer/layouts/default/DefaultLayout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { AnimatePresence } from 'framer-motion'; -import { Outlet, useLocation } from 'react-router-dom'; -import * as Space from 'react-spaces'; -import styled from 'styled-components'; -import { Playerbar } from '../../features/player'; -import { Sidebar } from '../../features/sidebar'; -import { Titlebar } from '../../features/titlebar'; - -const LayoutContainer = styled(Space.ViewPort)``; - -const LeftSidebar = styled(Space.LeftResizable)` - background: var(--sidebar-bg); -`; - -const RightSidebar = styled(Space.RightResizable)` - background: var(--sidebar-bg); -`; - -const TitlebarContainer = styled(Space.Top)` - position: sticky; - background: var(--titlebar-bg); - border-bottom: var(--playerbar-border-top); -`; - -const ContentContainer = styled(Space.Fill)``; - -const PlayerbarContainer = styled(Space.Bottom)``; - -const ResizeHandle = styled.span<{ - placement: 'top' | 'left' | 'bottom' | 'right'; -}>` - position: absolute; - width: 3px; - height: 100%; - border-top: ${({ placement }) => - placement === 'top' && '1px var(--sidebar-handle-bg) solid'}; - border-right: ${({ placement }) => - placement === 'right' && '1px var(--sidebar-handle-bg) solid'}; - border-bottom: ${({ placement }) => - placement === 'bottom' && '1px var(--sidebar-handle-bg) solid'}; - border-left: ${({ placement }) => - placement === 'left' && '1px var(--sidebar-handle-bg) solid'}; - opacity: 0; - - &:hover { - opacity: 1; - } -`; - -export const DefaultLayout = () => { - const location = useLocation(); - return ( - <> - - - - - - ( - - )} - maximumSize={400} - minimumSize={175} - size={175} - > - - - ( - - )} - maximumSize={400} - size={300} - /> - - - - - - - - - - - - - - ); -}; diff --git a/src/renderer/layouts/default/utils/constrainSidebarWidth.ts b/src/renderer/layouts/default/utils/constrainSidebarWidth.ts deleted file mode 100644 index dfa6930fd..000000000 --- a/src/renderer/layouts/default/utils/constrainSidebarWidth.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const constrainSidebarWidth = (num: number) => { - if (num < 165) { - return 165; - } - - if (num > 400) { - return 400; - } - - return num; -}; diff --git a/src/renderer/layouts/index.ts b/src/renderer/layouts/index.ts index 9437aeff7..6311d3c6e 100644 --- a/src/renderer/layouts/index.ts +++ b/src/renderer/layouts/index.ts @@ -1,2 +1,2 @@ -export * from './auth/AuthLayout'; -export * from './default/DefaultLayout'; +export * from './auth-layout'; +export * from './default-layout';