@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,84 @@
1
+ import { getTranslationKey, PLUGIN_ID } from './pluginId';
2
+ import { MediaLibraryCacheBridge } from './components/MediaLibraryCacheBridge';
3
+ import {
4
+ registerMediaLibraryDispatch,
5
+ } from './utils/invalidateMediaLibrary';
6
+ import { initUploadEnhancer } from './utils/initUploadEnhancer';
7
+ import { initMediaLibraryProgress } from './utils/initMediaLibraryProgress';
8
+ import { initMediaLibraryCardActions } from './utils/initMediaLibraryCardActions';
9
+ import { initJobPoller } from './utils/initJobPoller';
10
+ import { installQueryClientCapture } from './utils/captureQueryClient';
11
+ import { installDebugMediaLibraryProgress } from './utils/debugMediaLibraryProgress';
12
+ import { patchUploadFetch, patchUploadXHR } from './utils/uploadAssetStore';
13
+
14
+ const prefixPluginTranslations = (
15
+ trad: Record<string, string>,
16
+ pluginId: string
17
+ ): Record<string, string> => {
18
+ return Object.entries(trad).reduce<Record<string, string>>((acc, [key, value]) => {
19
+ acc[`${pluginId}.${key}`] = value;
20
+ return acc;
21
+ }, {});
22
+ };
23
+
24
+ export default {
25
+ register(app) {
26
+ app.addSettingsLink('global', {
27
+ id: 'video-optimizer',
28
+ to: 'video-optimizer',
29
+ intlLabel: {
30
+ id: getTranslationKey('settings.section-label'),
31
+ defaultMessage: 'Video Optimizer',
32
+ },
33
+ Component: () =>
34
+ import('./pages/SettingsPage').then((mod) => ({
35
+ default: mod.ProtectedSettingsPage,
36
+ })),
37
+ permissions: [],
38
+ });
39
+
40
+ app.addComponents([
41
+ {
42
+ name: 'future-global::video-optimizer-cache',
43
+ Component: MediaLibraryCacheBridge,
44
+ },
45
+ ]);
46
+
47
+ app.addMiddlewares([
48
+ () => (api) => {
49
+ registerMediaLibraryDispatch(api.dispatch);
50
+
51
+ return (next) => (action) => next(action);
52
+ },
53
+ ]);
54
+ },
55
+
56
+ bootstrap() {
57
+ patchUploadFetch();
58
+ patchUploadXHR();
59
+ installQueryClientCapture();
60
+ initUploadEnhancer();
61
+ initMediaLibraryProgress();
62
+ initMediaLibraryCardActions();
63
+ initJobPoller();
64
+ installDebugMediaLibraryProgress();
65
+ },
66
+
67
+ async registerTrads({ locales }: { locales: string[] }) {
68
+ const importedTrads = await Promise.all(
69
+ locales.map(async (locale) => {
70
+ try {
71
+ const { default: data } = await import(`./translations/${locale}.json`);
72
+ return {
73
+ data: prefixPluginTranslations(data, PLUGIN_ID),
74
+ locale,
75
+ };
76
+ } catch {
77
+ return { data: {}, locale };
78
+ }
79
+ })
80
+ );
81
+
82
+ return importedTrads;
83
+ },
84
+ };
@@ -0,0 +1,208 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Box, Button, Field, Flex, Grid, SingleSelect, SingleSelectOption, TextInput, Typography } from "@strapi/design-system";
3
+ import { Check } from "@strapi/icons";
4
+ import { Layouts, Page, useFetchClient, useNotification, useRBAC } from "@strapi/strapi/admin";
5
+ import { useIntl } from "react-intl";
6
+ import { OptimizationResizeFields } from "../components/OptimizationResizeFields";
7
+ import { OptimizationVideoFields } from "../components/OptimizationVideoFields";
8
+ import { DEFAULT_GLOBAL_SETTINGS, mergeGlobalSettings } from "../defaultGlobalSettings";
9
+ import { getTranslationKey, PLUGIN_ID, MAX_CONCURRENT_JOBS_LIMIT, MAX_FFMPEG_THREADS_LIMIT, clampMaxConcurrentJobs, clampMaxFfmpegThreads, type GlobalOptimizationSettings, type OptimizationChoice } from "../pluginId";
10
+
11
+ const SETTINGS_READ = [{ action: "plugin::video-optimizer.settings.read", subject: null }];
12
+ const SETTINGS_UPDATE = [{ action: "plugin::video-optimizer.settings.update", subject: null }];
13
+
14
+ const CHOICES: OptimizationChoice[] = ["original", "global", "custom"];
15
+
16
+ export const SettingsPage = () => {
17
+ const { formatMessage } = useIntl();
18
+ const { get, put } = useFetchClient();
19
+ const { toggleNotification } = useNotification();
20
+ const { allowedActions: readActions } = useRBAC(SETTINGS_READ);
21
+ const { allowedActions: updateActions } = useRBAC(SETTINGS_UPDATE);
22
+
23
+ const canReadGlobal = readActions.canRead;
24
+ const canUpdateGlobal = updateActions.canUpdate;
25
+
26
+ const [globalSettings, setGlobalSettings] = useState<GlobalOptimizationSettings>(DEFAULT_GLOBAL_SETTINGS);
27
+ const [isLoading, setIsLoading] = useState(true);
28
+ const [isSaving, setIsSaving] = useState(false);
29
+
30
+ useEffect(() => {
31
+ const load = async () => {
32
+ if (!canReadGlobal) {
33
+ setIsLoading(false);
34
+ return;
35
+ }
36
+
37
+ try {
38
+ const { data: settings } = await get(`/${PLUGIN_ID}/settings`);
39
+ setGlobalSettings(mergeGlobalSettings(settings));
40
+ } catch {
41
+ toggleNotification({
42
+ type: "danger",
43
+ message: formatMessage({ id: getTranslationKey("settings.error") }),
44
+ });
45
+ } finally {
46
+ setIsLoading(false);
47
+ }
48
+ };
49
+
50
+ load();
51
+ }, [canReadGlobal, formatMessage, get, toggleNotification]);
52
+
53
+ const handleSave = async () => {
54
+ if (!canUpdateGlobal) {
55
+ return;
56
+ }
57
+
58
+ setIsSaving(true);
59
+
60
+ try {
61
+ await put(`/${PLUGIN_ID}/settings`, globalSettings);
62
+
63
+ toggleNotification({
64
+ type: "success",
65
+ message: formatMessage({ id: getTranslationKey("settings.saved") }),
66
+ });
67
+ } catch {
68
+ toggleNotification({
69
+ type: "danger",
70
+ message: formatMessage({ id: getTranslationKey("settings.error") }),
71
+ });
72
+ } finally {
73
+ setIsSaving(false);
74
+ }
75
+ };
76
+
77
+ if (isLoading) {
78
+ return <Page.Loading />;
79
+ }
80
+
81
+ if (!canReadGlobal) {
82
+ return (
83
+ <Page.Main>
84
+ <Page.Title>{formatMessage({ id: getTranslationKey("settings.page.title") })}</Page.Title>
85
+ <Layouts.Header title={formatMessage({ id: getTranslationKey("settings.page.title") })} subtitle={formatMessage({ id: getTranslationKey("settings.page.description") })} />
86
+ <Layouts.Content>
87
+ <Typography textColor="neutral600">{formatMessage({ id: getTranslationKey("settings.global.noPermission") })}</Typography>
88
+ </Layouts.Content>
89
+ </Page.Main>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <Page.Main>
95
+ <Page.Title>{formatMessage({ id: getTranslationKey("settings.page.title") })}</Page.Title>
96
+ <Layouts.Header
97
+ title={formatMessage({ id: getTranslationKey("settings.page.title") })}
98
+ subtitle={formatMessage({ id: getTranslationKey("settings.page.description") })}
99
+ primaryAction={
100
+ canUpdateGlobal ? (
101
+ <Button onClick={handleSave} loading={isSaving} startIcon={<Check />} size="S">
102
+ {formatMessage({ id: getTranslationKey("settings.save") })}
103
+ </Button>
104
+ ) : undefined
105
+ }
106
+ />
107
+ <Layouts.Content>
108
+ <Layouts.Root>
109
+ <Flex direction="column" alignItems="stretch" gap={6}>
110
+ <Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
111
+ <Typography variant="delta" tag="h2">
112
+ {formatMessage({ id: getTranslationKey("settings.global.defaultChoiceTitle") })}
113
+ </Typography>
114
+ <Box paddingTop={2} paddingBottom={4}>
115
+ <Typography textColor="neutral600">{formatMessage({ id: getTranslationKey("settings.global.defaultChoiceDescription") })}</Typography>
116
+ </Box>
117
+
118
+ <Grid.Root gap={6}>
119
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
120
+ <Field.Root name="defaultChoice">
121
+ <Field.Label>{formatMessage({ id: getTranslationKey("settings.global.defaultChoice") })}</Field.Label>
122
+ <SingleSelect value={globalSettings.defaultChoice} onChange={(value: OptimizationChoice) => setGlobalSettings((prev) => ({ ...prev, defaultChoice: value }))} disabled={!canUpdateGlobal || isSaving}>
123
+ {CHOICES.map((choice) => (
124
+ <SingleSelectOption key={choice} value={choice}>
125
+ {formatMessage({ id: getTranslationKey(`choice.${choice}`) })}
126
+ </SingleSelectOption>
127
+ ))}
128
+ </SingleSelect>
129
+ <Field.Hint>{formatMessage({ id: getTranslationKey("settings.global.defaultChoiceHint") })}</Field.Hint>
130
+ </Field.Root>
131
+ </Grid.Item>
132
+ </Grid.Root>
133
+ </Box>
134
+
135
+ <Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
136
+ <Typography variant="delta" tag="h2">
137
+ {formatMessage({ id: getTranslationKey("settings.global.profileTitle") })}
138
+ </Typography>
139
+ <Box paddingTop={2} paddingBottom={4}>
140
+ <Typography textColor="neutral600">{formatMessage({ id: getTranslationKey("settings.global.profileDescription") })}</Typography>
141
+ </Box>
142
+
143
+ <Grid.Root gap={6}>
144
+ <OptimizationVideoFields value={globalSettings} onChange={(patch) => setGlobalSettings((prev) => ({ ...prev, ...patch }))} disabled={!canUpdateGlobal || isSaving} namePrefix="global" />
145
+
146
+ <OptimizationResizeFields value={globalSettings} onChange={(patch) => setGlobalSettings((prev) => ({ ...prev, ...patch }))} disabled={!canUpdateGlobal || isSaving} namePrefix="global" variant="global" />
147
+ </Grid.Root>
148
+ </Box>
149
+
150
+ <Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
151
+ <Typography variant="delta" tag="h2">
152
+ {formatMessage({ id: getTranslationKey("settings.global.concurrencyTitle") })}
153
+ </Typography>
154
+ <Box paddingTop={2} paddingBottom={4}>
155
+ <Typography textColor="neutral600">{formatMessage({ id: getTranslationKey("settings.global.concurrencyDescription") })}</Typography>
156
+ </Box>
157
+
158
+ <Grid.Root gap={6}>
159
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
160
+ <Field.Root name="maxConcurrentJobs">
161
+ <Field.Label>{formatMessage({ id: getTranslationKey("settings.global.maxConcurrentJobs") })}</Field.Label>
162
+ <TextInput
163
+ type="number"
164
+ min={1}
165
+ max={MAX_CONCURRENT_JOBS_LIMIT}
166
+ value={String(globalSettings.maxConcurrentJobs)}
167
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
168
+ setGlobalSettings((prev) => ({
169
+ ...prev,
170
+ maxConcurrentJobs: clampMaxConcurrentJobs(Number(event.target.value) || 1),
171
+ }))
172
+ }
173
+ disabled={!canUpdateGlobal || isSaving}
174
+ />
175
+ <Field.Hint>{formatMessage({ id: getTranslationKey("settings.global.maxConcurrentJobsHint") })}</Field.Hint>
176
+ </Field.Root>
177
+ </Grid.Item>
178
+ <Grid.Item col={6} xs={12} direction="column" alignItems="stretch">
179
+ <Field.Root name="maxFfmpegThreads">
180
+ <Field.Label>{formatMessage({ id: getTranslationKey("settings.global.maxFfmpegThreads") })}</Field.Label>
181
+ <TextInput
182
+ type="number"
183
+ min={1}
184
+ max={MAX_FFMPEG_THREADS_LIMIT}
185
+ value={String(globalSettings.maxFfmpegThreads)}
186
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
187
+ setGlobalSettings((prev) => ({
188
+ ...prev,
189
+ maxFfmpegThreads: clampMaxFfmpegThreads(Number(event.target.value) || 1),
190
+ }))
191
+ }
192
+ disabled={!canUpdateGlobal || isSaving}
193
+ />
194
+ <Field.Hint>{formatMessage({ id: getTranslationKey("settings.global.maxFfmpegThreadsHint") })}</Field.Hint>
195
+ </Field.Root>
196
+ </Grid.Item>
197
+ </Grid.Root>
198
+ </Box>
199
+ </Flex>
200
+ </Layouts.Root>
201
+ </Layouts.Content>
202
+ </Page.Main>
203
+ );
204
+ };
205
+
206
+ export const ProtectedSettingsPage = () => {
207
+ return <SettingsPage />;
208
+ };
@@ -0,0 +1,79 @@
1
+ export const PLUGIN_ID = 'video-optimizer';
2
+
3
+ export type OptimizationChoice = 'original' | 'global' | 'custom';
4
+ export type VideoFormat = 'mp4' | 'webm';
5
+ export type VideoCodec = 'h264' | 'vp9';
6
+ export type AudioMode = 'keep' | 'remove' | 'compress';
7
+ export type FfmpegPreset =
8
+ | 'ultrafast'
9
+ | 'superfast'
10
+ | 'veryfast'
11
+ | 'faster'
12
+ | 'fast'
13
+ | 'medium'
14
+ | 'slow'
15
+ | 'slower'
16
+ | 'veryslow';
17
+
18
+ export interface OptimizationSettings {
19
+ defaultFormat: VideoFormat;
20
+ videoCodec: VideoCodec;
21
+ crf: number;
22
+ preset: FfmpegPreset;
23
+ maxWidth: number;
24
+ maxHeight: number;
25
+ audioMode: AudioMode;
26
+ audioBitrate: string;
27
+ }
28
+
29
+ export interface AssetOptimizationPreference {
30
+ choice: OptimizationChoice;
31
+ custom?: OptimizationSettings;
32
+ }
33
+
34
+ export interface GlobalOptimizationSettings {
35
+ defaultChoice: OptimizationChoice;
36
+ defaultFormat: VideoFormat;
37
+ videoCodec: VideoCodec;
38
+ crf: number;
39
+ preset: FfmpegPreset;
40
+ maxWidth: number;
41
+ maxHeight: number;
42
+ audioMode: AudioMode;
43
+ audioBitrate: string;
44
+ maxConcurrentJobs: number;
45
+ maxFfmpegThreads: number;
46
+ }
47
+
48
+ export type JobStatus = 'queued' | 'processing' | 'completed' | 'failed';
49
+
50
+ export interface VideoOptimizerJob {
51
+ id: string;
52
+ fileId: number;
53
+ fileName?: string;
54
+ fileHash?: string;
55
+ status: JobStatus;
56
+ stage: string;
57
+ progress: number;
58
+ settings?: OptimizationSettings;
59
+ error?: string;
60
+ createdAt: string;
61
+ updatedAt: string;
62
+ }
63
+
64
+ export const getTranslationKey = (key: string) => `${PLUGIN_ID}.${key}`;
65
+
66
+ export const MAX_CONCURRENT_JOBS_LIMIT = 32;
67
+ export const MAX_FFMPEG_THREADS_LIMIT = 8;
68
+
69
+ export const clampMaxConcurrentJobs = (value: number) =>
70
+ Math.min(MAX_CONCURRENT_JOBS_LIMIT, Math.max(1, Math.round(value)));
71
+
72
+ export const clampMaxFfmpegThreads = (value: number) =>
73
+ Math.min(MAX_FFMPEG_THREADS_LIMIT, Math.max(1, Math.round(value)));
74
+
75
+ export const codecForFormat = (format: VideoFormat): VideoCodec =>
76
+ format === 'webm' ? 'vp9' : 'h264';
77
+
78
+ export const isVideoFileName = (name: string) =>
79
+ /\.(mp4|webm|mov|avi|mkv|m4v|ogv|wmv|flv|3gp)$/i.test(name);
@@ -0,0 +1,74 @@
1
+ {
2
+ "plugin.name": "Video Optimizer",
3
+ "settings.section-label": "Video Optimizer",
4
+ "settings.page.title": "Video Optimizer Settings",
5
+ "settings.page.description": "Configure global defaults used when uploading videos. Each video can be overridden in the upload dialog.",
6
+ "settings.format.mp4": "MP4 (H.264)",
7
+ "settings.format.webm": "WebM (VP9)",
8
+ "settings.audioMode.keep": "Keep original audio",
9
+ "settings.audioMode.remove": "Remove audio",
10
+ "settings.audioMode.compress": "Compress audio",
11
+ "settings.global.title": "Global settings",
12
+ "settings.global.description": "Default optimization settings for all video uploads. Individual videos can be changed in the Media Library upload dialog.",
13
+ "settings.global.defaultChoiceTitle": "Default upload choice",
14
+ "settings.global.defaultChoiceDescription": "Choose which option is pre-selected when opening the upload dialog for a new video.",
15
+ "settings.global.defaultChoice": "Default choice",
16
+ "settings.global.defaultChoiceHint": "This choice is applied automatically unless changed per video in the upload dialog.",
17
+ "settings.global.profileTitle": "Global optimization profile",
18
+ "settings.global.profileDescription": "Used when a video is set to Apply global settings, and as the starting values for Custom.",
19
+ "settings.global.defaultFormat": "Output format",
20
+ "settings.global.videoCodec": "Video codec",
21
+ "settings.global.crf": "CRF (quality)",
22
+ "settings.global.crfHint": "Lower values mean better quality and larger files (0–51).",
23
+ "settings.global.preset": "Encode preset",
24
+ "settings.global.audioMode": "Audio handling",
25
+ "settings.global.audioBitrate": "Audio bitrate",
26
+ "settings.global.codecHint": "Codec is selected automatically based on the output format.",
27
+ "settings.global.concurrencyTitle": "Processing concurrency",
28
+ "settings.global.concurrencyDescription": "Control how many FFmpeg jobs can run at the same time on this server.",
29
+ "settings.global.maxConcurrentJobs": "Max concurrent jobs",
30
+ "settings.global.maxConcurrentJobsHint": "1 is recommended on small servers. Can be set between 1 and 32.",
31
+ "settings.global.maxFfmpegThreads": "Max FFmpeg threads per job",
32
+ "settings.global.maxFfmpegThreadsHint": "Limits CPU usage per encode. Use 1–2 on weak VPS servers. Range: 1–8.",
33
+ "settings.resize.title": "Output dimensions",
34
+ "settings.resize.width": "Width (px)",
35
+ "settings.resize.height": "Height (px)",
36
+ "settings.resize.hint": "Defaults to the original video size. Change either value to resize; the other updates to keep aspect ratio.",
37
+ "settings.resize.globalHint": "Videos larger than this are scaled down while preserving aspect ratio.",
38
+ "settings.global.noPermission": "You do not have permission to view global Video Optimizer settings.",
39
+ "settings.save": "Save",
40
+ "settings.saved": "Settings saved",
41
+ "settings.error": "Failed to save settings",
42
+ "choice.original": "Keep original",
43
+ "choice.original.description": "No optimization is applied. The file is uploaded exactly as selected.",
44
+ "choice.global": "Apply global settings",
45
+ "choice.global.description": "Uses the global optimization profile configured in Settings.",
46
+ "choice.custom": "Custom",
47
+ "choice.custom.description": "Configure format and quality settings specifically for this video.",
48
+ "upload.optimization.label": "Optimization",
49
+ "upload.button.label": "Optimization settings",
50
+ "upload.modal.title": "Video optimization",
51
+ "upload.modal.save": "Save",
52
+ "upload.modal.cancel": "Cancel",
53
+ "upload.mode.footer.global": "Global: {mode}",
54
+ "upload.mode.footer.custom": "Custom: {mode}",
55
+ "jobs.status.queued": "Queued for optimization",
56
+ "jobs.status.processing": "Optimizing",
57
+ "jobs.status.completed": "Optimization completed",
58
+ "jobs.status.failed": "Optimization failed",
59
+ "jobs.stage.queued": "Waiting in queue",
60
+ "jobs.stage.preparing": "Preparing",
61
+ "jobs.stage.encoding": "Encoding video",
62
+ "jobs.stage.finalizing": "Finalizing",
63
+ "jobs.stage.completed": "Completed",
64
+ "jobs.stage.failed": "Failed",
65
+ "jobs.panel.title": "Video optimization",
66
+ "jobs.card.progress": "Optimizing: {progress}% → {format}",
67
+ "jobs.card.queued": "In queue",
68
+ "jobs.notification.completed": "Video #{fileId} optimized successfully. Media Library refreshed.",
69
+ "jobs.notification.failed": "Video optimization failed: {error}",
70
+ "mediaLibrary.button.optimize": "Optimize video",
71
+ "mediaLibrary.button.cancel": "Cancel optimization",
72
+ "mediaLibrary.modal.title": "Video optimization",
73
+ "mediaLibrary.modal.start": "Start optimization"
74
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "plugin.name": "Video Optimizasyon",
3
+ "settings.section-label": "Video Optimizasyon",
4
+ "settings.page.title": "Video Optimizasyon Ayarları",
5
+ "settings.page.description": "Video yüklerken kullanılacak genel varsayılanları buradan ayarlayın. Her video için yükleme penceresinde ayrı seçim yapılabilir.",
6
+ "settings.format.mp4": "MP4 (H.264)",
7
+ "settings.format.webm": "WebM (VP9)",
8
+ "settings.audioMode.keep": "Orijinal sesi koru",
9
+ "settings.audioMode.remove": "Sesi kaldır",
10
+ "settings.audioMode.compress": "Sesi sıkıştır",
11
+ "settings.global.title": "Genel ayarlar",
12
+ "settings.global.description": "Tüm video yüklemeleri için varsayılan optimizasyon ayarları. Her video için Media Library yükleme penceresinde farklı seçim yapılabilir.",
13
+ "settings.global.defaultChoiceTitle": "Varsayılan yükleme seçimi",
14
+ "settings.global.defaultChoiceDescription": "Yeni bir video için yükleme penceresi açıldığında hangi seçeneğin önceden işaretli geleceğini belirler.",
15
+ "settings.global.defaultChoice": "Varsayılan seçim",
16
+ "settings.global.defaultChoiceHint": "Upload penceresinde video bazında değiştirilmedikçe bu seçim uygulanır.",
17
+ "settings.global.profileTitle": "Global optimizasyon profili",
18
+ "settings.global.profileDescription": "Global ayarı uygula seçildiğinde kullanılır; Özel seçeneğinin başlangıç değerlerini de belirler.",
19
+ "settings.global.defaultFormat": "Çıktı formatı",
20
+ "settings.global.videoCodec": "Video codec",
21
+ "settings.global.crf": "CRF (kalite)",
22
+ "settings.global.crfHint": "Düşük değer daha iyi kalite ve daha büyük dosya demektir (0–51).",
23
+ "settings.global.preset": "Encode preset",
24
+ "settings.global.audioMode": "Ses işleme",
25
+ "settings.global.audioBitrate": "Ses bitrate",
26
+ "settings.global.codecHint": "Codec, çıktı formatına göre otomatik seçilir.",
27
+ "settings.global.concurrencyTitle": "İşlem eşzamanlılığı",
28
+ "settings.global.concurrencyDescription": "Bu sunucuda aynı anda kaç FFmpeg işinin çalışabileceğini belirler.",
29
+ "settings.global.maxConcurrentJobs": "Maks. eşzamanlı iş",
30
+ "settings.global.maxConcurrentJobsHint": "Küçük sunucularda 1 önerilir. 1–32 arasında ayarlanabilir.",
31
+ "settings.global.maxFfmpegThreads": "İş başına maks. FFmpeg thread",
32
+ "settings.global.maxFfmpegThreadsHint": "Her encode işinin CPU kullanımını sınırlar. Zayıf VPS'lerde 1–2 önerilir. Aralık: 1–8.",
33
+ "settings.resize.title": "Çıktı boyutları",
34
+ "settings.resize.width": "Genişlik (px)",
35
+ "settings.resize.height": "Yükseklik (px)",
36
+ "settings.resize.hint": "Varsayılan olarak videonun orijinal boyutu kullanılır. Boyutlandırmak için bir değeri değiştirin; diğeri en-boy oranını korumak için güncellenir.",
37
+ "settings.resize.globalHint": "Video bu boyuttan büyükse en-boy oranı korunarak küçültülür.",
38
+ "settings.global.noPermission": "Genel Video Optimizasyon ayarlarını görüntüleme izniniz yok.",
39
+ "settings.save": "Kaydet",
40
+ "settings.saved": "Ayarlar kaydedildi",
41
+ "settings.error": "Ayarlar kaydedilemedi",
42
+ "choice.original": "Olduğu gibi bırak",
43
+ "choice.original.description": "Hiçbir optimizasyon uygulanmaz. Dosya seçildiği haliyle yüklenir.",
44
+ "choice.global": "Global ayarı uygula",
45
+ "choice.global.description": "Settings altındaki global optimizasyon profilini kullanır.",
46
+ "choice.custom": "Özel",
47
+ "choice.custom.description": "Bu video için format ve kalite ayarlarını ayrıca yapılandırın.",
48
+ "upload.optimization.label": "Optimizasyon",
49
+ "upload.button.label": "Optimizasyon ayarları",
50
+ "upload.modal.title": "Video optimizasyonu",
51
+ "upload.modal.save": "Kaydet",
52
+ "upload.modal.cancel": "İptal",
53
+ "upload.mode.footer.global": "Global: {mode}",
54
+ "upload.mode.footer.custom": "Özel: {mode}",
55
+ "jobs.status.queued": "Optimizasyon kuyruğunda",
56
+ "jobs.status.processing": "Optimize ediliyor",
57
+ "jobs.status.completed": "Optimizasyon tamamlandı",
58
+ "jobs.status.failed": "Optimizasyon başarısız",
59
+ "jobs.stage.queued": "Kuyrukta bekliyor",
60
+ "jobs.stage.preparing": "Hazırlanıyor",
61
+ "jobs.stage.encoding": "Video encode ediliyor",
62
+ "jobs.stage.finalizing": "Tamamlanıyor",
63
+ "jobs.stage.completed": "Tamamlandı",
64
+ "jobs.stage.failed": "Başarısız",
65
+ "jobs.panel.title": "Video optimizasyonu",
66
+ "jobs.card.progress": "Optimizasyon: {progress}% → {format}",
67
+ "jobs.card.queued": "Sırada",
68
+ "jobs.notification.completed": "Video #{fileId} başarıyla optimize edildi. Media Library yenilendi.",
69
+ "jobs.notification.failed": "Video optimizasyonu başarısız: {error}",
70
+ "mediaLibrary.button.optimize": "Videoyu optimize et",
71
+ "mediaLibrary.button.cancel": "Optimizasyonu iptal et",
72
+ "mediaLibrary.modal.title": "Video optimizasyonu",
73
+ "mediaLibrary.modal.start": "Optimizasyonu başlat"
74
+ }
@@ -0,0 +1,57 @@
1
+ export const getAuthToken = (): string | null => {
2
+ const fromStorage = localStorage.getItem('jwtToken');
3
+
4
+ if (fromStorage) {
5
+ try {
6
+ return JSON.parse(fromStorage) as string;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ const match = document.cookie.match(/(?:^|;\s*)jwtToken=([^;]+)/);
13
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
14
+ };
15
+
16
+ export const adminFetch = async <T>(
17
+ path: string,
18
+ init: RequestInit = {}
19
+ ): Promise<T | null> => {
20
+ const backendURL = window.strapi?.backendURL;
21
+
22
+ if (!backendURL) {
23
+ return null;
24
+ }
25
+
26
+ const token = getAuthToken();
27
+ const headers = new Headers(init.headers);
28
+
29
+ if (!headers.has('Accept')) {
30
+ headers.set('Accept', 'application/json');
31
+ }
32
+
33
+ if (token && !headers.has('Authorization')) {
34
+ headers.set('Authorization', `Bearer ${token}`);
35
+ }
36
+
37
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
38
+ const response = await fetch(`${backendURL}${normalizedPath}`, {
39
+ ...init,
40
+ headers,
41
+ });
42
+
43
+ if (!response.ok) {
44
+ return null;
45
+ }
46
+
47
+ return response.json() as Promise<T>;
48
+ };
49
+
50
+ export const adminGet = <T>(path: string) => adminFetch<T>(path);
51
+
52
+ export const adminPost = <T>(path: string, body: unknown) =>
53
+ adminFetch<T>(path, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify(body),
57
+ });
@@ -0,0 +1,34 @@
1
+ import { registerMediaLibraryQueryClient } from './invalidateMediaLibrary';
2
+ import { registerMediaLibraryQueryClientBridge } from './mediaLibraryQueryBridge';
3
+
4
+ let installed = false;
5
+
6
+ export const installQueryClientCapture = () => {
7
+ if (installed || typeof window === 'undefined') {
8
+ return;
9
+ }
10
+
11
+ installed = true;
12
+
13
+ void import('react-query')
14
+ .then((reactQuery) => {
15
+ const original = reactQuery.useQueryClient;
16
+
17
+ if ((original as { __videoOptimizerPatched?: boolean }).__videoOptimizerPatched) {
18
+ return;
19
+ }
20
+
21
+ const patched = () => {
22
+ const client = original();
23
+ registerMediaLibraryQueryClientBridge(client);
24
+ registerMediaLibraryQueryClient(client);
25
+ return client;
26
+ };
27
+
28
+ (patched as { __videoOptimizerPatched?: boolean }).__videoOptimizerPatched = true;
29
+ Object.assign(reactQuery, { useQueryClient: patched });
30
+ })
31
+ .catch(() => {
32
+ installed = false;
33
+ });
34
+ };