@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.
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/admin/custom.d.ts +8 -0
- package/admin/src/buildVersion.ts +3 -0
- package/admin/src/components/AssetOptimizationLabel.tsx +61 -0
- package/admin/src/components/BridgeProviders.tsx +123 -0
- package/admin/src/components/MediaLibraryCacheBridge.tsx +24 -0
- package/admin/src/components/MediaLibraryCardActionsBridge.tsx +249 -0
- package/admin/src/components/MediaLibraryJobWatcher.tsx +136 -0
- package/admin/src/components/MediaLibraryProgressBridge.tsx +97 -0
- package/admin/src/components/OptimizationChoicePicker.tsx +65 -0
- package/admin/src/components/OptimizationResizeFields.tsx +120 -0
- package/admin/src/components/OptimizationVideoFields.tsx +217 -0
- package/admin/src/components/UploadEnhancerBridge.tsx +205 -0
- package/admin/src/components/upload/PendingAssetStep.tsx +97 -0
- package/admin/src/defaultGlobalSettings.ts +32 -0
- package/admin/src/hooks/useDefaultOptimizationMode.ts +24 -0
- package/admin/src/hooks/useUploadWithOptimizer.ts +45 -0
- package/admin/src/index.ts +84 -0
- package/admin/src/pages/SettingsPage.tsx +208 -0
- package/admin/src/pluginId.ts +79 -0
- package/admin/src/translations/en.json +74 -0
- package/admin/src/translations/tr.json +74 -0
- package/admin/src/utils/adminFetch.ts +57 -0
- package/admin/src/utils/captureQueryClient.ts +34 -0
- package/admin/src/utils/debugMediaLibraryProgress.ts +70 -0
- package/admin/src/utils/extractAssetDimensions.ts +22 -0
- package/admin/src/utils/initJobPoller.ts +173 -0
- package/admin/src/utils/initMediaLibraryCardActions.ts +308 -0
- package/admin/src/utils/initMediaLibraryProgress.ts +219 -0
- package/admin/src/utils/initUploadEnhancer.ts +447 -0
- package/admin/src/utils/invalidateMediaLibrary.ts +203 -0
- package/admin/src/utils/jobProgressStore.ts +113 -0
- package/admin/src/utils/mediaLibraryCardMatch.ts +414 -0
- package/admin/src/utils/mediaLibraryCardStore.ts +223 -0
- package/admin/src/utils/mediaLibraryQueryBridge.ts +113 -0
- package/admin/src/utils/mediaLibraryRoute.ts +9 -0
- package/admin/src/utils/optimizationFields.ts +17 -0
- package/admin/src/utils/probeVideoDimensions.ts +94 -0
- package/admin/src/utils/uploadAssetStore.ts +670 -0
- package/admin/tsconfig.json +8 -0
- package/dist/admin/SettingsPage-CN2fR83m.js +150 -0
- package/dist/admin/SettingsPage-D6e536P0.mjs +150 -0
- package/dist/admin/en-CqM903j3.js +77 -0
- package/dist/admin/en-CsHicGzL.mjs +77 -0
- package/dist/admin/index-BjWoS0YU.js +2542 -0
- package/dist/admin/index-Cs_uiChW.mjs +2541 -0
- package/dist/admin/index-DOuHOS2G.js +8799 -0
- package/dist/admin/index-rAmxCQz6.mjs +8781 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/tr-Y0-ANilh.mjs +77 -0
- package/dist/admin/tr-muzHkdC4.js +77 -0
- package/dist/server/index.js +1538 -0
- package/dist/server/index.mjs +1533 -0
- package/package.json +100 -0
- package/server/index.js +1 -0
- package/server/src/bootstrap.ts +377 -0
- package/server/src/buildVersion.ts +1 -0
- package/server/src/config/defaults.ts +91 -0
- package/server/src/config/index.ts +51 -0
- package/server/src/constants.ts +83 -0
- package/server/src/controllers/index.ts +7 -0
- package/server/src/controllers/job.ts +102 -0
- package/server/src/controllers/preference.ts +206 -0
- package/server/src/index.ts +15 -0
- package/server/src/register.ts +19 -0
- package/server/src/routes/index.ts +103 -0
- package/server/src/services/index.ts +9 -0
- package/server/src/services/job-queue.ts +663 -0
- package/server/src/services/optimizer.ts +284 -0
- package/server/src/services/preference.ts +172 -0
- package/server/src/utils/request-context.ts +7 -0
- package/server/src/utils/upload-preferences-context.ts +202 -0
- package/server/tsconfig.json +8 -0
- package/strapi-admin.js +7 -0
- package/strapi-server.js +7 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { MediaLibraryProgressRoot } from '../components/BridgeProviders';
|
|
4
|
+
import {
|
|
5
|
+
cleanupOrphanProgressHosts,
|
|
6
|
+
clearJobCardRefs,
|
|
7
|
+
collectVideoCards,
|
|
8
|
+
findCardForJob,
|
|
9
|
+
prepareCardsForMatching,
|
|
10
|
+
} from './mediaLibraryCardMatch';
|
|
11
|
+
import {
|
|
12
|
+
clearProgressEntries,
|
|
13
|
+
getProgressEntries,
|
|
14
|
+
getWatchedJobs,
|
|
15
|
+
setProgressEntries,
|
|
16
|
+
subscribeJobProgress,
|
|
17
|
+
type ProgressEntry,
|
|
18
|
+
} from './jobProgressStore';
|
|
19
|
+
import { getUploadAssetsFromCache } from './mediaLibraryQueryBridge';
|
|
20
|
+
import { isMediaLibraryPath } from './mediaLibraryRoute';
|
|
21
|
+
import { syncMediaLibraryCardActions } from './initMediaLibraryCardActions';
|
|
22
|
+
|
|
23
|
+
let progressRoot: Root | null = null;
|
|
24
|
+
let progressHost: HTMLElement | null = null;
|
|
25
|
+
let domObserver: MutationObserver | null = null;
|
|
26
|
+
let domObserverTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
+
let started = false;
|
|
28
|
+
let syncInFlight = false;
|
|
29
|
+
|
|
30
|
+
const isMediaLibraryRoute = () => isMediaLibraryPath(window.location.pathname);
|
|
31
|
+
|
|
32
|
+
const ensureProgressBridge = () => {
|
|
33
|
+
if (progressRoot) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
progressHost = document.createElement('div');
|
|
38
|
+
progressHost.id = 'video-optimizer-media-library-progress';
|
|
39
|
+
progressHost.style.cssText = 'display:none;';
|
|
40
|
+
document.body.appendChild(progressHost);
|
|
41
|
+
|
|
42
|
+
progressRoot = createRoot(progressHost);
|
|
43
|
+
progressRoot.render(React.createElement(MediaLibraryProgressRoot));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ensureMediaLibraryBridge = () => {
|
|
47
|
+
ensureProgressBridge();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const findCardFooter = (card: Element) => {
|
|
51
|
+
const titleNode = card.querySelector('[id$="-title"]');
|
|
52
|
+
const footerFromTitle = titleNode?.closest('article > div:last-of-type');
|
|
53
|
+
|
|
54
|
+
if (footerFromTitle && card.contains(footerFromTitle)) {
|
|
55
|
+
return footerFromTitle;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const directChildren = card.querySelectorAll(':scope > div');
|
|
59
|
+
|
|
60
|
+
if (directChildren.length >= 2) {
|
|
61
|
+
return directChildren[directChildren.length - 1]!;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return card;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const ensureProgressHost = (card: Element, fileId: number) => {
|
|
68
|
+
const selector = `[data-video-optimizer-progress-host="${fileId}"]`;
|
|
69
|
+
let host = card.querySelector(selector) as HTMLElement | null;
|
|
70
|
+
|
|
71
|
+
if (!host) {
|
|
72
|
+
host = document.createElement('div');
|
|
73
|
+
host.dataset.videoOptimizerProgressHost = String(fileId);
|
|
74
|
+
host.style.cssText = 'width:100%;box-sizing:border-box;display:block;';
|
|
75
|
+
|
|
76
|
+
const cardBody =
|
|
77
|
+
card.querySelector('[class*="CardBody"]') ??
|
|
78
|
+
card.querySelector('[class*="CardContent"]') ??
|
|
79
|
+
findCardFooter(card);
|
|
80
|
+
|
|
81
|
+
cardBody.appendChild(host);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
(card as HTMLElement).dataset.videoOptimizerFileId = String(fileId);
|
|
85
|
+
return host;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const syncDomObserver = () => {
|
|
89
|
+
const activeJobs = getWatchedJobs().filter(
|
|
90
|
+
(job) => job.status === 'queued' || job.status === 'processing'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!activeJobs.length) {
|
|
94
|
+
domObserver?.disconnect();
|
|
95
|
+
domObserver = null;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (domObserver) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const root = document.querySelector('main') ?? document.body;
|
|
104
|
+
|
|
105
|
+
domObserver = new MutationObserver(() => {
|
|
106
|
+
if (domObserverTimer) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
domObserverTimer = setTimeout(() => {
|
|
111
|
+
domObserverTimer = null;
|
|
112
|
+
syncProgress();
|
|
113
|
+
}, 250);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
domObserver.observe(root, { childList: true, subtree: true });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const syncProgress = () => {
|
|
120
|
+
if (syncInFlight) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
syncInFlight = true;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (!isMediaLibraryRoute()) {
|
|
128
|
+
clearProgressEntries();
|
|
129
|
+
domObserver?.disconnect();
|
|
130
|
+
domObserver = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
ensureProgressBridge();
|
|
135
|
+
|
|
136
|
+
const activeJobs = getWatchedJobs().filter(
|
|
137
|
+
(job) => job.status === 'queued' || job.status === 'processing'
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (!activeJobs.length) {
|
|
141
|
+
cleanupOrphanProgressHosts(new Set());
|
|
142
|
+
setProgressEntries([]);
|
|
143
|
+
syncDomObserver();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const activeFileIds = new Set(activeJobs.map((job) => job.fileId));
|
|
148
|
+
cleanupOrphanProgressHosts(activeFileIds);
|
|
149
|
+
clearJobCardRefs(activeFileIds);
|
|
150
|
+
|
|
151
|
+
const cards = collectVideoCards();
|
|
152
|
+
prepareCardsForMatching(cards, syncMediaLibraryProgress);
|
|
153
|
+
|
|
154
|
+
const uploadAssets = getUploadAssetsFromCache();
|
|
155
|
+
const assignedCards = new Set<Element>();
|
|
156
|
+
const existingByFileId = new Map(
|
|
157
|
+
getProgressEntries().map((entry) => [entry.fileId, entry])
|
|
158
|
+
);
|
|
159
|
+
const nextEntries: ProgressEntry[] = [];
|
|
160
|
+
|
|
161
|
+
for (const job of activeJobs) {
|
|
162
|
+
const existing = existingByFileId.get(job.fileId);
|
|
163
|
+
let card = findCardForJob(job, cards, assignedCards, uploadAssets);
|
|
164
|
+
let host: HTMLElement | null = null;
|
|
165
|
+
|
|
166
|
+
if (!card && existing?.host.isConnected) {
|
|
167
|
+
host = existing.host;
|
|
168
|
+
card =
|
|
169
|
+
host.closest('article[role="button"], article[aria-labelledby], [role="listitem"]') ??
|
|
170
|
+
host.parentElement;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!card) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
assignedCards.add(card);
|
|
178
|
+
host = host ?? ensureProgressHost(card, job.fileId);
|
|
179
|
+
|
|
180
|
+
nextEntries.push({
|
|
181
|
+
fileId: job.fileId,
|
|
182
|
+
host,
|
|
183
|
+
job,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setProgressEntries(nextEntries);
|
|
188
|
+
syncDomObserver();
|
|
189
|
+
syncMediaLibraryCardActions();
|
|
190
|
+
} finally {
|
|
191
|
+
syncInFlight = false;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const syncMediaLibraryProgress = () => {
|
|
196
|
+
syncProgress();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const initMediaLibraryProgress = () => {
|
|
200
|
+
if (started || typeof window === 'undefined') {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
started = true;
|
|
205
|
+
|
|
206
|
+
subscribeJobProgress(() => {
|
|
207
|
+
syncProgress();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const boot = () => {
|
|
211
|
+
syncProgress();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (document.readyState === 'loading') {
|
|
215
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
216
|
+
} else {
|
|
217
|
+
boot();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { UploadEnhancerRoot } from '../components/BridgeProviders';
|
|
4
|
+
import { mergeGlobalSettings } from '../defaultGlobalSettings';
|
|
5
|
+
import type { GlobalOptimizationSettings } from '../pluginId';
|
|
6
|
+
import { patchUploadFetch, patchUploadXHR } from './uploadAssetStore';
|
|
7
|
+
import {
|
|
8
|
+
clearUploadSession,
|
|
9
|
+
getEditingAssetId,
|
|
10
|
+
registerAsset,
|
|
11
|
+
setGlobalSettings,
|
|
12
|
+
setUploadAssetCards,
|
|
13
|
+
setUploadDialogElement,
|
|
14
|
+
updateAssetDimensions,
|
|
15
|
+
type UploadAssetEntry,
|
|
16
|
+
} from './uploadAssetStore';
|
|
17
|
+
import { isVideoFileName } from '../pluginId';
|
|
18
|
+
import { extractAssetDimensions } from './extractAssetDimensions';
|
|
19
|
+
import {
|
|
20
|
+
ensureVideoElementDimensions,
|
|
21
|
+
findUploadFilesInDialog,
|
|
22
|
+
matchUploadFile,
|
|
23
|
+
probeVideoFileDimensions,
|
|
24
|
+
} from './probeVideoDimensions';
|
|
25
|
+
|
|
26
|
+
const pendingDimensionProbes = new Set<string>();
|
|
27
|
+
|
|
28
|
+
let bridgeRoot: Root | null = null;
|
|
29
|
+
let bridgeHost: HTMLElement | null = null;
|
|
30
|
+
let mountedDialog: Element | null = null;
|
|
31
|
+
let started = false;
|
|
32
|
+
|
|
33
|
+
const VIDEO_EXT_PATTERN =
|
|
34
|
+
/\.(mp4|webm|mov|avi|mkv|m4v|ogv|wmv|flv|3gp)/i;
|
|
35
|
+
|
|
36
|
+
const getAuthToken = (): string | null => {
|
|
37
|
+
const fromStorage = localStorage.getItem('jwtToken');
|
|
38
|
+
if (fromStorage) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fromStorage) as string;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const match = document.cookie.match(/(?:^|;\s*)jwtToken=([^;]+)/);
|
|
47
|
+
return match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const loadGlobalSettings = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const backendURL = window.strapi?.backendURL;
|
|
53
|
+
if (!backendURL) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const token = getAuthToken();
|
|
58
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
59
|
+
if (token) {
|
|
60
|
+
headers.Authorization = `Bearer ${token}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await fetch(`${backendURL}/video-optimizer/default-mode`, { headers });
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = (await response.json()) as Partial<GlobalOptimizationSettings>;
|
|
69
|
+
|
|
70
|
+
setGlobalSettings(mergeGlobalSettings(data));
|
|
71
|
+
} catch {
|
|
72
|
+
// Keep fallback defaults.
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getButtonLabel = (btn: Element) => {
|
|
77
|
+
const aria = btn.getAttribute('aria-label') ?? '';
|
|
78
|
+
const text = btn.textContent ?? '';
|
|
79
|
+
const title = btn.getAttribute('title') ?? '';
|
|
80
|
+
|
|
81
|
+
return `${aria} ${text} ${title}`.toLowerCase();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isActionButtonLabel = (btn: Element, keywords: string[]) => {
|
|
85
|
+
const label = getButtonLabel(btn);
|
|
86
|
+
return keywords.some((keyword) => label.includes(keyword));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const isUploadPendingModal = (root: Element) => {
|
|
90
|
+
const text = root.textContent?.toLowerCase() ?? '';
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
text.includes('ready to upload') ||
|
|
94
|
+
text.includes('yüklenmeye hazır') ||
|
|
95
|
+
text.includes('manage the assets before adding') ||
|
|
96
|
+
text.includes('medya kütüphanesine eklemeden önce')
|
|
97
|
+
) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hasVideoAsset = VIDEO_EXT_PATTERN.test(text);
|
|
102
|
+
const hasEditButton = [...root.querySelectorAll('button')].some((btn) =>
|
|
103
|
+
isActionButtonLabel(btn, ['edit', 'düzenle'])
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return hasVideoAsset && hasEditButton;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const isLikelyVideoCard = (card: Element) => {
|
|
110
|
+
const text = card.textContent?.toLowerCase() ?? '';
|
|
111
|
+
return (
|
|
112
|
+
VIDEO_EXT_PATTERN.test(text) ||
|
|
113
|
+
/\b(mp4|webm|mov|avi|mkv|m4v|ogv|wmv|flv|3gp)\b/.test(text) ||
|
|
114
|
+
text.includes('video') ||
|
|
115
|
+
text.includes('video')
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const isOptimizerMutationNode = (node: Node): boolean => {
|
|
120
|
+
if (!(node instanceof Element)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Boolean(
|
|
125
|
+
node.id === 'video-optimizer-upload-bridge' ||
|
|
126
|
+
node.closest('#video-optimizer-upload-bridge, [data-optimizer-action-host], [data-optimizer-footer-host]')
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const shouldSyncForMutations = (mutations: MutationRecord[]) => {
|
|
131
|
+
return mutations.some((mutation) => {
|
|
132
|
+
if (isOptimizerMutationNode(mutation.target)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (mutation.type === 'attributes' && isOptimizerMutationNode(mutation.target)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const node of mutation.addedNodes) {
|
|
141
|
+
if (!isOptimizerMutationNode(node)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const node of mutation.removedNodes) {
|
|
147
|
+
if (!isOptimizerMutationNode(node)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return mutation.type === 'characterData';
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const extractAssetName = (card: Element) => {
|
|
157
|
+
const titleEl = card.querySelector('h2, [class*="CardTitle"]');
|
|
158
|
+
const titleText = titleEl?.textContent?.trim() ?? '';
|
|
159
|
+
|
|
160
|
+
if (titleText && isVideoFileName(titleText)) {
|
|
161
|
+
return titleText.match(/[\w.-]+\.(mp4|webm|mov|avi|mkv|m4v|ogv|wmv|flv|3gp)/i)?.[0] ?? titleText;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const cardText = card.textContent ?? '';
|
|
165
|
+
const dottedMatch = cardText.match(/[\w.-]+\.(mp4|webm|mov|avi|mkv|m4v|ogv|wmv|flv|3gp)/i);
|
|
166
|
+
if (dottedMatch) {
|
|
167
|
+
return dottedMatch[0];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const subtitleEl = card.querySelector('[class*="CardSubtitle"]');
|
|
171
|
+
const subtitleText = subtitleEl?.textContent?.trim() ?? '';
|
|
172
|
+
const extOnly = subtitleText.match(/^([a-z0-9]+)/i)?.[1]?.toLowerCase();
|
|
173
|
+
|
|
174
|
+
if (titleText && extOnly) {
|
|
175
|
+
if (titleText.toLowerCase().endsWith(`.${extOnly}`)) {
|
|
176
|
+
return titleText;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return `${titleText}.${extOnly}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return titleText || null;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const findCardRoot = (editButton: Element, dialog: Element) => {
|
|
186
|
+
let element = editButton.parentElement;
|
|
187
|
+
|
|
188
|
+
while (element && element !== dialog) {
|
|
189
|
+
if (element.getAttribute('role') === 'button') {
|
|
190
|
+
return element;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (element.tagName === 'ARTICLE' || element.querySelector('h2, [class*="CardTitle"]')) {
|
|
194
|
+
return element;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
element = element.parentElement;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const findUploadDialog = () => {
|
|
204
|
+
const candidates = [
|
|
205
|
+
...document.querySelectorAll('[role="dialog"]'),
|
|
206
|
+
...document.querySelectorAll('[aria-modal="true"]'),
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
return candidates.find(isUploadPendingModal) ?? null;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const cleanupEnhancerDom = () => {
|
|
213
|
+
document.querySelectorAll('[data-optimizer-footer-host]').forEach((el) => el.remove());
|
|
214
|
+
document.querySelectorAll('[data-optimizer-action-host]').forEach((el) => el.remove());
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const unmountBridge = () => {
|
|
218
|
+
bridgeRoot?.unmount();
|
|
219
|
+
bridgeRoot = null;
|
|
220
|
+
bridgeHost?.remove();
|
|
221
|
+
bridgeHost = null;
|
|
222
|
+
mountedDialog = null;
|
|
223
|
+
cleanupEnhancerDom();
|
|
224
|
+
setUploadDialogElement(null);
|
|
225
|
+
setUploadAssetCards([]);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const ensureBridge = (dialog: Element) => {
|
|
229
|
+
if (mountedDialog === dialog && bridgeRoot) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (getEditingAssetId()) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
unmountBridge();
|
|
238
|
+
mountedDialog = dialog;
|
|
239
|
+
|
|
240
|
+
const dialogElement = dialog as HTMLElement;
|
|
241
|
+
if (getComputedStyle(dialogElement).position === 'static') {
|
|
242
|
+
dialogElement.style.position = 'relative';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
setUploadDialogElement(dialogElement);
|
|
246
|
+
|
|
247
|
+
bridgeHost = document.createElement('div');
|
|
248
|
+
bridgeHost.id = 'video-optimizer-upload-bridge';
|
|
249
|
+
bridgeHost.style.cssText = 'display:none;';
|
|
250
|
+
dialog.appendChild(bridgeHost);
|
|
251
|
+
|
|
252
|
+
bridgeRoot = createRoot(bridgeHost);
|
|
253
|
+
bridgeRoot.render(React.createElement(UploadEnhancerRoot));
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const ensureActionHost = (actionsContainer: HTMLElement, editButton: Element, assetId: string) => {
|
|
257
|
+
const selector = `[data-optimizer-action-host="${assetId}"]`;
|
|
258
|
+
let actionHost = actionsContainer.querySelector(selector) as HTMLElement | null;
|
|
259
|
+
|
|
260
|
+
if (!actionHost) {
|
|
261
|
+
actionHost = document.createElement('span');
|
|
262
|
+
actionHost.dataset.optimizerActionHost = assetId;
|
|
263
|
+
editButton.insertAdjacentElement('afterend', actionHost);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
actionHost.style.cssText = 'display:contents;';
|
|
267
|
+
|
|
268
|
+
return actionHost;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const ensureFooterHost = (cardElement: HTMLElement, assetId: string) => {
|
|
272
|
+
const parent = cardElement.parentElement;
|
|
273
|
+
|
|
274
|
+
if (!parent) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const selector = `[data-optimizer-footer-host="${assetId}"]`;
|
|
279
|
+
let footerHost = parent.querySelector(selector) as HTMLElement | null;
|
|
280
|
+
|
|
281
|
+
if (!footerHost) {
|
|
282
|
+
footerHost = document.createElement('div');
|
|
283
|
+
footerHost.dataset.optimizerFooterHost = assetId;
|
|
284
|
+
parent.appendChild(footerHost);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return footerHost;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const queueDimensionProbe = (
|
|
291
|
+
dialog: Element,
|
|
292
|
+
assetId: string,
|
|
293
|
+
assetName: string,
|
|
294
|
+
card: Element
|
|
295
|
+
) => {
|
|
296
|
+
if (pendingDimensionProbes.has(assetId)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
pendingDimensionProbes.add(assetId);
|
|
301
|
+
|
|
302
|
+
void (async () => {
|
|
303
|
+
try {
|
|
304
|
+
let dimensions = extractAssetDimensions(card);
|
|
305
|
+
|
|
306
|
+
if (!dimensions) {
|
|
307
|
+
dimensions = await ensureVideoElementDimensions(card);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!dimensions) {
|
|
311
|
+
const files = findUploadFilesInDialog(dialog);
|
|
312
|
+
const file = matchUploadFile(files, assetName);
|
|
313
|
+
|
|
314
|
+
if (file) {
|
|
315
|
+
dimensions = await probeVideoFileDimensions(file);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (dimensions) {
|
|
320
|
+
updateAssetDimensions(assetId, dimensions);
|
|
321
|
+
}
|
|
322
|
+
} finally {
|
|
323
|
+
pendingDimensionProbes.delete(assetId);
|
|
324
|
+
}
|
|
325
|
+
})();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const collectCards = (dialog: Element): UploadAssetEntry[] => {
|
|
329
|
+
const entries: UploadAssetEntry[] = [];
|
|
330
|
+
const seenCards = new Set<Element>();
|
|
331
|
+
|
|
332
|
+
dialog.querySelectorAll('button').forEach((button) => {
|
|
333
|
+
if (isActionButtonLabel(button, ['optimization', 'optimizasyon'])) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!isActionButtonLabel(button, ['edit', 'düzenle'])) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const card = findCardRoot(button, dialog);
|
|
342
|
+
if (!card || seenCards.has(card)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
seenCards.add(card);
|
|
347
|
+
|
|
348
|
+
if (!isLikelyVideoCard(card)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const actionsContainer = button.parentElement as HTMLElement | null;
|
|
353
|
+
if (!actionsContainer) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const assetName = extractAssetName(card);
|
|
358
|
+
if (!assetName) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let assetId = card.getAttribute('data-optimizer-asset-id');
|
|
363
|
+
if (!assetId) {
|
|
364
|
+
assetId = crypto.randomUUID();
|
|
365
|
+
card.setAttribute('data-optimizer-asset-id', assetId);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const dimensions = extractAssetDimensions(card);
|
|
369
|
+
|
|
370
|
+
registerAsset(assetId, assetName, dimensions);
|
|
371
|
+
|
|
372
|
+
if (!dimensions) {
|
|
373
|
+
queueDimensionProbe(dialog, assetId, assetName, card);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
entries.push({
|
|
377
|
+
assetId,
|
|
378
|
+
assetName,
|
|
379
|
+
width: dimensions?.width,
|
|
380
|
+
height: dimensions?.height,
|
|
381
|
+
actionsContainer: ensureActionHost(actionsContainer, button, assetId),
|
|
382
|
+
footerHost: ensureFooterHost(card as HTMLElement, assetId) ?? undefined,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return entries;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const syncUploadModal = () => {
|
|
390
|
+
const dialog = findUploadDialog();
|
|
391
|
+
|
|
392
|
+
if (!dialog) {
|
|
393
|
+
if (getEditingAssetId()) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
clearUploadSession();
|
|
398
|
+
unmountBridge();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
ensureBridge(dialog);
|
|
403
|
+
setUploadAssetCards(collectCards(dialog));
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
let syncScheduled = false;
|
|
407
|
+
|
|
408
|
+
const scheduleSyncUploadModal = () => {
|
|
409
|
+
if (syncScheduled) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
syncScheduled = true;
|
|
414
|
+
requestAnimationFrame(() => {
|
|
415
|
+
syncScheduled = false;
|
|
416
|
+
syncUploadModal();
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export const initUploadEnhancer = () => {
|
|
421
|
+
if (started || typeof window === 'undefined') {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
started = true;
|
|
426
|
+
patchUploadFetch();
|
|
427
|
+
patchUploadXHR();
|
|
428
|
+
|
|
429
|
+
const boot = () => {
|
|
430
|
+
loadGlobalSettings();
|
|
431
|
+
syncUploadModal();
|
|
432
|
+
|
|
433
|
+
const observer = new MutationObserver((mutations) => {
|
|
434
|
+
if (shouldSyncForMutations(mutations)) {
|
|
435
|
+
scheduleSyncUploadModal();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (document.readyState === 'loading') {
|
|
443
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
444
|
+
} else {
|
|
445
|
+
boot();
|
|
446
|
+
}
|
|
447
|
+
};
|