strapi-plugin-radiocult-uploader 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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/admin/App-Cd-TZrqC.mjs +1179 -0
  4. package/dist/admin/App-DTOuR7aY.js +1199 -0
  5. package/dist/admin/Settings-B3cZR7VF.mjs +516 -0
  6. package/dist/admin/Settings-DHv9SmRU.js +534 -0
  7. package/dist/admin/en-CTCtUg23.mjs +8 -0
  8. package/dist/admin/en-D_61ojL7.js +8 -0
  9. package/dist/admin/index-BLAtUo_j.js +334 -0
  10. package/dist/admin/index-Dj3e3A79.mjs +318 -0
  11. package/dist/admin/index.js +4 -0
  12. package/dist/admin/index.mjs +4 -0
  13. package/dist/admin/src/api.d.ts +107 -0
  14. package/dist/admin/src/components/Initializer.d.ts +5 -0
  15. package/dist/admin/src/components/Panel.d.ts +18 -0
  16. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  17. package/dist/admin/src/index.d.ts +3 -0
  18. package/dist/admin/src/pages/App.d.ts +2 -0
  19. package/dist/admin/src/pages/HomePage.d.ts +6 -0
  20. package/dist/admin/src/pages/Settings.d.ts +8 -0
  21. package/dist/admin/src/pages/Uploader.d.ts +10 -0
  22. package/dist/admin/src/pluginId.d.ts +1 -0
  23. package/dist/admin/src/theme/tokens.d.ts +96 -0
  24. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  25. package/dist/admin/tokens-ACCekCH6.mjs +161 -0
  26. package/dist/admin/tokens-CDeNcvZq.js +178 -0
  27. package/dist/server/index.js +1193 -0
  28. package/dist/server/index.mjs +1165 -0
  29. package/dist/server/src/bootstrap.d.ts +5 -0
  30. package/dist/server/src/config/index.d.ts +17 -0
  31. package/dist/server/src/content-types/index.d.ts +51 -0
  32. package/dist/server/src/content-types/track-record/schema.json.d.ts +29 -0
  33. package/dist/server/src/controllers/index.d.ts +21 -0
  34. package/dist/server/src/controllers/settings.d.ts +16 -0
  35. package/dist/server/src/controllers/uploader.d.ts +10 -0
  36. package/dist/server/src/controllers/utils.d.ts +16 -0
  37. package/dist/server/src/destroy.d.ts +5 -0
  38. package/dist/server/src/index.d.ts +267 -0
  39. package/dist/server/src/middlewares/index.d.ts +2 -0
  40. package/dist/server/src/policies/index.d.ts +2 -0
  41. package/dist/server/src/register.d.ts +5 -0
  42. package/dist/server/src/routes/admin/index.d.ts +22 -0
  43. package/dist/server/src/routes/index.d.ts +19 -0
  44. package/dist/server/src/services/id3.d.ts +18 -0
  45. package/dist/server/src/services/index.d.ts +156 -0
  46. package/dist/server/src/services/mapping.d.ts +47 -0
  47. package/dist/server/src/services/radiocult.d.ts +115 -0
  48. package/dist/server/src/services/records.d.ts +25 -0
  49. package/dist/server/src/services/settings.d.ts +103 -0
  50. package/dist/server/src/utils/errors.d.ts +5 -0
  51. package/dist/server/src/utils/plugin.d.ts +4 -0
  52. package/logo.png +0 -0
  53. package/package.json +98 -0
  54. package/scripts/migrate-from-fields.mjs +78 -0
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ const React = require("react");
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const icons = require("@strapi/icons");
5
+ const designSystem = require("@strapi/design-system");
6
+ const admin = require("@strapi/strapi/admin");
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
9
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
10
+ if (e) {
11
+ for (const k in e) {
12
+ if (k !== "default") {
13
+ const d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: () => e[k]
17
+ });
18
+ }
19
+ }
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
25
+ const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
26
+ const v = glob[path];
27
+ if (v) {
28
+ return typeof v === "function" ? v() : Promise.resolve(v);
29
+ }
30
+ return new Promise((_, reject) => {
31
+ (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
32
+ reject.bind(
33
+ null,
34
+ new Error(
35
+ "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
36
+ )
37
+ )
38
+ );
39
+ });
40
+ };
41
+ const PLUGIN_ID = "radiocult-uploader";
42
+ const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
43
+ const Initializer = ({ setPlugin }) => {
44
+ const ref = React.useRef(setPlugin);
45
+ React.useEffect(() => {
46
+ ref.current(PLUGIN_ID);
47
+ }, []);
48
+ return null;
49
+ };
50
+ const PluginIcon = () => /* @__PURE__ */ jsxRuntime.jsx(icons.PuzzlePiece, {});
51
+ const base = `/${PLUGIN_ID}`;
52
+ const PLATFORMS = ["mixcloud", "soundcloud"];
53
+ const PLATFORM_LABEL = {
54
+ mixcloud: "Mixcloud",
55
+ soundcloud: "SoundCloud"
56
+ };
57
+ const apiError = (error) => error?.response?.data?.error ?? error?.message ?? "Something went wrong";
58
+ const fetchStatus = async (documentId, refresh = false) => {
59
+ const { get } = admin.getFetchClient();
60
+ const { data } = await get(
61
+ `${base}/entries/${documentId}/status${refresh ? "?refresh=true" : ""}`
62
+ );
63
+ return data;
64
+ };
65
+ const publishTrack = async (documentId, platform, body, force = false) => {
66
+ const { post } = admin.getFetchClient();
67
+ const { data } = await post(
68
+ `${base}/entries/${documentId}/publish/${platform}${force ? "?force=true" : ""}`,
69
+ body
70
+ );
71
+ return data;
72
+ };
73
+ const saveTrackDetails = async (documentId, meta) => {
74
+ const { put } = admin.getFetchClient();
75
+ const { data } = await put(`${base}/entries/${documentId}/track`, meta);
76
+ return data;
77
+ };
78
+ const fetchUiConfig = async () => {
79
+ const { get } = admin.getFetchClient();
80
+ const { data } = await get(`${base}/ui-config`);
81
+ return data;
82
+ };
83
+ const fetchSettings = async () => {
84
+ const { get } = admin.getFetchClient();
85
+ const { data } = await get(`${base}/settings`);
86
+ return data;
87
+ };
88
+ const saveSettings = async (patch) => {
89
+ const { put } = admin.getFetchClient();
90
+ const { data } = await put(`${base}/settings`, patch);
91
+ return data;
92
+ };
93
+ const testConnection = async () => {
94
+ const { post } = admin.getFetchClient();
95
+ const { data } = await post(`${base}/settings/test-connection`, {});
96
+ return data;
97
+ };
98
+ const fetchContentTypes = async () => {
99
+ const { get } = admin.getFetchClient();
100
+ const { data } = await get(`${base}/content-types`);
101
+ return data.contentTypes;
102
+ };
103
+ const fetchDiagnostics = async () => {
104
+ const { get } = admin.getFetchClient();
105
+ const { data } = await get(`${base}/diagnostics`);
106
+ return data;
107
+ };
108
+ const uploadTrack = (documentId, file, meta, force, token, callbacks) => {
109
+ const backendURL = window.strapi?.backendURL ?? "";
110
+ const xhr = new XMLHttpRequest();
111
+ xhr.open(
112
+ "POST",
113
+ `${backendURL}${base}/entries/${encodeURIComponent(documentId)}/upload${force ? "?force=true" : ""}`
114
+ );
115
+ xhr.setRequestHeader("Authorization", `Bearer ${token}`);
116
+ xhr.responseType = "json";
117
+ xhr.timeout = 0;
118
+ xhr.upload.onprogress = (ev) => {
119
+ if (ev.lengthComputable) callbacks.onProgress(Math.round(ev.loaded / ev.total * 100));
120
+ };
121
+ xhr.upload.onload = () => callbacks.onProgress(null);
122
+ xhr.onload = () => {
123
+ const body = xhr.response ?? {};
124
+ if (xhr.status >= 200 && xhr.status < 300) callbacks.onDone(body);
125
+ else if (xhr.status === 409 && !force) {
126
+ callbacks.onConflict(body.error ?? "This entry already has a Radio Cult track.");
127
+ } else callbacks.onError(body.error ?? `HTTP ${xhr.status}`, xhr.status);
128
+ };
129
+ xhr.onerror = () => callbacks.onError("Network error — is the server reachable?");
130
+ xhr.ontimeout = () => callbacks.onError("Timed out");
131
+ xhr.onabort = () => callbacks.onAbort();
132
+ const fd = new FormData();
133
+ fd.append("title", meta.title);
134
+ fd.append("artist", meta.artist);
135
+ fd.append("album", meta.album);
136
+ fd.append("files", file, file.name);
137
+ xhr.send(fd);
138
+ return xhr;
139
+ };
140
+ const WRAP = { overflowWrap: "anywhere", minWidth: 0 };
141
+ const shortLink = (url) => {
142
+ let text = url;
143
+ try {
144
+ const parsed = new URL(url);
145
+ text = parsed.host.replace(/^www\./, "") + parsed.pathname.replace(/\/$/, "");
146
+ } catch {
147
+ }
148
+ return text.length > 40 ? `${text.slice(0, 39)}…` : text;
149
+ };
150
+ const PlatformLine = ({ label, status }) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 0, style: { minWidth: 0 }, children: [
151
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: label }),
152
+ status.state === "linked" && status.link ? /* @__PURE__ */ jsxRuntime.jsx(
153
+ "a",
154
+ {
155
+ href: status.link,
156
+ target: "_blank",
157
+ rel: "noreferrer",
158
+ title: status.link,
159
+ style: { display: "block", minWidth: 0 },
160
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "primary600", ellipsis: true, children: shortLink(status.link) })
161
+ }
162
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
163
+ designSystem.Typography,
164
+ {
165
+ variant: "pi",
166
+ textColor: status.state === "stalled" ? "danger600" : "neutral600",
167
+ style: WRAP,
168
+ children: status.state === "publishing" ? "Publishing… link is saved automatically" : status.state === "stalled" ? "No link after 7 days — publish again from the uploader" : "Not published"
169
+ }
170
+ )
171
+ ] });
172
+ const PanelContent = ({ documentId }) => {
173
+ const uploaderHref = `/admin/plugins/${PLUGIN_ID}/upload/${documentId}`;
174
+ const [status, setStatus] = React__namespace.useState(null);
175
+ const [message, setMessage] = React__namespace.useState(null);
176
+ const [checking, setChecking] = React__namespace.useState(false);
177
+ const load = React__namespace.useCallback(
178
+ async (refresh = false) => {
179
+ try {
180
+ const data = await fetchStatus(documentId, refresh);
181
+ setStatus(data);
182
+ setMessage(data.rateLimitMessage ?? null);
183
+ } catch (error) {
184
+ setMessage(apiError(error));
185
+ }
186
+ },
187
+ [documentId]
188
+ );
189
+ React__namespace.useEffect(() => {
190
+ load();
191
+ }, [load]);
192
+ return (
193
+ // content-manager wraps every side panel in a Flex with
194
+ // alignItems="flex-start", so a child sizes to its own content and any
195
+ // long line (or a fullWidth Button) pushes straight out of the panel box.
196
+ // width:100% pins us to the panel instead; minWidth:0 lets text shrink.
197
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 3, style: { width: "100%", minWidth: 0 }, children: [
198
+ !status ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Loading…" }) : !status.enabled ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", style: WRAP, children: "Radio Cult uploads are switched off — check the plugin settings." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
199
+ /* @__PURE__ */ jsxRuntime.jsx(
200
+ designSystem.Typography,
201
+ {
202
+ variant: "pi",
203
+ textColor: status.trackId ? "success600" : "neutral600",
204
+ style: WRAP,
205
+ children: status.trackId ? `✓ Uploaded to Radio Cult${status.uploadedAt ? ` on ${new Date(status.uploadedAt).toLocaleDateString("en-GB")}` : ""}` : "Not uploaded to Radio Cult yet"
206
+ }
207
+ ),
208
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformLine, { label: "Mixcloud", status: status.platforms.mixcloud }),
209
+ /* @__PURE__ */ jsxRuntime.jsx(PlatformLine, { label: "SoundCloud", status: status.platforms.soundcloud }),
210
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {})
211
+ ] }),
212
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { fullWidth: true, onClick: () => window.open(uploaderHref, "_blank", "noopener"), children: "Open Episode Uploader" }),
213
+ status?.trackId ? /* @__PURE__ */ jsxRuntime.jsx(
214
+ designSystem.Button,
215
+ {
216
+ variant: "tertiary",
217
+ fullWidth: true,
218
+ loading: checking,
219
+ onClick: async () => {
220
+ setChecking(true);
221
+ await load(true);
222
+ setChecking(false);
223
+ },
224
+ children: "Check for links now"
225
+ }
226
+ ) : null,
227
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: WRAP, children: "Mixcloud/SoundCloud links are added to this entry automatically once Radio Cult finishes — reload this page to see them in the form fields." }),
228
+ message ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "danger600", style: WRAP, children: message }) : null
229
+ ] })
230
+ );
231
+ };
232
+ const PANEL_TITLE = "Upload to Soundcloud/​Mixcloud/​Radio Cult";
233
+ let configuredType;
234
+ let configPromise = null;
235
+ const ensureConfigLoaded = () => {
236
+ configPromise ?? (configPromise = fetchUiConfig().then(({ contentType }) => {
237
+ configuredType = contentType;
238
+ }).catch(() => {
239
+ configuredType = null;
240
+ }));
241
+ };
242
+ const RadioCultPanel = (props) => {
243
+ if (configuredType === void 0) {
244
+ ensureConfigLoaded();
245
+ return null;
246
+ }
247
+ if (configuredType !== props.model) return null;
248
+ if (!props.documentId) {
249
+ return {
250
+ title: PANEL_TITLE,
251
+ content: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", style: { ...WRAP, width: "100%" }, children: "Save the entry first — the uploader button appears here once it's saved." })
252
+ };
253
+ }
254
+ return {
255
+ title: PANEL_TITLE,
256
+ content: /* @__PURE__ */ jsxRuntime.jsx(PanelContent, { documentId: props.documentId })
257
+ };
258
+ };
259
+ const plugin = {
260
+ register(app) {
261
+ app.addMenuLink({
262
+ to: `plugins/${PLUGIN_ID}`,
263
+ icon: PluginIcon,
264
+ intlLabel: {
265
+ id: `${PLUGIN_ID}.plugin.name`,
266
+ defaultMessage: "Radio Cult Uploader"
267
+ },
268
+ Component: () => Promise.resolve().then(() => require("./App-DTOuR7aY.js")),
269
+ permissions: []
270
+ });
271
+ app.createSettingSection(
272
+ {
273
+ id: PLUGIN_ID,
274
+ intlLabel: {
275
+ id: `${PLUGIN_ID}.settings.section`,
276
+ defaultMessage: "Radio Cult Uploader"
277
+ }
278
+ },
279
+ [
280
+ {
281
+ intlLabel: {
282
+ id: `${PLUGIN_ID}.settings.link`,
283
+ defaultMessage: "Configuration"
284
+ },
285
+ id: `${PLUGIN_ID}-settings`,
286
+ to: `radiocult-uploader`,
287
+ Component: () => Promise.resolve().then(() => require("./Settings-DHv9SmRU.js")),
288
+ permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }]
289
+ }
290
+ ]
291
+ );
292
+ app.registerPlugin({
293
+ id: PLUGIN_ID,
294
+ initializer: Initializer,
295
+ isReady: false,
296
+ name: PLUGIN_ID
297
+ });
298
+ },
299
+ bootstrap(app) {
300
+ const contentManager = app.getPlugin("content-manager");
301
+ contentManager?.apis?.addEditViewSidePanel?.([RadioCultPanel]);
302
+ },
303
+ registerTrads({ locales }) {
304
+ return Promise.all(
305
+ locales.map(async (locale) => {
306
+ try {
307
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-D_61ojL7.js")) }), `./translations/${locale}.json`, 3);
308
+ const newData = {};
309
+ const keys = Object.keys(data);
310
+ for (const key of keys) {
311
+ newData[getTranslation(key)] = data[key];
312
+ }
313
+ return { data: newData, locale };
314
+ } catch {
315
+ return { data: {}, locale };
316
+ }
317
+ })
318
+ );
319
+ }
320
+ };
321
+ exports.PLATFORMS = PLATFORMS;
322
+ exports.PLATFORM_LABEL = PLATFORM_LABEL;
323
+ exports.apiError = apiError;
324
+ exports.fetchContentTypes = fetchContentTypes;
325
+ exports.fetchDiagnostics = fetchDiagnostics;
326
+ exports.fetchSettings = fetchSettings;
327
+ exports.fetchStatus = fetchStatus;
328
+ exports.fetchUiConfig = fetchUiConfig;
329
+ exports.plugin = plugin;
330
+ exports.publishTrack = publishTrack;
331
+ exports.saveSettings = saveSettings;
332
+ exports.saveTrackDetails = saveTrackDetails;
333
+ exports.testConnection = testConnection;
334
+ exports.uploadTrack = uploadTrack;
@@ -0,0 +1,318 @@
1
+ import * as React from "react";
2
+ import { useRef, useEffect } from "react";
3
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
4
+ import { PuzzlePiece } from "@strapi/icons";
5
+ import { Typography, Flex, Divider, Button } from "@strapi/design-system";
6
+ import { getFetchClient } from "@strapi/strapi/admin";
7
+ const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
8
+ const v = glob[path];
9
+ if (v) {
10
+ return typeof v === "function" ? v() : Promise.resolve(v);
11
+ }
12
+ return new Promise((_, reject) => {
13
+ (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
14
+ reject.bind(
15
+ null,
16
+ new Error(
17
+ "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
18
+ )
19
+ )
20
+ );
21
+ });
22
+ };
23
+ const PLUGIN_ID = "radiocult-uploader";
24
+ const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
25
+ const Initializer = ({ setPlugin }) => {
26
+ const ref = useRef(setPlugin);
27
+ useEffect(() => {
28
+ ref.current(PLUGIN_ID);
29
+ }, []);
30
+ return null;
31
+ };
32
+ const PluginIcon = () => /* @__PURE__ */ jsx(PuzzlePiece, {});
33
+ const base = `/${PLUGIN_ID}`;
34
+ const PLATFORMS = ["mixcloud", "soundcloud"];
35
+ const PLATFORM_LABEL = {
36
+ mixcloud: "Mixcloud",
37
+ soundcloud: "SoundCloud"
38
+ };
39
+ const apiError = (error) => error?.response?.data?.error ?? error?.message ?? "Something went wrong";
40
+ const fetchStatus = async (documentId, refresh = false) => {
41
+ const { get } = getFetchClient();
42
+ const { data } = await get(
43
+ `${base}/entries/${documentId}/status${refresh ? "?refresh=true" : ""}`
44
+ );
45
+ return data;
46
+ };
47
+ const publishTrack = async (documentId, platform, body, force = false) => {
48
+ const { post } = getFetchClient();
49
+ const { data } = await post(
50
+ `${base}/entries/${documentId}/publish/${platform}${force ? "?force=true" : ""}`,
51
+ body
52
+ );
53
+ return data;
54
+ };
55
+ const saveTrackDetails = async (documentId, meta) => {
56
+ const { put } = getFetchClient();
57
+ const { data } = await put(`${base}/entries/${documentId}/track`, meta);
58
+ return data;
59
+ };
60
+ const fetchUiConfig = async () => {
61
+ const { get } = getFetchClient();
62
+ const { data } = await get(`${base}/ui-config`);
63
+ return data;
64
+ };
65
+ const fetchSettings = async () => {
66
+ const { get } = getFetchClient();
67
+ const { data } = await get(`${base}/settings`);
68
+ return data;
69
+ };
70
+ const saveSettings = async (patch) => {
71
+ const { put } = getFetchClient();
72
+ const { data } = await put(`${base}/settings`, patch);
73
+ return data;
74
+ };
75
+ const testConnection = async () => {
76
+ const { post } = getFetchClient();
77
+ const { data } = await post(`${base}/settings/test-connection`, {});
78
+ return data;
79
+ };
80
+ const fetchContentTypes = async () => {
81
+ const { get } = getFetchClient();
82
+ const { data } = await get(`${base}/content-types`);
83
+ return data.contentTypes;
84
+ };
85
+ const fetchDiagnostics = async () => {
86
+ const { get } = getFetchClient();
87
+ const { data } = await get(`${base}/diagnostics`);
88
+ return data;
89
+ };
90
+ const uploadTrack = (documentId, file, meta, force, token, callbacks) => {
91
+ const backendURL = window.strapi?.backendURL ?? "";
92
+ const xhr = new XMLHttpRequest();
93
+ xhr.open(
94
+ "POST",
95
+ `${backendURL}${base}/entries/${encodeURIComponent(documentId)}/upload${force ? "?force=true" : ""}`
96
+ );
97
+ xhr.setRequestHeader("Authorization", `Bearer ${token}`);
98
+ xhr.responseType = "json";
99
+ xhr.timeout = 0;
100
+ xhr.upload.onprogress = (ev) => {
101
+ if (ev.lengthComputable) callbacks.onProgress(Math.round(ev.loaded / ev.total * 100));
102
+ };
103
+ xhr.upload.onload = () => callbacks.onProgress(null);
104
+ xhr.onload = () => {
105
+ const body = xhr.response ?? {};
106
+ if (xhr.status >= 200 && xhr.status < 300) callbacks.onDone(body);
107
+ else if (xhr.status === 409 && !force) {
108
+ callbacks.onConflict(body.error ?? "This entry already has a Radio Cult track.");
109
+ } else callbacks.onError(body.error ?? `HTTP ${xhr.status}`, xhr.status);
110
+ };
111
+ xhr.onerror = () => callbacks.onError("Network error — is the server reachable?");
112
+ xhr.ontimeout = () => callbacks.onError("Timed out");
113
+ xhr.onabort = () => callbacks.onAbort();
114
+ const fd = new FormData();
115
+ fd.append("title", meta.title);
116
+ fd.append("artist", meta.artist);
117
+ fd.append("album", meta.album);
118
+ fd.append("files", file, file.name);
119
+ xhr.send(fd);
120
+ return xhr;
121
+ };
122
+ const WRAP = { overflowWrap: "anywhere", minWidth: 0 };
123
+ const shortLink = (url) => {
124
+ let text = url;
125
+ try {
126
+ const parsed = new URL(url);
127
+ text = parsed.host.replace(/^www\./, "") + parsed.pathname.replace(/\/$/, "");
128
+ } catch {
129
+ }
130
+ return text.length > 40 ? `${text.slice(0, 39)}…` : text;
131
+ };
132
+ const PlatformLine = ({ label, status }) => /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 0, style: { minWidth: 0 }, children: [
133
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: label }),
134
+ status.state === "linked" && status.link ? /* @__PURE__ */ jsx(
135
+ "a",
136
+ {
137
+ href: status.link,
138
+ target: "_blank",
139
+ rel: "noreferrer",
140
+ title: status.link,
141
+ style: { display: "block", minWidth: 0 },
142
+ children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "primary600", ellipsis: true, children: shortLink(status.link) })
143
+ }
144
+ ) : /* @__PURE__ */ jsx(
145
+ Typography,
146
+ {
147
+ variant: "pi",
148
+ textColor: status.state === "stalled" ? "danger600" : "neutral600",
149
+ style: WRAP,
150
+ children: status.state === "publishing" ? "Publishing… link is saved automatically" : status.state === "stalled" ? "No link after 7 days — publish again from the uploader" : "Not published"
151
+ }
152
+ )
153
+ ] });
154
+ const PanelContent = ({ documentId }) => {
155
+ const uploaderHref = `/admin/plugins/${PLUGIN_ID}/upload/${documentId}`;
156
+ const [status, setStatus] = React.useState(null);
157
+ const [message, setMessage] = React.useState(null);
158
+ const [checking, setChecking] = React.useState(false);
159
+ const load = React.useCallback(
160
+ async (refresh = false) => {
161
+ try {
162
+ const data = await fetchStatus(documentId, refresh);
163
+ setStatus(data);
164
+ setMessage(data.rateLimitMessage ?? null);
165
+ } catch (error) {
166
+ setMessage(apiError(error));
167
+ }
168
+ },
169
+ [documentId]
170
+ );
171
+ React.useEffect(() => {
172
+ load();
173
+ }, [load]);
174
+ return (
175
+ // content-manager wraps every side panel in a Flex with
176
+ // alignItems="flex-start", so a child sizes to its own content and any
177
+ // long line (or a fullWidth Button) pushes straight out of the panel box.
178
+ // width:100% pins us to the panel instead; minWidth:0 lets text shrink.
179
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, style: { width: "100%", minWidth: 0 }, children: [
180
+ !status ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "Loading…" }) : !status.enabled ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", style: WRAP, children: "Radio Cult uploads are switched off — check the plugin settings." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
181
+ /* @__PURE__ */ jsx(
182
+ Typography,
183
+ {
184
+ variant: "pi",
185
+ textColor: status.trackId ? "success600" : "neutral600",
186
+ style: WRAP,
187
+ children: status.trackId ? `✓ Uploaded to Radio Cult${status.uploadedAt ? ` on ${new Date(status.uploadedAt).toLocaleDateString("en-GB")}` : ""}` : "Not uploaded to Radio Cult yet"
188
+ }
189
+ ),
190
+ /* @__PURE__ */ jsx(PlatformLine, { label: "Mixcloud", status: status.platforms.mixcloud }),
191
+ /* @__PURE__ */ jsx(PlatformLine, { label: "SoundCloud", status: status.platforms.soundcloud }),
192
+ /* @__PURE__ */ jsx(Divider, {})
193
+ ] }),
194
+ /* @__PURE__ */ jsx(Button, { fullWidth: true, onClick: () => window.open(uploaderHref, "_blank", "noopener"), children: "Open Episode Uploader" }),
195
+ status?.trackId ? /* @__PURE__ */ jsx(
196
+ Button,
197
+ {
198
+ variant: "tertiary",
199
+ fullWidth: true,
200
+ loading: checking,
201
+ onClick: async () => {
202
+ setChecking(true);
203
+ await load(true);
204
+ setChecking(false);
205
+ },
206
+ children: "Check for links now"
207
+ }
208
+ ) : null,
209
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: WRAP, children: "Mixcloud/SoundCloud links are added to this entry automatically once Radio Cult finishes — reload this page to see them in the form fields." }),
210
+ message ? /* @__PURE__ */ jsx(Typography, { textColor: "danger600", style: WRAP, children: message }) : null
211
+ ] })
212
+ );
213
+ };
214
+ const PANEL_TITLE = "Upload to Soundcloud/​Mixcloud/​Radio Cult";
215
+ let configuredType;
216
+ let configPromise = null;
217
+ const ensureConfigLoaded = () => {
218
+ configPromise ?? (configPromise = fetchUiConfig().then(({ contentType }) => {
219
+ configuredType = contentType;
220
+ }).catch(() => {
221
+ configuredType = null;
222
+ }));
223
+ };
224
+ const RadioCultPanel = (props) => {
225
+ if (configuredType === void 0) {
226
+ ensureConfigLoaded();
227
+ return null;
228
+ }
229
+ if (configuredType !== props.model) return null;
230
+ if (!props.documentId) {
231
+ return {
232
+ title: PANEL_TITLE,
233
+ content: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", style: { ...WRAP, width: "100%" }, children: "Save the entry first — the uploader button appears here once it's saved." })
234
+ };
235
+ }
236
+ return {
237
+ title: PANEL_TITLE,
238
+ content: /* @__PURE__ */ jsx(PanelContent, { documentId: props.documentId })
239
+ };
240
+ };
241
+ const plugin = {
242
+ register(app) {
243
+ app.addMenuLink({
244
+ to: `plugins/${PLUGIN_ID}`,
245
+ icon: PluginIcon,
246
+ intlLabel: {
247
+ id: `${PLUGIN_ID}.plugin.name`,
248
+ defaultMessage: "Radio Cult Uploader"
249
+ },
250
+ Component: () => import("./App-Cd-TZrqC.mjs"),
251
+ permissions: []
252
+ });
253
+ app.createSettingSection(
254
+ {
255
+ id: PLUGIN_ID,
256
+ intlLabel: {
257
+ id: `${PLUGIN_ID}.settings.section`,
258
+ defaultMessage: "Radio Cult Uploader"
259
+ }
260
+ },
261
+ [
262
+ {
263
+ intlLabel: {
264
+ id: `${PLUGIN_ID}.settings.link`,
265
+ defaultMessage: "Configuration"
266
+ },
267
+ id: `${PLUGIN_ID}-settings`,
268
+ to: `radiocult-uploader`,
269
+ Component: () => import("./Settings-B3cZR7VF.mjs"),
270
+ permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }]
271
+ }
272
+ ]
273
+ );
274
+ app.registerPlugin({
275
+ id: PLUGIN_ID,
276
+ initializer: Initializer,
277
+ isReady: false,
278
+ name: PLUGIN_ID
279
+ });
280
+ },
281
+ bootstrap(app) {
282
+ const contentManager = app.getPlugin("content-manager");
283
+ contentManager?.apis?.addEditViewSidePanel?.([RadioCultPanel]);
284
+ },
285
+ registerTrads({ locales }) {
286
+ return Promise.all(
287
+ locales.map(async (locale) => {
288
+ try {
289
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-CTCtUg23.mjs") }), `./translations/${locale}.json`, 3);
290
+ const newData = {};
291
+ const keys = Object.keys(data);
292
+ for (const key of keys) {
293
+ newData[getTranslation(key)] = data[key];
294
+ }
295
+ return { data: newData, locale };
296
+ } catch {
297
+ return { data: {}, locale };
298
+ }
299
+ })
300
+ );
301
+ }
302
+ };
303
+ export {
304
+ PLATFORMS as P,
305
+ PLATFORM_LABEL as a,
306
+ apiError as b,
307
+ fetchUiConfig as c,
308
+ fetchSettings as d,
309
+ fetchContentTypes as e,
310
+ fetchStatus as f,
311
+ fetchDiagnostics as g,
312
+ saveSettings as h,
313
+ plugin as i,
314
+ publishTrack as p,
315
+ saveTrackDetails as s,
316
+ testConnection as t,
317
+ uploadTrack as u
318
+ };
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
+ const index = require("./index-BLAtUo_j.js");
4
+ exports.default = index.plugin;
@@ -0,0 +1,4 @@
1
+ import { i } from "./index-Dj3e3A79.mjs";
2
+ export {
3
+ i as default
4
+ };