mirror of
https://github.com/codeforreal1/compressO
synced 2026-04-21 15:47:56 +00:00
feat: improve metadata stripping
This commit is contained in:
parent
885c8b74d1
commit
abc0ad72f4
24 changed files with 303 additions and 209 deletions
|
|
@ -143,16 +143,17 @@ pub struct VideoCompressionConfig {
|
|||
pub fps: Option<String>,
|
||||
pub video_codec: Option<String>,
|
||||
pub transform_history: Option<MediaTransformHistory>,
|
||||
pub metadata_config: Option<VideoMetadataConfig>,
|
||||
pub strip_metadata: Option<bool>,
|
||||
pub metadata_config: Option<MediaMetadataConfig>,
|
||||
pub custom_thumbnail_path: Option<String>,
|
||||
pub should_enable_custom_thumbnail: Option<bool>,
|
||||
pub trim_segments: Option<Vec<VideoTrimSegment>>,
|
||||
pub subtitles_config: Option<SubtitlesConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoMetadataConfig {
|
||||
pub struct MediaMetadataConfig {
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::core::domain::{
|
||||
AudioConfig, BatchCompressionResult, BatchVideoCompressionProgress,
|
||||
BatchVideoIndividualCompressionResult, CustomEvents, MediaTransform, MediaTransformCrop,
|
||||
MediaTransformHistory, SubtitlesConfig, VideoCompressionConfig, VideoCompressionProgress,
|
||||
VideoCompressionResult, VideoMetadataConfig, VideoThumbnail, VideoTrimSegment,
|
||||
BatchVideoIndividualCompressionResult, CustomEvents, MediaMetadataConfig, MediaTransform,
|
||||
MediaTransformCrop, MediaTransformHistory, SubtitlesConfig, VideoCompressionConfig,
|
||||
VideoCompressionProgress, VideoCompressionResult, VideoThumbnail, VideoTrimSegment,
|
||||
};
|
||||
use crate::core::ffprobe::FFPROBE;
|
||||
use crate::core::image::ImageCompressor;
|
||||
|
|
@ -62,7 +62,8 @@ impl FFMPEG {
|
|||
fps: Option<&str>,
|
||||
video_codec: Option<&str>,
|
||||
transform_history: Option<&MediaTransformHistory>,
|
||||
metadata_config: Option<&VideoMetadataConfig>,
|
||||
strip_metadata: Option<bool>,
|
||||
metadata_config: Option<&MediaMetadataConfig>,
|
||||
custom_thumbnail_path: Option<&str>,
|
||||
trim_segments: Option<&Vec<VideoTrimSegment>>,
|
||||
subtitles_config: Option<&SubtitlesConfig>,
|
||||
|
|
@ -158,8 +159,12 @@ impl FFMPEG {
|
|||
Vec::new()
|
||||
};
|
||||
|
||||
// Preserve existing metadata
|
||||
cmd_args.extend_from_slice(&["-map_metadata", "0"]);
|
||||
let should_strip_metadata = strip_metadata.unwrap_or(false);
|
||||
if should_strip_metadata {
|
||||
cmd_args.extend_from_slice(&["-map_metadata", "-1"]);
|
||||
} else {
|
||||
cmd_args.extend_from_slice(&["-map_metadata", "0"]);
|
||||
}
|
||||
|
||||
cmd_args.extend_from_slice(&[
|
||||
"-hide_banner",
|
||||
|
|
@ -503,7 +508,7 @@ impl FFMPEG {
|
|||
|
||||
let mut metadata_args: Vec<String> = Vec::new();
|
||||
|
||||
if !is_gif_target {
|
||||
if !is_gif_target && !should_strip_metadata {
|
||||
if let Some(metadata) = metadata_config {
|
||||
if let Some(ref title) = metadata.title {
|
||||
metadata_args.push("-metadata".to_string());
|
||||
|
|
@ -771,6 +776,7 @@ impl FFMPEG {
|
|||
let thumbnail_path = video_options.custom_thumbnail_path.as_deref();
|
||||
let trim_segments = video_options.trim_segments.as_ref();
|
||||
let subtitles_config = video_options.subtitles_config.as_ref();
|
||||
let strip_metadata = video_options.strip_metadata;
|
||||
|
||||
match ffmpeg_instance
|
||||
.compress_video(
|
||||
|
|
@ -785,6 +791,7 @@ impl FFMPEG {
|
|||
fps,
|
||||
video_codec,
|
||||
transform_history,
|
||||
strip_metadata,
|
||||
metadata_config,
|
||||
thumbnail_path,
|
||||
trim_segments,
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ impl ImageCompressor {
|
|||
let transform_input_path = if !original_extension.eq("svg")
|
||||
&& (dimensions.is_some() || transform_history.is_some())
|
||||
{
|
||||
log::info!("[image] Applying transformations before compression");
|
||||
let transform_temp_filename =
|
||||
format!("{}_transformed.{}", image_id, original_extension);
|
||||
let transform_temp_path: PathBuf = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::core::{
|
||||
domain::{
|
||||
AudioConfig, BatchCompressionResult, MediaTransformHistory, SubtitlesConfig,
|
||||
VideoCompressionConfig, VideoCompressionResult, VideoMetadataConfig, VideoThumbnail,
|
||||
AudioConfig, BatchCompressionResult, MediaMetadataConfig, MediaTransformHistory,
|
||||
SubtitlesConfig, VideoCompressionConfig, VideoCompressionResult, VideoThumbnail,
|
||||
VideoTrimSegment,
|
||||
},
|
||||
ffmpeg::{self},
|
||||
|
|
@ -22,10 +22,11 @@ pub async fn compress_video(
|
|||
fps: Option<&str>,
|
||||
video_codec: Option<&str>,
|
||||
transform_history: Option<MediaTransformHistory>,
|
||||
metadata_config: Option<VideoMetadataConfig>,
|
||||
metadata_config: Option<MediaMetadataConfig>,
|
||||
custom_thumbnail_path: Option<&str>,
|
||||
trim_segments: Option<Vec<VideoTrimSegment>>,
|
||||
subtitles_config: Option<SubtitlesConfig>,
|
||||
strip_metadata: Option<bool>,
|
||||
) -> Result<VideoCompressionResult, String> {
|
||||
let mut ffmpeg = ffmpeg::FFMPEG::new(&app)?;
|
||||
if let Ok(files) =
|
||||
|
|
@ -49,6 +50,7 @@ pub async fn compress_video(
|
|||
fps,
|
||||
video_codec,
|
||||
transform_history.as_ref(),
|
||||
strip_metadata,
|
||||
metadata_config.as_ref(),
|
||||
custom_thumbnail_path,
|
||||
trim_segments.as_ref(),
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ pub async fn compress_media_batch(
|
|||
let thumbnail_path = video_config.custom_thumbnail_path.as_deref();
|
||||
let trim_segments = video_config.trim_segments.as_ref();
|
||||
let subtitles_config = video_config.subtitles_config.as_ref();
|
||||
let strip_metadata = video_config.strip_metadata;
|
||||
|
||||
let mut ffmpeg_instance = FFMPEG::new(&app)
|
||||
.map_err(|e| format!("Failed to create ffmpeg instance: {}", e))?;
|
||||
|
|
@ -84,6 +85,7 @@ pub async fn compress_media_batch(
|
|||
fps,
|
||||
video_codec,
|
||||
transform_history,
|
||||
strip_metadata,
|
||||
metadata_config,
|
||||
thumbnail_path,
|
||||
trim_segments,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { proxy } from 'valtio'
|
|||
import {
|
||||
App,
|
||||
ImageConfig,
|
||||
MediaMetadataConfig,
|
||||
VideoConfig,
|
||||
VideoMetadataConfig,
|
||||
} from '../../types/app'
|
||||
|
||||
export const videoMetadataConfigInitialState: VideoMetadataConfig = {
|
||||
export const videoMetadataConfigInitialState: MediaMetadataConfig = {
|
||||
title: '',
|
||||
album: '',
|
||||
artist: '',
|
||||
|
|
@ -41,7 +41,7 @@ export const videoConfigInitialState: VideoConfig = {
|
|||
shouldEnableAudioTrackSelection: false,
|
||||
quality: 50,
|
||||
shouldEnableQuality: false,
|
||||
shouldPreserveMetadata: false,
|
||||
shouldStripMetadata: true,
|
||||
metadataConfig: null,
|
||||
customThumbnailPath: null,
|
||||
shouldEnableCustomThumbnail: false,
|
||||
|
|
@ -69,7 +69,7 @@ export const imageConfigInitialState: ImageConfig = {
|
|||
convertToExtension: '-',
|
||||
isLossless: false,
|
||||
quality: 50,
|
||||
stripMetadata: true,
|
||||
shouldStripMetadata: true,
|
||||
svgScaleFactor: 4,
|
||||
shouldEnableAdvancedSvgSetting: false,
|
||||
svgConfig: svgSettingInitialState,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { snapshot, useSnapshot } from 'valtio'
|
|||
|
||||
import Button from '@/components/Button'
|
||||
import { compressMediaBatch } from '@/tauri/commands/media'
|
||||
import { VideoMetadataConfig } from '@/types/app'
|
||||
import { MediaMetadataConfig } from '@/types/app'
|
||||
import {
|
||||
CustomEvents,
|
||||
ImageCompressionConfig,
|
||||
|
|
@ -150,11 +150,11 @@ function StartCompression() {
|
|||
? ((v.config.transformVideoConfig?.transformHistory ??
|
||||
[]) as MediaTransformHistory[])
|
||||
: null,
|
||||
stripMetadata: v.config?.shouldStripMetadata,
|
||||
metadataConfig:
|
||||
!v.config?.shouldPreserveMetadata &&
|
||||
v.config?.metadataConfig
|
||||
!v.config?.shouldStripMetadata && v.config?.metadataConfig
|
||||
? Object.entries(
|
||||
v.config?.metadataConfig as VideoMetadataConfig,
|
||||
v.config?.metadataConfig as MediaMetadataConfig,
|
||||
).reduce(
|
||||
(a, [key, value]: [string, any]) => {
|
||||
a[key] = value?.length > 0 ? value : null
|
||||
|
|
@ -215,7 +215,7 @@ function StartCompression() {
|
|||
quality: v.config.isLossless
|
||||
? 100
|
||||
: (v.config.quality ?? 100),
|
||||
stripMetadata: v.config.stripMetadata,
|
||||
stripMetadata: v.config.shouldStripMetadata,
|
||||
svgScaleFactor: v.config.svgScaleFactor ?? null,
|
||||
svgConfig: v.config?.shouldEnableAdvancedSvgSetting
|
||||
? (v.config.svgConfig ?? null)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
import { Divider } from '@heroui/react'
|
||||
import { Divider, Tab } from '@heroui/react'
|
||||
import { useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import Tabs from '@/components/Tabs'
|
||||
import { appProxy } from '@/routes/(root)/-state'
|
||||
import { ImageExtension as ImageExtensionType } from '@/types/compression'
|
||||
import CompressionQuality from './CompressionQuality'
|
||||
import ImageDimensions from './ImageDimensions'
|
||||
import ImageExtension from './ImageExtension'
|
||||
import ImageMetadata from './ImageMetadata'
|
||||
import SvgConfig from './SvgConfig'
|
||||
import SvgScaleFactor from './SvgScaleFactor'
|
||||
import TransformImage from './TransformImage'
|
||||
import CompressionQuality from './image/CompressionQuality'
|
||||
import ImageDimensions from './image/Dimensions'
|
||||
import ImageExtension from './image/Extension'
|
||||
import SvgConfig from './image/SvgConfig'
|
||||
import SvgScaleFactor from './image/SvgScaleFactor'
|
||||
import TransformImage from './image/TransformImage'
|
||||
import Others from './others/-index'
|
||||
|
||||
type ImageSettingsProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
const TABS = {
|
||||
image: {
|
||||
id: 'image',
|
||||
title: 'Image',
|
||||
},
|
||||
others: {
|
||||
id: 'others',
|
||||
title: 'Others',
|
||||
},
|
||||
} as const
|
||||
|
||||
function ImageSettings({ mediaIndex }: ImageSettingsProps) {
|
||||
const {
|
||||
state: { media, commonConfigForBatchCompression },
|
||||
|
|
@ -27,59 +40,85 @@ function ImageSettings({ mediaIndex }: ImageSettingsProps) {
|
|||
const { convertToExtension } =
|
||||
config ?? commonConfigForBatchCompression.imageConfig ?? {}
|
||||
|
||||
const [tab, setTab] = useState<keyof typeof TABS>('image')
|
||||
|
||||
return (
|
||||
<div className="space-y-3 my-3">
|
||||
<div>
|
||||
<CompressionQuality mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
<>
|
||||
<section>
|
||||
<Tabs
|
||||
aria-label="Compression Settings"
|
||||
size="sm"
|
||||
selectedKey={tab}
|
||||
onSelectionChange={(t) => setTab(t as keyof typeof TABS)}
|
||||
className="w-full"
|
||||
fullWidth
|
||||
classNames={{
|
||||
tab: 'h-6',
|
||||
tabContent: 'text-[11px]',
|
||||
}}
|
||||
>
|
||||
{Object.values(TABS).map((t) => (
|
||||
<Tab key={t.id} value={t.id} title={t.title} />
|
||||
))}
|
||||
</Tabs>
|
||||
<div className="my-4">
|
||||
{tab === 'image' ? (
|
||||
<div>
|
||||
<div>
|
||||
<CompressionQuality mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
|
||||
{(['svg'] as ImageExtensionType[]).includes(
|
||||
image?.extension as ImageExtensionType,
|
||||
) &&
|
||||
(['png', 'jpg', 'jpeg', 'webp'] as ImageExtensionType[]).includes(
|
||||
convertToExtension as ImageExtensionType,
|
||||
) ? (
|
||||
<div>
|
||||
<SvgScaleFactor mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3 !mt-6" />
|
||||
{(['svg'] as ImageExtensionType[]).includes(
|
||||
image?.extension as ImageExtensionType,
|
||||
) &&
|
||||
(['png', 'jpg', 'jpeg', 'webp'] as ImageExtensionType[]).includes(
|
||||
convertToExtension as ImageExtensionType,
|
||||
) ? (
|
||||
<div>
|
||||
<SvgScaleFactor mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3 !mt-6" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(
|
||||
['png', 'jpg', 'jpeg', 'webp', 'gif'] as ImageExtensionType[]
|
||||
).includes(imageExtension as ImageExtensionType) &&
|
||||
!(['svg'] as ImageExtensionType[]).includes(
|
||||
convertToExtension as ImageExtensionType,
|
||||
) ? (
|
||||
<>
|
||||
<div>
|
||||
<ImageDimensions mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
<div>
|
||||
<TransformImage mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{convertToExtension === 'svg' ? (
|
||||
<div>
|
||||
<SvgConfig mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="!mt-8">
|
||||
<ImageExtension mediaIndex={mediaIndex} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === 'others' ? (
|
||||
<div>
|
||||
<Others mediaIndex={mediaIndex} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(['png', 'jpg', 'jpeg', 'webp', 'gif'] as ImageExtensionType[]).includes(
|
||||
imageExtension as ImageExtensionType,
|
||||
) &&
|
||||
!(['svg'] as ImageExtensionType[]).includes(
|
||||
convertToExtension as ImageExtensionType,
|
||||
) ? (
|
||||
<>
|
||||
<div>
|
||||
<ImageDimensions mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
<div>
|
||||
<TransformImage mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<ImageMetadata mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
|
||||
{convertToExtension === 'svg' ? (
|
||||
<div>
|
||||
<SvgConfig mediaIndex={mediaIndex} />
|
||||
<Divider className="my-3" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="!mt-8">
|
||||
<ImageExtension mediaIndex={mediaIndex} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import { useCallback } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import Switch from '@/components/Switch'
|
||||
import { ImageExtension } from '@/types/compression'
|
||||
import { appProxy, normalizeBatchMediaConfig } from '../../../-state'
|
||||
|
||||
type ImageMetadataProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
const ImageMetadata = ({ mediaIndex }: ImageMetadataProps) => {
|
||||
const {
|
||||
state: {
|
||||
isCompressing,
|
||||
isProcessCompleted,
|
||||
media,
|
||||
commonConfigForBatchCompression,
|
||||
},
|
||||
} = useSnapshot(appProxy)
|
||||
const image =
|
||||
media.length > 0 && mediaIndex >= 0 && media[mediaIndex].type == 'image'
|
||||
? media[mediaIndex]
|
||||
: null
|
||||
const { config } = image ?? {}
|
||||
const { stripMetadata, convertToExtension } =
|
||||
config ?? commonConfigForBatchCompression.imageConfig ?? {}
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(preserveMetadata: boolean) => {
|
||||
const shouldStripMetadata = !preserveMetadata
|
||||
if (
|
||||
mediaIndex >= 0 &&
|
||||
appProxy.state.media[mediaIndex].type === 'image' &&
|
||||
appProxy.state.media[mediaIndex]?.config
|
||||
) {
|
||||
appProxy.state.media[mediaIndex].config.stripMetadata =
|
||||
shouldStripMetadata
|
||||
appProxy.state.media[mediaIndex].isConfigDirty = true
|
||||
} else {
|
||||
if (appProxy.state.media.length > 1) {
|
||||
appProxy.state.commonConfigForBatchCompression.imageConfig.stripMetadata =
|
||||
shouldStripMetadata
|
||||
normalizeBatchMediaConfig()
|
||||
}
|
||||
}
|
||||
},
|
||||
[mediaIndex],
|
||||
)
|
||||
|
||||
const shouldDisableInput =
|
||||
media.length === 0 ||
|
||||
isCompressing ||
|
||||
isProcessCompleted ||
|
||||
(['gif', 'svg', 'webp'] as ImageExtension[]).includes(
|
||||
image?.extension as ImageExtension,
|
||||
) ||
|
||||
(['svg', 'webp'] as ImageExtension[]).includes(
|
||||
convertToExtension as ImageExtension,
|
||||
)
|
||||
|
||||
return (
|
||||
<Switch
|
||||
isSelected={!stripMetadata}
|
||||
onValueChange={handleValueChange}
|
||||
isDisabled={shouldDisableInput}
|
||||
>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mr-2 w-full">
|
||||
Preserve Metadata
|
||||
</p>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageMetadata
|
||||
|
|
@ -6,12 +6,12 @@ import { useSnapshot } from 'valtio'
|
|||
import Slider from '@/components/Slider'
|
||||
import Switch from '@/components/Switch'
|
||||
import { useSyncState } from '@/hooks/useSyncState'
|
||||
import { slideDownTransition } from '@/utils/animation'
|
||||
import {
|
||||
appProxy,
|
||||
imageConfigInitialState,
|
||||
normalizeBatchMediaConfig,
|
||||
} from '../../../-state'
|
||||
} from '@/routes/(root)/-state'
|
||||
import { slideDownTransition } from '@/utils/animation'
|
||||
|
||||
type CompressionQualityProps = {
|
||||
mediaIndex: number
|
||||
|
|
@ -9,11 +9,11 @@ import Switch from '@/components/Switch'
|
|||
import { appProxy } from '@/routes/(root)/-state'
|
||||
import { slideDownTransition } from '@/utils/animation'
|
||||
|
||||
type ImageDimensionsProps = {
|
||||
type DimensionsProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
function ImageDimensions({ mediaIndex }: ImageDimensionsProps) {
|
||||
function Dimensions({ mediaIndex }: DimensionsProps) {
|
||||
if (mediaIndex < 0) return
|
||||
|
||||
const {
|
||||
|
|
@ -131,7 +131,7 @@ function ImageDimensions({ mediaIndex }: ImageDimensionsProps) {
|
|||
return
|
||||
}
|
||||
const targetImage = appProxy.state.media[mediaIndex]
|
||||
const targetImageDimensions = targetImage.config?.shouldTransformImage
|
||||
const targetDimensions = targetImage.config?.shouldTransformImage
|
||||
? {
|
||||
width:
|
||||
targetImage?.config?.transformImageConfig?.transforms?.crop
|
||||
|
|
@ -142,14 +142,13 @@ function ImageDimensions({ mediaIndex }: ImageDimensionsProps) {
|
|||
}
|
||||
: targetImage?.dimensions
|
||||
if (
|
||||
targetImageDimensions == null ||
|
||||
Number.isNaN(targetImageDimensions?.width) ||
|
||||
Number.isNaN(targetImageDimensions?.height)
|
||||
targetDimensions == null ||
|
||||
Number.isNaN(targetDimensions?.width) ||
|
||||
Number.isNaN(targetDimensions?.height)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const aspectRatio =
|
||||
targetImageDimensions.width! / targetImageDimensions.height!
|
||||
const aspectRatio = targetDimensions.width! / targetDimensions.height!
|
||||
const _dimensions: [number, number] =
|
||||
type === 'width'
|
||||
? [value, Math.round(value / aspectRatio)]
|
||||
|
|
@ -248,4 +247,4 @@ function ImageDimensions({ mediaIndex }: ImageDimensionsProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export default React.memo(ImageDimensions)
|
||||
export default React.memo(Dimensions)
|
||||
|
|
@ -6,12 +6,12 @@ import Select from '@/components/Select'
|
|||
import { appProxy, normalizeBatchMediaConfig } from '@/routes/(root)/-state'
|
||||
import { extensions } from '@/types/compression'
|
||||
|
||||
type ImageExtensionProps = {
|
||||
type ExtensionProps = {
|
||||
mediaIndex: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ImageExtension = ({ mediaIndex, disabled }: ImageExtensionProps) => {
|
||||
const Extension = ({ mediaIndex, disabled }: ExtensionProps) => {
|
||||
const {
|
||||
state: {
|
||||
media,
|
||||
|
|
@ -90,4 +90,4 @@ const ImageExtension = ({ mediaIndex, disabled }: ImageExtensionProps) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default ImageExtension
|
||||
export default Extension
|
||||
|
|
@ -8,12 +8,12 @@ import Slider from '@/components/Slider'
|
|||
import Switch from '@/components/Switch'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import { useSyncState } from '@/hooks/useSyncState'
|
||||
import { slideDownTransition } from '@/utils/animation'
|
||||
import {
|
||||
appProxy,
|
||||
normalizeBatchMediaConfig,
|
||||
svgSettingInitialState,
|
||||
} from '../../../-state'
|
||||
} from '@/routes/(root)/-state'
|
||||
import { slideDownTransition } from '@/utils/animation'
|
||||
|
||||
type SvgConfigProps = {
|
||||
mediaIndex: number
|
||||
|
|
@ -6,11 +6,11 @@ import Icon from '@/components/Icon'
|
|||
import Switch from '@/components/Switch'
|
||||
import { appProxy } from '@/routes/(root)/-state'
|
||||
|
||||
type TransformImageProps = {
|
||||
type TransformProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
function TransformImage({ mediaIndex }: TransformImageProps) {
|
||||
function Transform({ mediaIndex }: TransformProps) {
|
||||
if (mediaIndex < 0) return
|
||||
|
||||
const {
|
||||
|
|
@ -98,4 +98,4 @@ function TransformImage({ mediaIndex }: TransformImageProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export default TransformImage
|
||||
export default Transform
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import Metadata from './Metadata'
|
||||
|
||||
type OthersProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
function Others({ mediaIndex }: OthersProps) {
|
||||
return (
|
||||
<div>
|
||||
<Metadata mediaIndex={mediaIndex} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Others
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { useCallback } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import Switch from '@/components/Switch'
|
||||
import {
|
||||
appProxy,
|
||||
normalizeBatchMediaConfig,
|
||||
videoMetadataConfigInitialState,
|
||||
} from '@/routes/(root)/-state'
|
||||
import { ImageExtension } from '@/types/compression'
|
||||
|
||||
type MetadataProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
function Metadata({ mediaIndex }: MetadataProps) {
|
||||
const {
|
||||
state: {
|
||||
isCompressing,
|
||||
isProcessCompleted,
|
||||
media,
|
||||
commonConfigForBatchCompression,
|
||||
isLoadingMediaFiles,
|
||||
},
|
||||
} = useSnapshot(appProxy)
|
||||
const image =
|
||||
media.length > 0 && mediaIndex >= 0 && media[mediaIndex].type == 'image'
|
||||
? media[mediaIndex]
|
||||
: null
|
||||
const { config } = image ?? {}
|
||||
const { shouldStripMetadata, convertToExtension } =
|
||||
config ?? commonConfigForBatchCompression.imageConfig ?? {}
|
||||
|
||||
const handleStripMetadataToggle = useCallback(() => {
|
||||
if (
|
||||
mediaIndex >= 0 &&
|
||||
appProxy.state.media[mediaIndex].type === 'image' &&
|
||||
appProxy.state.media[mediaIndex]?.config
|
||||
) {
|
||||
appProxy.state.media[mediaIndex].config.shouldStripMetadata =
|
||||
!appProxy.state.media[mediaIndex].config.shouldStripMetadata
|
||||
appProxy.state.media[mediaIndex].isConfigDirty = true
|
||||
|
||||
if (appProxy.state.media[mediaIndex].config.shouldStripMetadata) {
|
||||
appProxy.state.media[mediaIndex].config.metadataConfig = null
|
||||
} else {
|
||||
appProxy.state.media[mediaIndex].config.metadataConfig = cloneDeep(
|
||||
videoMetadataConfigInitialState,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (appProxy.state.media.length > 1) {
|
||||
appProxy.state.commonConfigForBatchCompression.imageConfig.shouldStripMetadata =
|
||||
!appProxy.state.commonConfigForBatchCompression.imageConfig
|
||||
.shouldStripMetadata
|
||||
|
||||
if (
|
||||
appProxy.state.commonConfigForBatchCompression.imageConfig
|
||||
.shouldStripMetadata
|
||||
) {
|
||||
appProxy.state.commonConfigForBatchCompression.imageConfig.metadataConfig =
|
||||
null
|
||||
} else {
|
||||
appProxy.state.commonConfigForBatchCompression.imageConfig.metadataConfig =
|
||||
cloneDeep(videoMetadataConfigInitialState)
|
||||
}
|
||||
|
||||
normalizeBatchMediaConfig()
|
||||
}
|
||||
}
|
||||
}, [mediaIndex])
|
||||
|
||||
const shouldDisableInput =
|
||||
media.length === 0 ||
|
||||
isCompressing ||
|
||||
isProcessCompleted ||
|
||||
isLoadingMediaFiles ||
|
||||
(['gif', 'svg', 'webp'] as ImageExtension[]).includes(
|
||||
image?.extension as ImageExtension,
|
||||
) ||
|
||||
(['svg', 'webp'] as ImageExtension[]).includes(
|
||||
convertToExtension as ImageExtension,
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
isSelected={shouldStripMetadata}
|
||||
onValueChange={handleStripMetadataToggle}
|
||||
isDisabled={shouldDisableInput}
|
||||
>
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 block mr-2 text-sm">
|
||||
Strip Metadata
|
||||
</span>
|
||||
</div>
|
||||
</Switch>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Metadata
|
||||
|
|
@ -4,7 +4,6 @@ import { useSnapshot } from 'valtio'
|
|||
|
||||
import Tabs from '@/components/Tabs'
|
||||
import { getAudioStreams } from '@/tauri/commands/ffprobe'
|
||||
import { appProxy } from '../../../-state'
|
||||
import AudioBitrate from './audio/AudioBitrate'
|
||||
import AudioChannels from './audio/AudioChannels'
|
||||
import AudioCodec from './audio/AudioCodec'
|
||||
|
|
@ -14,12 +13,13 @@ import Others from './others/-index'
|
|||
import CompressionPreset from './video/CompressionPreset'
|
||||
import CompressionQuality from './video/CompressionQuality'
|
||||
import CustomThumbnail from './video/CustomThumbnail'
|
||||
import VideoDimensions from './video/Dimensions'
|
||||
import VideoExtension from './video/Extension'
|
||||
import VideoFPS from './video/FPS'
|
||||
import TransformVideo from './video/TransformVideo'
|
||||
import TrimVideo from './video/TrimVideo'
|
||||
import VideoCodec from './video/VideoCodec'
|
||||
import VideoDimensions from './video/VideoDimensions'
|
||||
import VideoExtension from './video/VideoExtension'
|
||||
import VideoFPS from './video/VideoFPS'
|
||||
import { appProxy } from '../../../-state'
|
||||
|
||||
type VideoSettingsProps = {
|
||||
mediaIndex: number // if mediaIndex < 0, we'll only show settings that applies to all videos
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Divider from '@/components/Divider'
|
|||
import Switch from '@/components/Switch'
|
||||
import TextArea from '@/components/TextArea'
|
||||
import TextInput from '@/components/TextInput'
|
||||
import type { VideoMetadataConfig } from '@/types/app'
|
||||
import type { MediaMetadataConfig } from '@/types/app'
|
||||
import { slideDownTransition } from '@/utils/animation'
|
||||
import {
|
||||
appProxy,
|
||||
|
|
@ -36,12 +36,12 @@ function Metadata({ mediaIndex }: MetadataProps) {
|
|||
? media[mediaIndex]
|
||||
: null
|
||||
const { config } = video ?? {}
|
||||
const { shouldPreserveMetadata, metadataConfig, convertToExtension } =
|
||||
const { shouldStripMetadata, metadataConfig, convertToExtension } =
|
||||
config ?? commonConfigForBatchCompression.videoConfig ?? {}
|
||||
|
||||
const updateMetadataField = useCallback(
|
||||
(
|
||||
field: keyof VideoMetadataConfig,
|
||||
field: keyof MediaMetadataConfig,
|
||||
value: string | boolean | null | undefined,
|
||||
) => {
|
||||
if (
|
||||
|
|
@ -98,17 +98,17 @@ function Metadata({ mediaIndex }: MetadataProps) {
|
|||
[mediaIndex],
|
||||
)
|
||||
|
||||
const handlePreserveMetadataToggle = useCallback(() => {
|
||||
const handleStripMetadataToggle = useCallback(() => {
|
||||
if (
|
||||
mediaIndex >= 0 &&
|
||||
appProxy.state.media[mediaIndex].type === 'video' &&
|
||||
appProxy.state.media[mediaIndex]?.config
|
||||
) {
|
||||
appProxy.state.media[mediaIndex].config.shouldPreserveMetadata =
|
||||
!appProxy.state.media[mediaIndex].config.shouldPreserveMetadata
|
||||
appProxy.state.media[mediaIndex].config.shouldStripMetadata =
|
||||
!appProxy.state.media[mediaIndex].config.shouldStripMetadata
|
||||
appProxy.state.media[mediaIndex].isConfigDirty = true
|
||||
|
||||
if (appProxy.state.media[mediaIndex].config.shouldPreserveMetadata) {
|
||||
if (appProxy.state.media[mediaIndex].config.shouldStripMetadata) {
|
||||
appProxy.state.media[mediaIndex].config.metadataConfig = null
|
||||
} else {
|
||||
appProxy.state.media[mediaIndex].config.metadataConfig = cloneDeep(
|
||||
|
|
@ -117,13 +117,13 @@ function Metadata({ mediaIndex }: MetadataProps) {
|
|||
}
|
||||
} else {
|
||||
if (appProxy.state.media.length > 1) {
|
||||
appProxy.state.commonConfigForBatchCompression.videoConfig.shouldPreserveMetadata =
|
||||
appProxy.state.commonConfigForBatchCompression.videoConfig.shouldStripMetadata =
|
||||
!appProxy.state.commonConfigForBatchCompression.videoConfig
|
||||
.shouldPreserveMetadata
|
||||
.shouldStripMetadata
|
||||
|
||||
if (
|
||||
appProxy.state.commonConfigForBatchCompression.videoConfig
|
||||
.shouldPreserveMetadata
|
||||
.shouldStripMetadata
|
||||
) {
|
||||
appProxy.state.commonConfigForBatchCompression.videoConfig.metadataConfig =
|
||||
null
|
||||
|
|
@ -147,18 +147,18 @@ function Metadata({ mediaIndex }: MetadataProps) {
|
|||
return (
|
||||
<>
|
||||
<Switch
|
||||
isSelected={shouldPreserveMetadata}
|
||||
onValueChange={handlePreserveMetadataToggle}
|
||||
isSelected={shouldStripMetadata}
|
||||
onValueChange={handleStripMetadataToggle}
|
||||
isDisabled={shouldDisableInput}
|
||||
>
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 block mr-2 text-sm">
|
||||
Preserve Metadata
|
||||
Strip Metadata
|
||||
</span>
|
||||
</div>
|
||||
</Switch>
|
||||
<AnimatePresence mode="wait">
|
||||
{!shouldPreserveMetadata ? (
|
||||
{!shouldStripMetadata ? (
|
||||
<Card className="px-2 my-2 pb-4 shadow-none border-1 dark:border-none">
|
||||
<motion.div {...slideDownTransition} className="space-y-4 mt-2">
|
||||
<div className="text-zinc-700 dark:text-zinc-400">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ type VideoDimensionsProps = {
|
|||
mediaIndex: number
|
||||
}
|
||||
|
||||
function VideoDimensions({ mediaIndex }: VideoDimensionsProps) {
|
||||
function Dimensions({ mediaIndex }: VideoDimensionsProps) {
|
||||
if (mediaIndex < 0) return
|
||||
|
||||
const {
|
||||
|
|
@ -248,4 +248,4 @@ function VideoDimensions({ mediaIndex }: VideoDimensionsProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export default React.memo(VideoDimensions)
|
||||
export default React.memo(Dimensions)
|
||||
|
|
@ -12,7 +12,7 @@ type VideoExtensionProps = {
|
|||
mediaIndex: number
|
||||
}
|
||||
|
||||
function VideoExtension({ mediaIndex }: VideoExtensionProps) {
|
||||
function Extension({ mediaIndex }: VideoExtensionProps) {
|
||||
const {
|
||||
state: {
|
||||
media,
|
||||
|
|
@ -99,4 +99,4 @@ function VideoExtension({ mediaIndex }: VideoExtensionProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export default VideoExtension
|
||||
export default Extension
|
||||
|
|
@ -9,13 +9,13 @@ import Switch from '@/components/Switch'
|
|||
import { slideDownTransition } from '@/utils/animation'
|
||||
import { appProxy, normalizeBatchMediaConfig } from '../../../../-state'
|
||||
|
||||
const FPS = [24, 25, 30, 50, 60] as const
|
||||
const fps = [24, 25, 30, 50, 60] as const
|
||||
|
||||
type VideoFPSProps = {
|
||||
mediaIndex: number
|
||||
}
|
||||
|
||||
function VideoFPS({ mediaIndex }: VideoFPSProps) {
|
||||
function FPS({ mediaIndex }: VideoFPSProps) {
|
||||
const {
|
||||
state: {
|
||||
media,
|
||||
|
|
@ -29,7 +29,7 @@ function VideoFPS({ mediaIndex }: VideoFPSProps) {
|
|||
media.length > 0 && mediaIndex >= 0 && media[mediaIndex].type === 'video'
|
||||
? media[mediaIndex]
|
||||
: null
|
||||
const { config, fps } = video ?? {}
|
||||
const { config, fps: videoFps } = video ?? {}
|
||||
const { shouldEnableCustomFPS, customFPS, convertToExtension } =
|
||||
config ?? commonConfigForBatchCompression.videoConfig ?? {}
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ function VideoFPS({ mediaIndex }: VideoFPSProps) {
|
|||
isProcessCompleted ||
|
||||
isLoadingMediaFiles
|
||||
|
||||
const initialFpsValue = customFPS ?? fps ?? 30
|
||||
const initialFpsValue = customFPS ?? videoFps ?? 30
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -135,7 +135,7 @@ function VideoFPS({ mediaIndex }: VideoFPSProps) {
|
|||
label: '!text-gray-600 dark:!text-gray-400 text-xs',
|
||||
}}
|
||||
>
|
||||
{FPS?.map((f) => (
|
||||
{fps?.map((f) => (
|
||||
<SelectItem
|
||||
key={String(f)}
|
||||
textValue={String(f)}
|
||||
|
|
@ -153,4 +153,4 @@ function VideoFPS({ mediaIndex }: VideoFPSProps) {
|
|||
)
|
||||
}
|
||||
|
||||
export default VideoFPS
|
||||
export default FPS
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
VideoStream,
|
||||
} from '@/types/compression'
|
||||
|
||||
export type VideoMetadataConfig = {
|
||||
export type MediaMetadataConfig = {
|
||||
title?: string | null
|
||||
artist?: string | null
|
||||
album?: string | null
|
||||
|
|
@ -73,8 +73,8 @@ export type VideoConfig = {
|
|||
shouldTrimVideo?: boolean
|
||||
trimConfig?: TimelineAction[]
|
||||
isVideoTrimEditMode?: boolean
|
||||
shouldPreserveMetadata?: boolean
|
||||
metadataConfig?: VideoMetadataConfig | null
|
||||
shouldStripMetadata?: boolean
|
||||
metadataConfig?: MediaMetadataConfig | null
|
||||
customThumbnailPath?: string | null
|
||||
shouldEnableCustomThumbnail?: boolean
|
||||
shouldEnableCustomChannel?: boolean
|
||||
|
|
@ -133,7 +133,8 @@ export type ImageConfig = {
|
|||
convertToExtension: keyof typeof extensions.image | '-'
|
||||
isLossless: boolean
|
||||
quality: number
|
||||
stripMetadata: boolean
|
||||
shouldStripMetadata: boolean
|
||||
metadataConfig?: MediaMetadataConfig | null
|
||||
svgScaleFactor?: number
|
||||
svgConfig?: SvgConfig
|
||||
shouldEnableAdvancedSvgSetting?: boolean
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AudioConfig, SubtitlesConfig, VideoMetadataConfig } from './app'
|
||||
import { AudioConfig, MediaMetadataConfig, SubtitlesConfig } from './app'
|
||||
import { FileMetadata } from './fs'
|
||||
|
||||
export const extensions = {
|
||||
|
|
@ -218,7 +218,8 @@ export type VideoCompressionConfig = {
|
|||
fps?: string | null
|
||||
videoCodec?: string | null
|
||||
transformHistory?: MediaTransformHistory[] | null
|
||||
metadataConfig?: VideoMetadataConfig | null
|
||||
stripMetadata?: boolean
|
||||
metadataConfig?: MediaMetadataConfig | null
|
||||
customThumbnailPath?: string | null
|
||||
trimSegments?: VideoTrimSegment[] | null
|
||||
subtitlesConfig?: SubtitlesConfig | null
|
||||
|
|
|
|||
Loading…
Reference in a new issue