diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 42304bc..992e4b8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -18,6 +18,15 @@ module.exports = { }, plugins: ['import', 'prettier', 'unused-imports'], rules: { + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/member-ordering': [ + 'error', + { + interfaces: { order: 'alphabetically-case-insensitive' }, + typeLiterals: { order: 'alphabetically-case-insensitive' }, + }, + ], 'import/first': 'error', 'import/newline-after-import': 'error', 'import/no-duplicates': 'error', diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 7b78301..ec60df6 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -1,3 +1,5 @@ +import type { IpcRenderer } from 'electron'; + /// declare namespace NodeJS { interface ProcessEnv { @@ -22,5 +24,5 @@ declare namespace NodeJS { // Used in Renderer process, expose in `preload.ts` interface Window { - ipcRenderer: import('electron').IpcRenderer; + ipcRenderer: IpcRenderer; } diff --git a/package.json b/package.json index 3cf1ccd..f8562e7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-label": "2.0.2", "@radix-ui/react-navigation-menu": "1.1.4", "@radix-ui/react-select": "2.0.0", + "@radix-ui/react-separator": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-tooltip": "1.0.7", "class-variance-authority": "0.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69318aa..267d78e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@radix-ui/react-select': specifier: 2.0.0 version: 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: 1.0.3 + version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: 1.0.2 version: 1.0.2(@types/react@18.2.31)(react@18.2.0) @@ -1227,6 +1230,27 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.31)(react@18.2.0) dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.31 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.31)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: diff --git a/src/components/ConversionSettings/BitrateSelect.tsx b/src/components/ConversionSettings/BitrateSelect.tsx new file mode 100644 index 0000000..1477b27 --- /dev/null +++ b/src/components/ConversionSettings/BitrateSelect.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; +import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { FormControl, FormItem, FormLabel } from '../ui/Form'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/Select'; +import type { Bitrate, Codec } from 'src/schemas/conversionSettings.schema'; + +type BitrateSelectProps = { + codec: Codec; + onChange: (value: Bitrate) => void; + value: Bitrate; +}; + +const bitrateByCodec: Record = { + default: ['default'], + aac: ['default', '32k', '64k', '96k', '128k', '192k', '256k', '320k'], + ac3: ['default', '128k', '192k', '256k', '320k', '384k', '448k', '512k', '640k'], + eac3: ['default', '192k', '320k', '448k', '640k', '1024k', '2048k', '4096k'], +}; + +export const BitrateSelect = ({ codec, value, onChange }: BitrateSelectProps) => { + const { t } = useTranslation(); + const options = bitrateByCodec[codec]; + const isDisabled = codec === codecSchema.enum.default; + + return ( + + {t('conversionSettings.bitrate.label')} + + + ); +}; diff --git a/src/components/ConversionSettings/ChannelsSelect.tsx b/src/components/ConversionSettings/ChannelsSelect.tsx new file mode 100644 index 0000000..ff47990 --- /dev/null +++ b/src/components/ConversionSettings/ChannelsSelect.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; +import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { FormControl, FormItem, FormLabel } from '../ui/Form'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/Select'; +import type { Channels, Codec } from 'src/schemas/conversionSettings.schema'; + +type ChannelsSelectProps = { + codec: Codec; + onChange: (value: Channels) => void; + value: Channels; +}; + +const channelsByCodec: Record = { + default: ['default'], + aac: ['default', '1', '2', '3', '4', '5', '6', '7', '8'], + ac3: ['default', '1', '2', '3', '4', '5', '6'], + eac3: ['default', '1', '2', '3', '4', '5', '6'], +}; + +export const ChannelsSelect = ({ codec, onChange, value }: ChannelsSelectProps) => { + const { t } = useTranslation(); + const options = channelsByCodec[codec]; + const isDisabled = codec === codecSchema.enum.default; + + return ( + + {t('conversionSettings.channels.label')} + + + ); +}; diff --git a/src/components/ConversionSettings/CodecSelect.tsx b/src/components/ConversionSettings/CodecSelect.tsx new file mode 100644 index 0000000..98cb9eb --- /dev/null +++ b/src/components/ConversionSettings/CodecSelect.tsx @@ -0,0 +1,35 @@ +import { useTranslation } from 'react-i18next'; +import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { FormControl, FormItem, FormLabel } from '../ui/Form'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/Select'; +import type { Codec } from 'src/schemas/conversionSettings.schema'; + +type CodecSelectProps = { + onChange: (value: Codec) => void; + value: Codec; +}; + +export const CodecSelect = ({ onChange, value }: CodecSelectProps) => { + const { t } = useTranslation(); + const options = codecSchema.options.map(option => option); + + return ( + + {t('conversionSettings.codec.label')} + + + ); +}; diff --git a/src/components/ConversionSettings/ConversionSettings.tsx b/src/components/ConversionSettings/ConversionSettings.tsx new file mode 100644 index 0000000..15f025c --- /dev/null +++ b/src/components/ConversionSettings/ConversionSettings.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useConversionSettingsForm } from 'src/hooks/useConversionSettingsForm'; +import { bitrateSchema, channelsSchema, codecSchema, sampleRateSchema } from 'src/schemas/conversionSettings.schema'; +import { Form, FormField } from '../ui/Form'; +import { BitrateSelect } from './BitrateSelect'; +import { ChannelsSelect } from './ChannelsSelect'; +import { CodecSelect } from './CodecSelect'; +import { SampleRateSelect } from './SampleRateSelect'; + +export const ConversionSettings = () => { + const { t } = useTranslation(); + const form = useConversionSettingsForm({ + defaultValues: { + codec: codecSchema.enum.default, + bitrate: bitrateSchema.enum.default, + sampleRate: sampleRateSchema.enum.default, + channels: channelsSchema.enum.default, + }, + }); + const { control, setValue, watch } = form; + const codec = watch('codec'); + + useEffect(() => { + setValue('bitrate', bitrateSchema.enum.default); + setValue('sampleRate', sampleRateSchema.enum.default); + setValue('channels', channelsSchema.enum.default); + }, [codec, setValue]); + + return ( +
+ +

