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 (
-
-
-
- );
-};
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 (
-
- );
-};
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 (
+
+ );
+};
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 (
+
+ );
+};
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 (
+
+ );
+};
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 && (
+ }>
+ Delete server
+
+ )}
+
+
+ >
+ );
+};
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}
+
+
+
+ );
+};
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' && (
- <>
-
-
-
-
-
-
-
-
-
-
-
- >
+
+
+
+
+
+
+
+
+
+
+
)}
>
)}
-
+ >
);
};
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';