@frkntmbs/strapi-plugin-video-optimizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/admin/custom.d.ts +8 -0
  4. package/admin/src/buildVersion.ts +3 -0
  5. package/admin/src/components/AssetOptimizationLabel.tsx +61 -0
  6. package/admin/src/components/BridgeProviders.tsx +123 -0
  7. package/admin/src/components/MediaLibraryCacheBridge.tsx +24 -0
  8. package/admin/src/components/MediaLibraryCardActionsBridge.tsx +249 -0
  9. package/admin/src/components/MediaLibraryJobWatcher.tsx +136 -0
  10. package/admin/src/components/MediaLibraryProgressBridge.tsx +97 -0
  11. package/admin/src/components/OptimizationChoicePicker.tsx +65 -0
  12. package/admin/src/components/OptimizationResizeFields.tsx +120 -0
  13. package/admin/src/components/OptimizationVideoFields.tsx +217 -0
  14. package/admin/src/components/UploadEnhancerBridge.tsx +205 -0
  15. package/admin/src/components/upload/PendingAssetStep.tsx +97 -0
  16. package/admin/src/defaultGlobalSettings.ts +32 -0
  17. package/admin/src/hooks/useDefaultOptimizationMode.ts +24 -0
  18. package/admin/src/hooks/useUploadWithOptimizer.ts +45 -0
  19. package/admin/src/index.ts +84 -0
  20. package/admin/src/pages/SettingsPage.tsx +208 -0
  21. package/admin/src/pluginId.ts +79 -0
  22. package/admin/src/translations/en.json +74 -0
  23. package/admin/src/translations/tr.json +74 -0
  24. package/admin/src/utils/adminFetch.ts +57 -0
  25. package/admin/src/utils/captureQueryClient.ts +34 -0
  26. package/admin/src/utils/debugMediaLibraryProgress.ts +70 -0
  27. package/admin/src/utils/extractAssetDimensions.ts +22 -0
  28. package/admin/src/utils/initJobPoller.ts +173 -0
  29. package/admin/src/utils/initMediaLibraryCardActions.ts +308 -0
  30. package/admin/src/utils/initMediaLibraryProgress.ts +219 -0
  31. package/admin/src/utils/initUploadEnhancer.ts +447 -0
  32. package/admin/src/utils/invalidateMediaLibrary.ts +203 -0
  33. package/admin/src/utils/jobProgressStore.ts +113 -0
  34. package/admin/src/utils/mediaLibraryCardMatch.ts +414 -0
  35. package/admin/src/utils/mediaLibraryCardStore.ts +223 -0
  36. package/admin/src/utils/mediaLibraryQueryBridge.ts +113 -0
  37. package/admin/src/utils/mediaLibraryRoute.ts +9 -0
  38. package/admin/src/utils/optimizationFields.ts +17 -0
  39. package/admin/src/utils/probeVideoDimensions.ts +94 -0
  40. package/admin/src/utils/uploadAssetStore.ts +670 -0
  41. package/admin/tsconfig.json +8 -0
  42. package/dist/admin/SettingsPage-CN2fR83m.js +150 -0
  43. package/dist/admin/SettingsPage-D6e536P0.mjs +150 -0
  44. package/dist/admin/en-CqM903j3.js +77 -0
  45. package/dist/admin/en-CsHicGzL.mjs +77 -0
  46. package/dist/admin/index-BjWoS0YU.js +2542 -0
  47. package/dist/admin/index-Cs_uiChW.mjs +2541 -0
  48. package/dist/admin/index-DOuHOS2G.js +8799 -0
  49. package/dist/admin/index-rAmxCQz6.mjs +8781 -0
  50. package/dist/admin/index.js +4 -0
  51. package/dist/admin/index.mjs +4 -0
  52. package/dist/admin/tr-Y0-ANilh.mjs +77 -0
  53. package/dist/admin/tr-muzHkdC4.js +77 -0
  54. package/dist/server/index.js +1538 -0
  55. package/dist/server/index.mjs +1533 -0
  56. package/package.json +100 -0
  57. package/server/index.js +1 -0
  58. package/server/src/bootstrap.ts +377 -0
  59. package/server/src/buildVersion.ts +1 -0
  60. package/server/src/config/defaults.ts +91 -0
  61. package/server/src/config/index.ts +51 -0
  62. package/server/src/constants.ts +83 -0
  63. package/server/src/controllers/index.ts +7 -0
  64. package/server/src/controllers/job.ts +102 -0
  65. package/server/src/controllers/preference.ts +206 -0
  66. package/server/src/index.ts +15 -0
  67. package/server/src/register.ts +19 -0
  68. package/server/src/routes/index.ts +103 -0
  69. package/server/src/services/index.ts +9 -0
  70. package/server/src/services/job-queue.ts +663 -0
  71. package/server/src/services/optimizer.ts +284 -0
  72. package/server/src/services/preference.ts +172 -0
  73. package/server/src/utils/request-context.ts +7 -0
  74. package/server/src/utils/upload-preferences-context.ts +202 -0
  75. package/server/tsconfig.json +8 -0
  76. package/strapi-admin.js +7 -0
  77. package/strapi-server.js +7 -0
