diff --git a/.eslintignore b/.eslintignore index 9147e7a..540ca6d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,6 @@ dist-electron -# TODO temp fix for eslint/ts error in electron/file.types.ts -electron/file.types.ts +# TODO temp fix for eslint/ts error +electron/convert/index.ts +electron/convert/convert.utils.ts +schema/index.ts diff --git a/electron/ConversionManager.ts b/electron/ConversionManager.ts index d40305b..89d0e62 100644 --- a/electron/ConversionManager.ts +++ b/electron/ConversionManager.ts @@ -1,5 +1,6 @@ import { ipcMain } from 'electron'; -import type { VideoFile } from './file.types'; +import { convert } from './convert'; +import type { ConversionSettings, VideoFile } from '../schema'; import type { BrowserWindow } from 'electron'; export class ConversionManager { @@ -10,10 +11,41 @@ export class ConversionManager { this.mainWindow = mainWindow; this.isConversionInterrupted = false; - ipcMain.handle('start-conversion', (_event, { files }: { files: VideoFile[] }) => { - this.isConversionInterrupted = false; - this.handleFileConversionStart(files[0].path); - }); + ipcMain.handle( + 'start-conversion', + async ( + _event, + { + conversionSettings, + destinationPath, + files, + }: { conversionSettings: ConversionSettings; destinationPath: string; files: VideoFile[] }, + ) => { + this.isConversionInterrupted = false; + + for (let i = 0; i < files.length; i++) { + if (this.isConversionInterrupted) { + break; + } + + try { + const file = files[i]; + await convert( + { conversionSettings, file, destinationPath }, + { + onFileConversionEnd: filePath => this.handleFileConversionEnd(filePath), + onFileConversionProgress: (filePath, progress) => this.handleFileConversionProgress(filePath, progress), + onFileConversionStart: filePath => this.handleFileConversionStart(filePath), + onFileConversionError: (filePath, error) => this.handleFileConversionError(filePath, error), + }, + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + } + }, + ); ipcMain.handle('stop-conversion', () => { this.isConversionInterrupted = true; @@ -24,15 +56,15 @@ export class ConversionManager { this.mainWindow.webContents.send('file-conversion-start', { filePath }); } - handleFileConversionProgress() { - this.mainWindow.webContents.send('file-conversion-progress'); + handleFileConversionProgress(filePath: string, progress: number) { + this.mainWindow.webContents.send('file-conversion-progress', { filePath, progress }); } - handleFileConversionEnd() { - this.mainWindow.webContents.send('file-conversion-end'); + handleFileConversionEnd(filePath: string) { + this.mainWindow.webContents.send('file-conversion-end', { filePath }); } - handleFileConversionError() { - this.mainWindow.webContents.send('file-conversion-error'); + handleFileConversionError(filePath: string, error: string) { + this.mainWindow.webContents.send('file-conversion-error', { filePath, error }); } } diff --git a/electron/convert/convert.ts b/electron/convert/convert.ts new file mode 100644 index 0000000..c08843b --- /dev/null +++ b/electron/convert/convert.ts @@ -0,0 +1,54 @@ +/// +import { getOutputOptions, getOutputPath } from './convert.utils'; +import type { ConversionSettings, VideoFile } from '../../schema'; + +const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; +const ffmpeg = require('fluent-ffmpeg'); + +ffmpeg.setFfmpegPath(ffmpegPath); + +type ConvertParams = { + conversionSettings: ConversionSettings; + destinationPath: string; + file: VideoFile; +}; + +type ConvertCallbacks = { + onFileConversionEnd: (filePath: string) => void; + onFileConversionError: (filePath: string, error: string) => void; + onFileConversionProgress: (filePath: string, progress: number) => void; + onFileConversionStart: (filePath: string, commandLine: string) => void; +}; + +export function convert( + { conversionSettings, file, destinationPath }: ConvertParams, + { onFileConversionEnd, onFileConversionError, onFileConversionProgress, onFileConversionStart }: ConvertCallbacks, +) { + const inputPath = file.path; + const outputPath = getOutputPath(inputPath, destinationPath); + const outputOptions = getOutputOptions(conversionSettings); + + const command = ffmpeg().input(inputPath).output(outputPath).outputOptions(outputOptions); + + return new Promise((resolve, reject) => { + command + .on('start', (commandLine: string) => { + // eslint-disable-next-line no-console + console.log(commandLine); + onFileConversionStart(inputPath, commandLine); + }) + .on('progress', ({ percent }: { percent?: number }) => { + onFileConversionProgress(inputPath, percent ?? 0); + }) + .on('end', () => { + onFileConversionEnd(inputPath); + resolve(); + }) + .on('error', (error: Error) => { + onFileConversionError(inputPath, error.message); + reject(error); + }); + + command.run(); + }); +} diff --git a/electron/convert/convert.utils.ts b/electron/convert/convert.utils.ts new file mode 100644 index 0000000..ef52ad0 --- /dev/null +++ b/electron/convert/convert.utils.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { bitrateSchema, channelsSchema, codecSchema, sampleRateSchema } from '../../schema'; +import type { ConversionSettings } from '../../schema'; + +const baseOutputOptions = ['-map 0', '-codec copy']; +const audioCodecFlag = '-c:a'; +const audioBitrateFlag = '-c:a'; +const audioChannelsFlag = '-ac'; +const audioSampleReteFlag = '-ar'; + +export function getOutputOptions({ bitrate, channels, codec, sampleRate }: ConversionSettings) { + const options = [...baseOutputOptions]; + + if (codec && codec !== codecSchema.enum.default) { + options.push(`${audioCodecFlag} ${codec}`); + } + + if (bitrate && bitrate !== bitrateSchema.enum.default) { + options.push(`${audioBitrateFlag} ${bitrate}`); + } + + if (channels && channels !== channelsSchema.enum.default) { + options.push(`${audioChannelsFlag} ${channels}`); + } + + if (sampleRate && sampleRate !== sampleRateSchema.enum.default) { + options.push(`${audioSampleReteFlag} ${sampleRate}`); + } + + return options; +} + +export function getOutputPath(inputPath: string, destinationPath: string) { + const { base, dir, ext, name } = path.parse(inputPath); + const outputDirectory = destinationPath || dir; + + let index = 1; + let outputPath = path.join(outputDirectory, base); + + while (fs.existsSync(outputPath)) { + outputPath = path.join(outputDirectory, `${name} (${index})${ext}`); + index++; + } + + return outputPath; +} diff --git a/electron/convert/index.ts b/electron/convert/index.ts new file mode 100644 index 0000000..5432f05 --- /dev/null +++ b/electron/convert/index.ts @@ -0,0 +1 @@ +export { convert } from './convert'; diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 37d8c52..44d051e 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -4,7 +4,7 @@ import type { FileConversionProgressCallback, FileConversionStartCallback, } from './conversion-events.types'; -import type { VideoFile } from './file.types'; +import type { ConversionSettings, VideoFile } from '../schema'; declare namespace NodeJS { interface ProcessEnv { @@ -22,7 +22,15 @@ export interface IConversion { onFileConversionError: (callback: FileConversionErrorCallback) => void; onFileConversionProgress: (callback: FileConversionProgressCallback) => void; onFileConversionStart: (callback: FileConversionStartCallback) => void; - startConversion: ({ files }: { files: VideoFile[] }) => void; + startConversion: ({ + conversionSettings, + destinationPath, + files, + }: { + conversionSettings: ConversionSettings; + destinationPath: string; + files: VideoFile[]; + }) => void; } declare global { diff --git a/electron/file.types.ts b/electron/file.types.ts deleted file mode 100644 index 6626e03..0000000 --- a/electron/file.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type VideoFile = { - name: string; - path: string; - progress: number; - size: number; - status: 'imported' | 'converting' | 'conversionSuccess' | 'conversionError'; - type: string; -}; diff --git a/electron/preload.ts b/electron/preload.ts index c159438..dffa872 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -5,7 +5,7 @@ import type { FileConversionProgressCallback, FileConversionStartCallback, } from './conversion-events.types'; -import type { VideoFile } from './file.types'; +import type { ConversionSettings, VideoFile } from '../schema'; contextBridge.exposeInMainWorld('dialog', { openDirectory: () => ipcRenderer.invoke('dialog:openDirectory'), @@ -17,5 +17,13 @@ contextBridge.exposeInMainWorld('conversion', { onFileConversionProgress: (callback: FileConversionProgressCallback) => ipcRenderer.on('file-conversion-progress', callback), onFileConversionStart: (callback: FileConversionStartCallback) => ipcRenderer.on('file-conversion-start', callback), - startConversion: ({ files }: { files: VideoFile[] }) => ipcRenderer.invoke('start-conversion', { files }), + startConversion: ({ + conversionSettings, + destinationPath, + files, + }: { + conversionSettings: ConversionSettings; + destinationPath: string; + files: VideoFile[]; + }) => ipcRenderer.invoke('start-conversion', { conversionSettings, destinationPath, files }), }); diff --git a/package.json b/package.json index 427f982..ca0895e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test": "vitest" }, "dependencies": { + "@ffmpeg-installer/ffmpeg": "1.1.0", "@formkit/auto-animate": "0.8.1", "@hookform/resolvers": "3.3.2", "@radix-ui/react-label": "2.0.2", @@ -34,6 +35,7 @@ "@radix-ui/react-tooltip": "1.0.7", "class-variance-authority": "0.7.0", "clsx": "2.0.0", + "fluent-ffmpeg": "2.1.2", "i18next": "23.6.0", "immer": "10.0.3", "lucide-react": "0.292.0", @@ -53,6 +55,7 @@ "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.1.0", "@testing-library/user-event": "14.5.1", + "@types/fluent-ffmpeg": "2.1.24", "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@typescript-eslint/eslint-plugin": "6.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f63541c..5c33d9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@ffmpeg-installer/ffmpeg': + specifier: 1.1.0 + version: 1.1.0 '@formkit/auto-animate': specifier: 0.8.1 version: 0.8.1 @@ -38,6 +41,9 @@ dependencies: clsx: specifier: 2.0.0 version: 2.0.0 + fluent-ffmpeg: + specifier: 2.1.2 + version: 2.1.2 i18next: specifier: 23.6.0 version: 23.6.0 @@ -91,6 +97,9 @@ devDependencies: '@testing-library/user-event': specifier: 14.5.1 version: 14.5.1(@testing-library/dom@9.3.3) + '@types/fluent-ffmpeg': + specifier: 2.1.24 + version: 2.1.24 '@types/react': specifier: 18.2.31 version: 18.2.31 @@ -733,6 +742,83 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@ffmpeg-installer/darwin-arm64@4.1.5: + resolution: {integrity: sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/darwin-x64@4.1.0: + resolution: {integrity: sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/ffmpeg@1.1.0: + resolution: {integrity: sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==} + optionalDependencies: + '@ffmpeg-installer/darwin-arm64': 4.1.5 + '@ffmpeg-installer/darwin-x64': 4.1.0 + '@ffmpeg-installer/linux-arm': 4.1.3 + '@ffmpeg-installer/linux-arm64': 4.1.4 + '@ffmpeg-installer/linux-ia32': 4.1.0 + '@ffmpeg-installer/linux-x64': 4.1.0 + '@ffmpeg-installer/win32-ia32': 4.1.0 + '@ffmpeg-installer/win32-x64': 4.1.0 + dev: false + + /@ffmpeg-installer/linux-arm64@4.1.4: + resolution: {integrity: sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/linux-arm@4.1.3: + resolution: {integrity: sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/linux-ia32@4.1.0: + resolution: {integrity: sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/linux-x64@4.1.0: + resolution: {integrity: sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/win32-ia32@4.1.0: + resolution: {integrity: sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@ffmpeg-installer/win32-x64@4.1.0: + resolution: {integrity: sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@floating-ui/core@1.5.0: resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} dependencies: @@ -1609,6 +1695,12 @@ packages: '@types/ms': 0.7.33 dev: true + /@types/fluent-ffmpeg@2.1.24: + resolution: {integrity: sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==} + dependencies: + '@types/node': 20.8.7 + dev: true + /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: @@ -2143,7 +2235,6 @@ packages: /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} - dev: true /asynciterator.prototype@1.0.0: resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} @@ -3468,6 +3559,14 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /fluent-ffmpeg@2.1.2: + resolution: {integrity: sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==} + engines: {node: '>=0.8.0'} + dependencies: + async: 3.2.4 + which: 1.3.1 + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -4129,7 +4228,6 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true /iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -6186,6 +6284,13 @@ packages: has-tostringtag: 1.0.0 dev: true + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/src/schemas/conversionSettings.schema.ts b/schema/conversionSettings.schema.ts similarity index 74% rename from src/schemas/conversionSettings.schema.ts rename to schema/conversionSettings.schema.ts index 085ad60..c8c9a48 100644 --- a/src/schemas/conversionSettings.schema.ts +++ b/schema/conversionSettings.schema.ts @@ -27,3 +27,11 @@ export type SampleRate = z.infer; export const channelsSchema = z.enum(['default', '1', '2', '3', '4', '5', '6', '7', '8']); export type Channels = z.infer; + +export const conversionSettingsSchema = z.object({ + codec: codecSchema, + bitrate: bitrateSchema, + sampleRate: sampleRateSchema, + channels: channelsSchema, +}); +export type ConversionSettings = z.infer; diff --git a/src/types/file.types.ts b/schema/file.types.ts similarity index 100% rename from src/types/file.types.ts rename to schema/file.types.ts diff --git a/schema/index.ts b/schema/index.ts new file mode 100644 index 0000000..0bd8a0d --- /dev/null +++ b/schema/index.ts @@ -0,0 +1,2 @@ +export * from './conversionSettings.schema'; +export * from './file.types'; diff --git a/src/components/ConversionSettings/index.ts b/src/components/ConversionSettings/index.ts deleted file mode 100644 index 1d29261..0000000 --- a/src/components/ConversionSettings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ConversionSettings } from './ConversionSettings'; diff --git a/src/components/ConversionSettings/BitrateSelect.tsx b/src/components/ConversionSettingsForm/BitrateSelect.tsx similarity index 90% rename from src/components/ConversionSettings/BitrateSelect.tsx rename to src/components/ConversionSettingsForm/BitrateSelect.tsx index 1477b27..9b0153d 100644 --- a/src/components/ConversionSettings/BitrateSelect.tsx +++ b/src/components/ConversionSettingsForm/BitrateSelect.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { codecSchema } from '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'; +import type { Bitrate, Codec } from 'schema'; type BitrateSelectProps = { codec: Codec; diff --git a/src/components/ConversionSettings/ChannelsSelect.tsx b/src/components/ConversionSettingsForm/ChannelsSelect.tsx similarity index 90% rename from src/components/ConversionSettings/ChannelsSelect.tsx rename to src/components/ConversionSettingsForm/ChannelsSelect.tsx index ff47990..00780b9 100644 --- a/src/components/ConversionSettings/ChannelsSelect.tsx +++ b/src/components/ConversionSettingsForm/ChannelsSelect.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { codecSchema } from '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'; +import type { Channels, Codec } from 'schema'; type ChannelsSelectProps = { codec: Codec; diff --git a/src/components/ConversionSettings/CodecSelect.tsx b/src/components/ConversionSettingsForm/CodecSelect.tsx similarity index 88% rename from src/components/ConversionSettings/CodecSelect.tsx rename to src/components/ConversionSettingsForm/CodecSelect.tsx index 98cb9eb..38058f6 100644 --- a/src/components/ConversionSettings/CodecSelect.tsx +++ b/src/components/ConversionSettingsForm/CodecSelect.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { codecSchema } from 'src/schemas/conversionSettings.schema'; +import { codecSchema } from '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'; +import type { Codec } from 'schema'; type CodecSelectProps = { onChange: (value: Codec) => void; diff --git a/src/components/ConversionSettings/ConversionSettings.tsx b/src/components/ConversionSettingsForm/ConversionSettingsForm.tsx similarity index 80% rename from src/components/ConversionSettings/ConversionSettings.tsx rename to src/components/ConversionSettingsForm/ConversionSettingsForm.tsx index 15f025c..727c24e 100644 --- a/src/components/ConversionSettings/ConversionSettings.tsx +++ b/src/components/ConversionSettingsForm/ConversionSettingsForm.tsx @@ -1,14 +1,19 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { bitrateSchema, channelsSchema, codecSchema, sampleRateSchema } from 'schema'; 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'; +import type { ConversionSettings } from 'schema'; -export const ConversionSettings = () => { +type ConversionSettingsFormProps = { + onStartConversion: (conversionSettings: ConversionSettings) => void; +}; + +export const ConversionSettingsForm = ({ onStartConversion }: ConversionSettingsFormProps) => { const { t } = useTranslation(); const form = useConversionSettingsForm({ defaultValues: { @@ -18,7 +23,7 @@ export const ConversionSettings = () => { channels: channelsSchema.enum.default, }, }); - const { control, setValue, watch } = form; + const { control, handleSubmit, setValue, watch } = form; const codec = watch('codec'); useEffect(() => { @@ -29,7 +34,7 @@ export const ConversionSettings = () => { return (
- +