{t('conversionSettings.title')}

+ } + /> + } + /> + } + /> + } + /> + + + ); +}; diff --git a/src/components/ConversionSettings/SampleRateSelect.tsx b/src/components/ConversionSettings/SampleRateSelect.tsx new file mode 100644 index 0000000..14f6ac9 --- /dev/null +++ b/src/components/ConversionSettings/SampleRateSelect.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; +import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { FormControl, FormItem, FormLabel } from '../ui/Form'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/Select'; +import type { Codec, SampleRate } from 'src/schemas/conversionSettings.schema'; + +type SampleRateSelectProps = { + codec: Codec; + onChange: (value: SampleRate) => void; + value: SampleRate; +}; + +const sampleRateByCodec: Record = { + default: ['default'], + aac: ['default', '8000', '22050', '32000', '44100', '48000', '96000'], + ac3: ['default', '8000', '22050', '32000', '44100', '48000'], + eac3: ['default', '32000', '44100', '48000'], +}; + +export const SampleRateSelect = ({ codec, onChange, value }: SampleRateSelectProps) => { + const { t } = useTranslation(); + const options = sampleRateByCodec[codec]; + const isDisabled = codec === codecSchema.enum.default; + + return ( + + {t('conversionSettings.sampleRate.label')} + + + ); +}; diff --git a/src/components/ConversionSettings/index.ts b/src/components/ConversionSettings/index.ts new file mode 100644 index 0000000..1d29261 --- /dev/null +++ b/src/components/ConversionSettings/index.ts @@ -0,0 +1 @@ +export { ConversionSettings } from './ConversionSettings'; diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index edcf1c2..4274a34 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { cn } from 'src/lib/utils'; -import { PropsWithClassName } from './shadcn.types'; +import type { PropsWithClassName } from './ui.types'; const Card = React.forwardRef & PropsWithClassName>( ({ className, ...props }, ref) => ( diff --git a/src/components/ui/Form.tsx b/src/components/ui/Form.tsx index 05cecbd..9d76bf1 100644 --- a/src/components/ui/Form.tsx +++ b/src/components/ui/Form.tsx @@ -1,10 +1,11 @@ -import * as LabelPrimitive from '@radix-ui/react-label'; import { Slot } from '@radix-ui/react-slot'; import * as React from 'react'; -import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form'; +import { Controller, FormProvider, useFormContext } from 'react-hook-form'; import { Label } from 'src/components/ui/Label'; import { cn } from 'src/lib/utils'; -import { PropsWithClassName } from './shadcn.types'; +import type { PropsWithClassName } from './ui.types'; +import type * as LabelPrimitive from '@radix-ui/react-label'; +import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; const Form = FormProvider; @@ -65,7 +66,7 @@ const FormItem = React.forwardRef -
+
); }, diff --git a/src/components/ui/NavigationMenu.tsx b/src/components/ui/NavigationMenu.tsx index 2a98d6f..de398e7 100644 --- a/src/components/ui/NavigationMenu.tsx +++ b/src/components/ui/NavigationMenu.tsx @@ -3,7 +3,7 @@ import { cva } from 'class-variance-authority'; import { ChevronDown } from 'lucide-react'; import * as React from 'react'; import { cn } from 'src/lib/utils'; -import { PropsWithClassName } from './shadcn.types'; +import type { PropsWithClassName } from './ui.types'; const NavigationMenu = React.forwardRef< React.ElementRef, diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx index 492f20b..db262cf 100644 --- a/src/components/ui/Select.tsx +++ b/src/components/ui/Select.tsx @@ -2,7 +2,7 @@ import * as SelectPrimitive from '@radix-ui/react-select'; import { Check, ChevronDown, ChevronUp } from 'lucide-react'; import * as React from 'react'; import { cn } from 'src/lib/utils'; -import { PropsWithClassName } from './shadcn.types'; +import type { PropsWithClassName } from './ui.types'; const Select = SelectPrimitive.Root; diff --git a/src/components/ui/Separator.tsx b/src/components/ui/Separator.tsx new file mode 100644 index 0000000..906debd --- /dev/null +++ b/src/components/ui/Separator.tsx @@ -0,0 +1,20 @@ +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import * as React from 'react'; +import { cn } from 'src/lib/utils'; +import type { PropsWithClassName } from './ui.types'; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & PropsWithClassName +>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( + +)); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/components/ui/Tooltip.tsx b/src/components/ui/Tooltip.tsx index a1a2581..5a3c73f 100644 --- a/src/components/ui/Tooltip.tsx +++ b/src/components/ui/Tooltip.tsx @@ -1,7 +1,7 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import * as React from 'react'; import { cn } from 'src/lib/utils'; -import { PropsWithClassName } from './shadcn.types'; +import type { PropsWithClassName } from './ui.types'; const TooltipProvider = TooltipPrimitive.Provider; diff --git a/src/components/ui/shadcn.types.ts b/src/components/ui/ui.types.ts similarity index 100% rename from src/components/ui/shadcn.types.ts rename to src/components/ui/ui.types.ts diff --git a/src/hooks/useConversionSettingsForm.ts b/src/hooks/useConversionSettingsForm.ts new file mode 100644 index 0000000..0fa3b90 --- /dev/null +++ b/src/hooks/useConversionSettingsForm.ts @@ -0,0 +1,21 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { bitrateSchema, channelsSchema, codecSchema, sampleRateSchema } from 'src/schemas/conversionSettings.schema'; +import { z } from 'zod'; +import type { UseFormProps } from 'react-hook-form'; + +const formSchema = z.object({ + codec: codecSchema, + bitrate: bitrateSchema, + sampleRate: sampleRateSchema, + channels: channelsSchema, +}); + +type FormData = z.infer; + +export const useConversionSettingsForm = (props?: UseFormProps) => { + return useForm({ + ...props, + resolver: zodResolver(formSchema), + }); +}; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a3408b1..9fc1305 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -4,6 +4,64 @@ "home": "Converter", "settings": "Settings" }, + "conversionSettings": { + "bitrate": { + "label": "Bitrate", + "values": { + "default": "Default", + "32k": "32 kbps", + "64k": "64 kbps", + "96k": "96 kbps", + "128k": "128 kbps", + "192k": "192 kbps", + "256k": "256 kbps", + "320k": "320 kbps", + "384k": "384 kbps", + "448k": "448 kbps", + "512k": "512 kbps", + "640k": "640 kbps", + "1024k": "1024 kbps", + "2048k": "2048 kbps", + "4096k": "4096 kbps" + } + }, + "channels": { + "label": "Channels", + "values": { + "default": "Same as source", + "1": "Mono", + "2": "Stereo", + "3": "2.1", + "4": "4.0", + "5": "5.0", + "6": "5.1", + "7": "7.0", + "8": "7.1" + } + }, + "codec": { + "label": "Codec", + "values": { + "default": "Without reencoding - Copy audio", + "aac": "AAC", + "ac3": "AC3", + "eac3": "EAC3" + } + }, + "sampleRate": { + "label": "Sample rate", + "values": { + "default": "Same as source", + "8000": "8000 Hz", + "22050": "22050 Hz", + "32000": "32000 Hz", + "44100": "44100 Hz", + "48000": "48000 Hz", + "96000": "96000 Hz" + } + }, + "title": "Conversion settings" + }, "fileImport": { "button": "Add files", "description": "Add video files for conversion.\nYou can drag & drop your files here." diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 0381dc6..9577bed 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -4,6 +4,64 @@ "home": "Convertisseur", "settings": "Paramètres" }, + "conversionSettings": { + "bitrate": { + "label": "Débit", + "values": { + "default": "Par défaut", + "32k": "32 kbps", + "64k": "64 kbps", + "96k": "96 kbps", + "128k": "128 kbps", + "192k": "192 kbps", + "256k": "256 kbps", + "320k": "320 kbps", + "384k": "384 kbps", + "448k": "448 kbps", + "512k": "512 kbps", + "640k": "640 kbps", + "1024k": "1024 kbps", + "2048k": "2048 kbps", + "4096k": "4096 kbps" + } + }, + "channels": { + "label": "Canaux", + "values": { + "default": "Identique à la source", + "1": "Mono", + "2": "Stéréo", + "3": "2.1", + "4": "4.0", + "5": "5.0", + "6": "5.1", + "7": "7.0", + "8": "7.1" + } + }, + "codec": { + "label": "Codec", + "values": { + "default": "Sans réencodage - Copier l'audio", + "aac": "AAC", + "ac3": "AC3", + "eac3": "EAC3" + } + }, + "sampleRate": { + "label": "Fréquence d'échantillonnage", + "values": { + "default": "Identique à la source", + "8000": "8000 Hz", + "22050": "22050 Hz", + "32000": "32000 Hz", + "44100": "44100 Hz", + "48000": "48000 Hz", + "96000": "96000 Hz" + } + }, + "title": "Paramètres de conversion" + }, "fileImport": { "button": "Ajouter des fichiers", "description": "Ajouter des fichiers vidéo à convertir.\nVous pouvez glisser-déposer vos fichiers ici." diff --git a/src/routes/Converter.tsx b/src/routes/Converter.tsx index b1a7df8..3bf5cb7 100644 --- a/src/routes/Converter.tsx +++ b/src/routes/Converter.tsx @@ -1,3 +1,4 @@ +import { ConversionSettings } from 'src/components/ConversionSettings'; import { FileImport } from 'src/components/FileImport'; import { FileList } from 'src/components/FileList'; @@ -11,7 +12,7 @@ export const Converter = () => {
-

Conversion Settings

+
Footer
diff --git a/src/schemas/conversionSettings.schema.ts b/src/schemas/conversionSettings.schema.ts new file mode 100644 index 0000000..085ad60 --- /dev/null +++ b/src/schemas/conversionSettings.schema.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +export const codecSchema = z.enum(['default', 'aac', 'ac3', 'eac3']); +export type Codec = z.infer; + +export const bitrateSchema = z.enum([ + 'default', + '32k', + '64k', + '96k', + '128k', + '192k', + '256k', + '320k', + '384k', + '448k', + '512k', + '640k', + '1024k', + '2048k', + '4096k', +]); +export type Bitrate = z.infer; + +export const sampleRateSchema = z.enum(['default', '8000', '22050', '32000', '44100', '48000', '96000']); +export type SampleRate = z.infer; + +export const channelsSchema = z.enum(['default', '1', '2', '3', '4', '5', '6', '7', '8']); +export type Channels = z.infer; diff --git a/src/store/selectors.ts b/src/store/selectors.ts index 9d2b6e3..24af9f3 100644 --- a/src/store/selectors.ts +++ b/src/store/selectors.ts @@ -1,3 +1,3 @@ -import { State } from './store.types'; +import type { State } from './store.types'; export const getFiles = (state: State) => Object.values(state.files); diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 6c297ad..3aee7c6 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; -import { Store } from './store.types'; +import type { Store } from './store.types'; export const useStore = create()( immer(set => ({