@@ -0,0 +1,217 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Field, Grid, SingleSelect, SingleSelectOption, TextInput, Typography } from '@strapi/design-system';
3
+ import { useIntl } from 'react-intl';
4
+ import {
5
+ codecForFormat,
6
+ getTranslationKey,
7
+ type AudioMode,
8
+ type FfmpegPreset,
9
+ type OptimizationSettings,
10
+ type VideoFormat,
11
+ } from '../pluginId';
12
+ import { OptimizationResizeFields } from './OptimizationResizeFields';
13
+
14
+ interface OptimizationVideoFieldsProps {
15
+ value: Pick<
16
+ OptimizationSettings,
17
+ 'defaultFormat' | 'videoCodec' | 'crf' | 'preset' | 'audioMode' | 'audioBitrate'
18
+ >;
19
+ onChange: (patch: Partial<OptimizationSettings>) => void;
20
+ disabled?: boolean;
21
+ namePrefix?: string;
22
+ }
23
+
24
+ const FORMATS: VideoFormat[] = ['mp4', 'webm'];
25
+ const PRESETS: FfmpegPreset[] = [
26
+ 'ultrafast',
27
+ 'superfast',
28
+ 'veryfast',
29
+ 'faster',
30
+ 'fast',
31
+ 'medium',
32
+ 'slow',
33
+ 'slower',
34
+ 'veryslow',
35
+ ];
36
+ const AUDIO_MODES: AudioMode[] = ['keep', 'remove', 'compress'];
37
+
38
+ export const OptimizationVideoFields = ({
39
+ value,
40
+ onChange,
41
+ disabled = false,
42
+ namePrefix = '',
43
+ }: OptimizationVideoFieldsProps) => {
44
+ const { formatMessage } = useIntl();
45
+
46
+ const handleFormatChange = (format: VideoFormat) => {
47
+ onChange({
48
+ defaultFormat: format,
49
+ videoCodec: codecForFormat(format),
50
+ });
51
+ };
52
+
53
+ return (
54
+ <>
55
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
56
+ <Field.Root name={`${namePrefix}defaultFormat`}>
57
+ <Field.Label>
58
+ {formatMessage({ id: getTranslationKey('settings.global.defaultFormat') })}
59
+ </Field.Label>
60
+ <SingleSelect
61
+ value={value.defaultFormat}
62
+ onChange={handleFormatChange}
63
+ disabled={disabled}
64
+ >
65
+ {FORMATS.map((format) => (
66
+ <SingleSelectOption key={format} value={format}>
67
+ {formatMessage({ id: getTranslationKey(`settings.format.${format}`) })}
68
+ </SingleSelectOption>
69
+ ))}
70
+ </SingleSelect>
71
+ </Field.Root>
72
+ </Grid.Item>
73
+
74
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
75
+ <Field.Root name={`${namePrefix}crf`}>
76
+ <Field.Label>
77
+ {formatMessage({ id: getTranslationKey('settings.global.crf') })}
78
+ </Field.Label>
79
+ <TextInput
80
+ type="number"
81
+ min={0}
82
+ max={51}
83
+ value={value.crf}
84
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
85
+ onChange({ crf: Number(e.target.value) })
86
+ }
87
+ disabled={disabled}
88
+ />
89
+ <Field.Hint>
90
+ {formatMessage({ id: getTranslationKey('settings.global.crfHint') })}
91
+ </Field.Hint>
92
+ </Field.Root>
93
+ </Grid.Item>
94
+
95
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
96
+ <Field.Root name={`${namePrefix}preset`}>
97
+ <Field.Label>
98
+ {formatMessage({ id: getTranslationKey('settings.global.preset') })}
99
+ </Field.Label>
100
+ <SingleSelect
101
+ value={value.preset}
102
+ onChange={(preset: FfmpegPreset) => onChange({ preset })}
103
+ disabled={disabled}
104
+ >
105
+ {PRESETS.map((preset) => (
106
+ <SingleSelectOption key={preset} value={preset}>
107
+ {preset}
108
+ </SingleSelectOption>
109
+ ))}
110
+ </SingleSelect>
111
+ </Field.Root>
112
+ </Grid.Item>
113
+
114
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
115
+ <Field.Root name={`${namePrefix}audioMode`}>
116
+ <Field.Label>
117
+ {formatMessage({ id: getTranslationKey('settings.global.audioMode') })}
118
+ </Field.Label>
119
+ <SingleSelect
120
+ value={value.audioMode}
121
+ onChange={(audioMode: AudioMode) => onChange({ audioMode })}
122
+ disabled={disabled}
123
+ >
124
+ {AUDIO_MODES.map((mode) => (
125
+ <SingleSelectOption key={mode} value={mode}>
126
+ {formatMessage({ id: getTranslationKey(`settings.audioMode.${mode}`) })}
127
+ </SingleSelectOption>
128
+ ))}
129
+ </SingleSelect>
130
+ </Field.Root>
131
+ </Grid.Item>
132
+
133
+ {value.audioMode === 'compress' && (
134
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
135
+ <Field.Root name={`${namePrefix}audioBitrate`}>
136
+ <Field.Label>
137
+ {formatMessage({ id: getTranslationKey('settings.global.audioBitrate') })}
138
+ </Field.Label>
139
+ <TextInput
140
+ value={value.audioBitrate}
141
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
142
+ onChange({ audioBitrate: e.target.value })
143
+ }
144
+ disabled={disabled}
145
+ />
146
+ </Field.Root>
147
+ </Grid.Item>
148
+ )}
149
+
150
+ <Grid.Item col={12} direction="column" alignItems="stretch">
151
+ <Typography variant="pi" textColor="neutral600">
152
+ {formatMessage({
153
+ id: getTranslationKey('settings.global.codecHint'),
154
+ defaultMessage: 'Codec is selected automatically based on the output format.',
155
+ })}{' '}
156
+ ({value.videoCodec.toUpperCase()})
157
+ </Typography>
158
+ </Grid.Item>
159
+ </>
160
+ );
161
+ };
162
+
163
+ interface OptimizationCustomFormProps {
164
+ value: OptimizationSettings;
165
+ onChange: (value: OptimizationSettings) => void;
166
+ sourceWidth?: number;
167
+ sourceHeight?: number;
168
+ disabled?: boolean;
169
+ }
170
+
171
+ export const OptimizationCustomForm = ({
172
+ value,
173
+ onChange,
174
+ sourceWidth,
175
+ sourceHeight,
176
+ disabled = false,
177
+ }: OptimizationCustomFormProps) => {
178
+ const update = (patch: Partial<OptimizationSettings>) => {
179
+ onChange({ ...value, ...patch });
180
+ };
181
+
182
+ useEffect(() => {
183
+ if (!sourceWidth || !sourceHeight) {
184
+ return;
185
+ }
186
+
187
+ if (value.maxWidth > 0 && value.maxHeight > 0) {
188
+ return;
189
+ }
190
+
191
+ onChange({
192
+ ...value,
193
+ maxWidth: sourceWidth,
194
+ maxHeight: sourceHeight,
195
+ });
196
+ }, [sourceWidth, sourceHeight]);
197
+
198
+ return (
199
+ <Grid.Root gap={4}>
200
+ <OptimizationVideoFields
201
+ value={value}
202
+ onChange={update}
203
+ disabled={disabled}
204
+ namePrefix="custom"
205
+ />
206
+
207
+ <OptimizationResizeFields
208
+ value={value}
209
+ sourceWidth={sourceWidth}
210
+ sourceHeight={sourceHeight}
211
+ onChange={update}
212
+ disabled={disabled}
213
+ namePrefix="custom"
214
+ />
215
+ </Grid.Root>
216
+ );
217
+ };
@@ -0,0 +1,205 @@
1
+ import React, { useEffect, useSyncExternalStore, type MouseEvent, type PointerEvent } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import {
4
+ Box,
5
+ Button,
6
+ Flex,
7
+ IconButton,
8
+ Typography,
9
+ } from '@strapi/design-system';
10
+ import { Cross, Sparkle } from '@strapi/icons';
11
+ import { useIntl } from 'react-intl';
12
+ import { AssetOptimizationLabel } from './AssetOptimizationLabel';
13
+ import { OptimizationChoicePicker } from './OptimizationChoicePicker';
14
+ import { OptimizationCustomForm } from './OptimizationVideoFields';
15
+ import { getTranslationKey } from '../pluginId';
16
+ import {
17
+ closeAssetEditor,
18
+ createCustomForAsset,
19
+ getAssetPreference,
20
+ getDraftPreference,
21
+ getEditingAssetId,
22
+ getSourceDimensionsForAsset,
23
+ getUploadAssetCards,
24
+ getUploadDialogElement,
25
+ openAssetEditor,
26
+ resolveCustomSettingsForAsset,
27
+ saveAssetEditor,
28
+ setDraftChoice,
29
+ setDraftCustom,
30
+ subscribeUploadAssets,
31
+ } from '../utils/uploadAssetStore';
32
+
33
+ const stopEventPropagation = (event: MouseEvent | PointerEvent) => {
34
+ event.stopPropagation();
35
+ };
36
+
37
+ export const UploadEnhancerBridge = () => {
38
+ const { formatMessage } = useIntl();
39
+ const cards = useSyncExternalStore(subscribeUploadAssets, getUploadAssetCards);
40
+ const editingAssetId = useSyncExternalStore(subscribeUploadAssets, getEditingAssetId);
41
+ const draftPreference = useSyncExternalStore(subscribeUploadAssets, getDraftPreference);
42
+ const dialogElement = useSyncExternalStore(subscribeUploadAssets, getUploadDialogElement);
43
+
44
+ const editingCard = cards.find((card) => card.assetId === editingAssetId);
45
+ const sourceDimensions = editingAssetId ? getSourceDimensionsForAsset(editingAssetId) : undefined;
46
+ const resolvedCustom =
47
+ editingAssetId && draftPreference.choice === 'custom'
48
+ ? resolveCustomSettingsForAsset(editingAssetId, draftPreference.custom)
49
+ : draftPreference.custom;
50
+
51
+ useEffect(() => {
52
+ if (!dialogElement || !editingAssetId) {
53
+ return;
54
+ }
55
+
56
+ const previousOverflow = dialogElement.style.overflow;
57
+ dialogElement.style.overflow = 'visible';
58
+
59
+ return () => {
60
+ dialogElement.style.overflow = previousOverflow;
61
+ };
62
+ }, [dialogElement, editingAssetId]);
63
+
64
+ const editorPanel =
65
+ editingAssetId && dialogElement ? (
66
+ <Box
67
+ position="fixed"
68
+ top={0}
69
+ left={0}
70
+ right={0}
71
+ bottom={0}
72
+ style={{ zIndex: 10, pointerEvents: 'auto' }}
73
+ onPointerDown={stopEventPropagation}
74
+ onMouseDown={stopEventPropagation}
75
+ onClick={stopEventPropagation}
76
+ >
77
+ <Box
78
+ position="absolute"
79
+ top={0}
80
+ left={0}
81
+ right={0}
82
+ bottom={0}
83
+ background="neutral800"
84
+ style={{ opacity: 0.2 }}
85
+ onClick={closeAssetEditor}
86
+ />
87
+
88
+ <Flex
89
+ direction="column"
90
+ alignItems="stretch"
91
+ background="neutral0"
92
+ hasRadius
93
+ shadow="popupShadow"
94
+ style={{
95
+ position: 'fixed',
96
+ top: '50%',
97
+ left: '50%',
98
+ transform: 'translate(-50%, -50%)',
99
+ width: 'min(520px, calc(100% - 32px))',
100
+ maxHeight: 'min(90vh, 640px)',
101
+ overflow: 'hidden',
102
+ zIndex: 11,
103
+ }}
104
+ >
105
+ <Flex
106
+ tag="header"
107
+ padding={4}
108
+ paddingLeft={5}
109
+ paddingRight={5}
110
+ background="neutral100"
111
+ justifyContent="space-between"
112
+ alignItems="center"
113
+ borderColor="neutral150"
114
+ borderWidth="0 0 1px"
115
+ borderStyle="solid"
116
+ >
117
+ <Typography variant="omega" fontWeight="bold" textColor="neutral800">
118
+ {formatMessage({ id: getTranslationKey('upload.modal.title') })}
119
+ </Typography>
120
+ <IconButton
121
+ label={formatMessage({ id: getTranslationKey('upload.modal.cancel') })}
122
+ onClick={closeAssetEditor}
123
+ >
124
+ <Cross />
125
+ </IconButton>
126
+ </Flex>
127
+
128
+ <Box padding={7} style={{ overflow: 'auto' }}>
129
+ <Flex direction="column" alignItems="stretch" gap={5}>
130
+ {editingCard?.assetName && (
131
+ <Typography variant="pi" textColor="neutral600">
132
+ {editingCard.assetName}
133
+ </Typography>
134
+ )}
135
+ <OptimizationChoicePicker
136
+ value={draftPreference.choice}
137
+ onChange={setDraftChoice}
138
+ />
139
+ {draftPreference.choice === 'custom' && (
140
+ <Box background="neutral100" padding={5} hasRadius>
141
+ <OptimizationCustomForm
142
+ value={resolvedCustom ?? createCustomForAsset(editingAssetId)}
143
+ onChange={setDraftCustom}
144
+ sourceWidth={editingCard?.width ?? sourceDimensions?.width}
145
+ sourceHeight={editingCard?.height ?? sourceDimensions?.height}
146
+ />
147
+ </Box>
148
+ )}
149
+ </Flex>
150
+ </Box>
151
+
152
+ <Flex
153
+ tag="footer"
154
+ gap={2}
155
+ justifyContent="flex-end"
156
+ padding={4}
157
+ paddingLeft={5}
158
+ paddingRight={5}
159
+ background="neutral100"
160
+ borderColor="neutral150"
161
+ borderWidth="1px 0 0"
162
+ borderStyle="solid"
163
+ >
164
+ <Button onClick={closeAssetEditor} variant="tertiary">
165
+ {formatMessage({ id: getTranslationKey('upload.modal.cancel') })}
166
+ </Button>
167
+ <Button onClick={saveAssetEditor}>
168
+ {formatMessage({ id: getTranslationKey('upload.modal.save') })}
169
+ </Button>
170
+ </Flex>
171
+ </Flex>
172
+ </Box>
173
+ ) : null;
174
+
175
+ return (
176
+ <>
177
+ {cards.map((card) => (
178
+ <React.Fragment key={card.assetId}>
179
+ {createPortal(
180
+ <IconButton
181
+ label={formatMessage({ id: getTranslationKey('upload.button.label') })}
182
+ onClick={(event) => {
183
+ event.preventDefault();
184
+ event.stopPropagation();
185
+ openAssetEditor(card.assetId);
186
+ }}
187
+ >
188
+ <Sparkle />
189
+ </IconButton>,
190
+ card.actionsContainer
191
+ )}
192
+
193
+ {card.footerHost
194
+ ? createPortal(
195
+ <AssetOptimizationLabel preference={getAssetPreference(card.assetId)} />,
196
+ card.footerHost
197
+ )
198
+ : null}
199
+ </React.Fragment>
200
+ ))}
201
+
202
+ {editorPanel ? createPortal(editorPanel, dialogElement) : null}
203
+ </>
204
+ );
205
+ };
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { Box, Flex, Typography } from '@strapi/design-system';
3
+ import { Sparkle } from '@strapi/icons';
4
+ import { useIntl } from 'react-intl';
5
+ import { AssetOptimizationLabel } from '../AssetOptimizationLabel';
6
+ import { OptimizationChoicePicker } from '../OptimizationChoicePicker';
7
+ import { OptimizationCustomForm } from '../OptimizationVideoFields';
8
+ import {
9
+ createCustomForAsset,
10
+ getAssetPreference,
11
+ getSourceDimensionsForAsset,
12
+ resolveCustomSettingsForAsset,
13
+ type UploadAssetEntry,
14
+ } from '../../utils/uploadAssetStore';
15
+ import { getTranslationKey, type AssetOptimizationPreference } from '../../pluginId';
16
+
17
+ interface PendingAssetStepProps {
18
+ asset: UploadAssetEntry;
19
+ preference: AssetOptimizationPreference;
20
+ onPreferenceChange: (preference: AssetOptimizationPreference) => void;
21
+ }
22
+
23
+ export const PendingAssetStep = ({
24
+ asset,
25
+ preference,
26
+ onPreferenceChange,
27
+ }: PendingAssetStepProps) => {
28
+ const { formatMessage } = useIntl();
29
+
30
+ const sourceDimensions = getSourceDimensionsForAsset(asset.assetId);
31
+ const resolvedCustom = resolveCustomSettingsForAsset(asset.assetId, preference.custom);
32
+
33
+ const handleChoiceChange = (choice: AssetOptimizationPreference['choice']) => {
34
+ onPreferenceChange({
35
+ choice,
36
+ custom:
37
+ choice === 'custom'
38
+ ? resolveCustomSettingsForAsset(asset.assetId, preference.custom)
39
+ : undefined,
40
+ });
41
+ };
42
+
43
+ return (
44
+ <Box padding={4} background="neutral100" hasRadius>
45
+ <Flex direction="column" gap={4} alignItems="stretch">
46
+ <Flex alignItems="center" gap={2}>
47
+ <Sparkle />
48
+ <Typography variant="omega" fontWeight="bold">
49
+ {formatMessage({ id: getTranslationKey('upload.modal.title') })}
50
+ </Typography>
51
+ </Flex>
52
+
53
+ <Typography variant="pi" textColor="neutral600">
54
+ {asset.assetName}
55
+ </Typography>
56
+
57
+ <OptimizationChoicePicker value={preference.choice} onChange={handleChoiceChange} />
58
+
59
+ {preference.choice === 'custom' && (
60
+ <OptimizationCustomForm
61
+ value={resolvedCustom}
62
+ onChange={(custom) => onPreferenceChange({ choice: 'custom', custom })}
63
+ sourceWidth={asset.width ?? sourceDimensions?.width}
64
+ sourceHeight={asset.height ?? sourceDimensions?.height}
65
+ />
66
+ )}
67
+
68
+ <AssetOptimizationLabel preference={getAssetPreference(asset.assetId)} />
69
+ </Flex>
70
+ </Box>
71
+ );
72
+ };
73
+
74
+ export const createPendingPreference = (assetId: string): AssetOptimizationPreference => ({
75
+ choice: 'original',
76
+ custom: createCustomForAsset(assetId),
77
+ });
78
+
79
+ export const mergePendingPreference = (
80
+ assetId: string,
81
+ preference?: AssetOptimizationPreference
82
+ ): AssetOptimizationPreference => {
83
+ if (!preference) {
84
+ return { choice: 'original' };
85
+ }
86
+
87
+ if (preference.choice === 'custom') {
88
+ return {
89
+ choice: 'custom',
90
+ custom: preference.custom ?? createCustomForAsset(assetId),
91
+ };
92
+ }
93
+
94
+ return {
95
+ choice: preference.choice,
96
+ };
97
+ };
@@ -0,0 +1,32 @@
1
+ import {
2
+ clampMaxConcurrentJobs,
3
+ clampMaxFfmpegThreads,
4
+ type GlobalOptimizationSettings,
5
+ } from './pluginId';
6
+
7
+ export const DEFAULT_GLOBAL_SETTINGS: GlobalOptimizationSettings = {
8
+ defaultChoice: 'original',
9
+ defaultFormat: 'mp4',
10
+ videoCodec: 'h264',
11
+ crf: 23,
12
+ preset: 'medium',
13
+ maxWidth: 1920,
14
+ maxHeight: 1080,
15
+ audioMode: 'compress',
16
+ audioBitrate: '128k',
17
+ maxConcurrentJobs: 1,
18
+ maxFfmpegThreads: 2,
19
+ };
20
+
21
+ export const mergeGlobalSettings = (
22
+ partial?: Partial<GlobalOptimizationSettings> | null
23
+ ): GlobalOptimizationSettings => ({
24
+ ...DEFAULT_GLOBAL_SETTINGS,
25
+ ...partial,
26
+ maxConcurrentJobs: clampMaxConcurrentJobs(
27
+ partial?.maxConcurrentJobs ?? DEFAULT_GLOBAL_SETTINGS.maxConcurrentJobs
28
+ ),
29
+ maxFfmpegThreads: clampMaxFfmpegThreads(
30
+ partial?.maxFfmpegThreads ?? DEFAULT_GLOBAL_SETTINGS.maxFfmpegThreads
31
+ ),
32
+ });
@@ -0,0 +1,24 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useFetchClient } from '@strapi/strapi/admin';
3
+ import { PLUGIN_ID, type GlobalOptimizationSettings } from '../pluginId';
4
+
5
+ export const useDefaultOptimizationMode = () => {
6
+ const { get } = useFetchClient();
7
+ const [settings, setSettings] = useState<GlobalOptimizationSettings | null>(null);
8
+ const [isLoading, setIsLoading] = useState(true);
9
+
10
+ useEffect(() => {
11
+ const load = async () => {
12
+ try {
13
+ const { data } = await get(`/${PLUGIN_ID}/default-mode`);
14
+ setSettings(data);
15
+ } finally {
16
+ setIsLoading(false);
17
+ }
18
+ };
19
+
20
+ void load();
21
+ }, [get]);
22
+
23
+ return { settings, isLoading };
24
+ };
@@ -0,0 +1,45 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import { useFetchClient } from '@strapi/strapi/admin';
3
+ import type { AssetOptimizationPreference } from '../pluginId';
4
+
5
+ interface UploadFilePayload {
6
+ file: File;
7
+ fileInfo?: Record<string, unknown>;
8
+ preference?: AssetOptimizationPreference;
9
+ }
10
+
11
+ export const useUploadWithOptimizer = () => {
12
+ const { post } = useFetchClient();
13
+
14
+ return useMutation({
15
+ mutationFn: async ({ file, fileInfo = {}, preference }: UploadFilePayload) => {
16
+ const formData = new FormData();
17
+ formData.append('files', file);
18
+
19
+ const payload = {
20
+ ...fileInfo,
21
+ optimizationChoice: preference?.choice ?? 'original',
22
+ ...(preference?.choice === 'custom' && preference.custom
23
+ ? { optimizationCustom: preference.custom }
24
+ : {}),
25
+ };
26
+
27
+ formData.append('fileInfo', JSON.stringify(payload));
28
+
29
+ if (preference) {
30
+ formData.append(
31
+ 'videoOptimizerPreferences',
32
+ JSON.stringify([
33
+ {
34
+ fileName: file.name,
35
+ preference,
36
+ },
37
+ ])
38
+ );
39
+ }
40
+
41
+ const { data } = await post('/upload', formData);
42
+ return data;
43
+ },
44
+ });
45
+ };