{t('conversionSettings.title')}

{ const { name, path, progress, size, status } = file; const formattedFileSize = formatFileSize(size); const removeFile = useStore(state => state.removeFile); - const isConverting = status === fileStatusSchema.enum.converting; + const isProgressDisplayed = status === fileStatusSchema.enum.converting && progress > 0; return ( @@ -41,7 +41,7 @@ export const FileCard = ({ file }: FileCardProps) => { -
{isConverting && }
+
{isProgressDisplayed && }
); }; diff --git a/src/components/FileCard/FileCardIcon.tsx b/src/components/FileCard/FileCardIcon.tsx index 743fe22..2a145b4 100644 --- a/src/components/FileCard/FileCardIcon.tsx +++ b/src/components/FileCard/FileCardIcon.tsx @@ -1,6 +1,6 @@ import { AlertOctagonIcon, CheckCircleIcon, FileVideoIcon, Loader2Icon } from 'lucide-react'; -import { fileStatusSchema } from 'src/types/file.types'; -import type { FileStatus } from 'src/types/file.types'; +import { fileStatusSchema } from 'schema'; +import type { FileStatus } from 'schema'; type FileCardIconProps = { status: FileStatus; diff --git a/src/components/FileCard/__tests__/FileCard.test.tsx b/src/components/FileCard/__tests__/FileCard.test.tsx index 457fedf..2ed6d2d 100644 --- a/src/components/FileCard/__tests__/FileCard.test.tsx +++ b/src/components/FileCard/__tests__/FileCard.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; -import { fileStatusSchema } from 'src/types/file.types'; +import { fileStatusSchema } from 'schema'; import { describe, expect, it } from 'vitest'; import { FileCard } from '../FileCard'; -import type { VideoFile } from 'src/types/file.types'; +import type { VideoFile } from 'schema'; describe('FileCard', () => { it('should not display a progress bar if file is not converting', () => { @@ -18,7 +18,7 @@ describe('FileCard', () => { expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); - it('should display a progress bar if file is converting', () => { + it('should not display a progress bar if file is converting but progress is 0', () => { const file = { name: 'matrix.mkv', path: '/path/matrix.mkv', @@ -28,6 +28,19 @@ describe('FileCard', () => { } as VideoFile; render(); + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); + + it('should display a progress bar if file is converting but progress is more than 0', () => { + const file = { + name: 'matrix.mkv', + path: '/path/matrix.mkv', + progress: 50, + size: 1024, + status: fileStatusSchema.enum.converting, + } as VideoFile; + render(); + expect(screen.getByRole('progressbar')).toBeVisible(); }); }); diff --git a/src/components/FileCard/__tests__/FileCardIcon.test.tsx b/src/components/FileCard/__tests__/FileCardIcon.test.tsx index 5a62a88..a894ab5 100644 --- a/src/components/FileCard/__tests__/FileCardIcon.test.tsx +++ b/src/components/FileCard/__tests__/FileCardIcon.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { fileStatusSchema } from 'src/types/file.types'; +import { fileStatusSchema } from 'schema'; import { describe, expect, it } from 'vitest'; import { FileCardIcon } from '../FileCardIcon'; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index bd7c6af..39cb864 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -6,17 +6,13 @@ import { DestinationInput } from './DestinationInput'; export const Footer = () => { const { t } = useTranslation(); - const files = useStore(getFilesToConvert); - const isButtonDisabled = files.length === 0; - - const handleConversionStart = () => { - window.conversion.startConversion({ files }); - }; + const filesToConvert = useStore(getFilesToConvert); + const isButtonDisabled = filesToConvert.length === 0; return (