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,534 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const designSystem = require("@strapi/design-system");
6
+ const index = require("./index-BLAtUo_j.js");
7
+ const tokens = require("./tokens-CDeNcvZq.js");
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
11
+ if (e) {
12
+ for (const k in e) {
13
+ if (k !== "default") {
14
+ const d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: () => e[k]
18
+ });
19
+ }
20
+ }
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
26
+ const MAPPING_FIELDS = [
27
+ { key: "title", label: "Title", hint: "Track title on Radio Cult", types: ["string", "text"], required: true },
28
+ { key: "image", label: "Artwork image", hint: "Embedded in mp3s and sent to Mixcloud/SoundCloud", types: ["media"] },
29
+ { key: "broadcastDate", label: "Broadcast date", hint: 'Used for the default album name ("August 2026")', types: ["date", "datetime"] },
30
+ { key: "soundcloudLink", label: "SoundCloud link", hint: "The published SoundCloud URL is written here", types: ["string", "text"] },
31
+ { key: "mixcloudLink", label: "Mixcloud link", hint: "The published Mixcloud URL is written here", types: ["string", "text"] },
32
+ { key: "tagsRelation", label: "Tags / genres relation", hint: "Relation whose entries become publish tags", types: ["relation"] },
33
+ { key: "showRelation", label: "Show relation", hint: "Optional — used for the uploader tab title", types: ["relation"] }
34
+ ];
35
+ const isHex = (value) => /^#[0-9a-fA-F]{6}$/.test(value);
36
+ const ColorField = ({
37
+ label,
38
+ hint,
39
+ value,
40
+ overridden,
41
+ onChange
42
+ }) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
43
+ /* @__PURE__ */ jsxRuntime.jsx(
44
+ "input",
45
+ {
46
+ type: "color",
47
+ "aria-label": label,
48
+ value: isHex(value) ? value : "#000000",
49
+ style: {
50
+ width: 34,
51
+ height: 34,
52
+ flex: "none",
53
+ padding: 0,
54
+ border: "1px solid rgba(0,0,0,.2)",
55
+ borderRadius: 6,
56
+ background: "none",
57
+ cursor: "pointer"
58
+ },
59
+ onChange: (e) => onChange(e.target.value)
60
+ }
61
+ ),
62
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { minWidth: 0, flex: 1 }, children: [
63
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: overridden ? "bold" : void 0, ellipsis: true, title: hint, children: label }),
64
+ /* @__PURE__ */ jsxRuntime.jsx(
65
+ "input",
66
+ {
67
+ type: "text",
68
+ "aria-label": `${label} hex value`,
69
+ value,
70
+ style: {
71
+ width: "100%",
72
+ fontSize: 11,
73
+ fontFamily: "monospace",
74
+ border: "1px solid rgba(0,0,0,.15)",
75
+ borderRadius: 4,
76
+ padding: "2px 6px"
77
+ },
78
+ onChange: (e) => onChange(e.target.value)
79
+ }
80
+ )
81
+ ] })
82
+ ] });
83
+ const Section = ({ title, children }) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "neutral0", hasRadius: true, shadow: "filterShadow", padding: 6, marginBottom: 6, children: [
84
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", tag: "h2", children: title }),
85
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 4, children })
86
+ ] });
87
+ const Settings = () => {
88
+ const [settings, setSettings] = React__namespace.useState(null);
89
+ const [contentTypes, setContentTypes] = React__namespace.useState([]);
90
+ const [diagnostics, setDiagnostics] = React__namespace.useState(null);
91
+ const [apiKeyInput, setApiKeyInput] = React__namespace.useState("");
92
+ const [saving, setSaving] = React__namespace.useState(false);
93
+ const [testing, setTesting] = React__namespace.useState(false);
94
+ const [message, setMessage] = React__namespace.useState(null);
95
+ const [testResult, setTestResult] = React__namespace.useState(null);
96
+ const [dirty, setDirty] = React__namespace.useState(false);
97
+ const [loadError, setLoadError] = React__namespace.useState(null);
98
+ tokens.useGoogleFont(tokens.googleFontHref(tokens.resolveFont(settings?.theme)));
99
+ React__namespace.useEffect(() => {
100
+ Promise.all([index.fetchSettings(), index.fetchContentTypes(), index.fetchDiagnostics()]).then(([s, cts, diag]) => {
101
+ setSettings(s);
102
+ setContentTypes(cts);
103
+ setDiagnostics(diag);
104
+ }).catch((error) => setLoadError(index.apiError(error)));
105
+ }, []);
106
+ if (loadError) {
107
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "danger600", children: loadError }) });
108
+ }
109
+ if (!settings) {
110
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Loading…" }) });
111
+ }
112
+ const selectedType = contentTypes.find((ct) => ct.uid === settings.mapping.contentType);
113
+ const patch = (path, value) => {
114
+ setDirty(true);
115
+ setSettings((current) => {
116
+ if (!current) return current;
117
+ const next = structuredClone(current);
118
+ const keys = path.split(".");
119
+ let obj = next;
120
+ for (const key of keys.slice(0, -1)) obj = obj[key];
121
+ obj[keys[keys.length - 1]] = value;
122
+ return next;
123
+ });
124
+ };
125
+ const setMappingType = (uid) => {
126
+ setDirty(true);
127
+ const ct = contentTypes.find((c) => c.uid === uid);
128
+ const auto = (kinds, nameHints) => {
129
+ const byName = ct?.attributes.find((a) => kinds.includes(a.type) && nameHints.test(a.name));
130
+ return byName?.name ?? null;
131
+ };
132
+ setSettings(
133
+ (current) => current ? {
134
+ ...current,
135
+ mapping: {
136
+ contentType: uid,
137
+ title: auto(["string", "text"], /title|name/i),
138
+ image: auto(["media"], /image|artwork|cover|photo/i) ?? auto(["media"], /./),
139
+ broadcastDate: auto(["date", "datetime"], /broadcast|date|air/i),
140
+ soundcloudLink: auto(["string", "text"], /soundcloud/i),
141
+ mixcloudLink: auto(["string", "text"], /mixcloud/i),
142
+ tagsRelation: auto(["relation"], /tag|genre/i),
143
+ tagsLabelField: null,
144
+ showRelation: auto(["relation"], /show|series|program/i),
145
+ showLabelField: null
146
+ }
147
+ } : current
148
+ );
149
+ };
150
+ const save = async () => {
151
+ setSaving(true);
152
+ setMessage(null);
153
+ try {
154
+ const payload = {
155
+ credentials: {
156
+ stationId: settings.credentials.stationId,
157
+ enabled: settings.credentials.enabled,
158
+ ...apiKeyInput ? { apiKey: apiKeyInput } : {}
159
+ },
160
+ mapping: settings.mapping,
161
+ defaults: settings.defaults,
162
+ theme: { ...settings.theme, overrides: tokens.pickEditableOverrides(settings.theme.overrides) }
163
+ };
164
+ const next = await index.saveSettings(payload);
165
+ setSettings(next);
166
+ setApiKeyInput("");
167
+ setDirty(false);
168
+ setMessage({ ok: true, text: "Settings saved" });
169
+ } catch (error) {
170
+ setMessage({ ok: false, text: index.apiError(error) });
171
+ } finally {
172
+ setSaving(false);
173
+ }
174
+ };
175
+ const runTest = async () => {
176
+ setTesting(true);
177
+ setMessage(null);
178
+ setTestResult(null);
179
+ try {
180
+ const result = await index.testConnection();
181
+ setTestResult({ ok: result.ok, text: result.message });
182
+ } catch (error) {
183
+ setTestResult({ ok: false, text: index.apiError(error) });
184
+ } finally {
185
+ setTesting(false);
186
+ }
187
+ };
188
+ const unsavedChanges = dirty || apiKeyInput !== "";
189
+ const targetAttributesFor = (relationKey) => {
190
+ const relationField = settings.mapping[relationKey];
191
+ const attr = selectedType?.attributes.find((a) => a.name === relationField);
192
+ return attr?.targetAttributes ?? [];
193
+ };
194
+ const effectiveTokens = tokens.resolveTokens(settings.theme);
195
+ const previewVars = tokens.tokensToCssVars(effectiveTokens);
196
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { padding: 8, style: { maxWidth: 960 }, children: [
197
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-start", paddingBottom: 6, children: [
198
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
199
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", children: "Radio Cult Uploader" }),
200
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: "Credentials, field mapping and uploader theme" })
201
+ ] }),
202
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: save, loading: saving, size: "L", children: "Save" })
203
+ ] }),
204
+ message ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: message.ok ? "success600" : "danger600", fontWeight: "semiBold", children: message.text }) }) : null,
205
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Radio Cult credentials", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
206
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(
207
+ designSystem.Field.Root,
208
+ {
209
+ hint: settings.envLocks.apiKey ? "Set by RADIOCULT_API_KEY — the environment value wins" : settings.credentials.apiKeySet ? "A key is stored — leave blank to keep it" : "From your Radio Cult station dashboard",
210
+ children: [
211
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "API key" }),
212
+ /* @__PURE__ */ jsxRuntime.jsx(
213
+ designSystem.TextInput,
214
+ {
215
+ type: "password",
216
+ placeholder: settings.credentials.apiKeySet ? "••••••••••••" : "",
217
+ value: apiKeyInput,
218
+ disabled: settings.envLocks.apiKey,
219
+ onChange: (e) => setApiKeyInput(e.target.value)
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
223
+ ]
224
+ }
225
+ ) }),
226
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(
227
+ designSystem.Field.Root,
228
+ {
229
+ hint: settings.envLocks.stationId ? "Set by RADIOCULT_STATION_ID — the environment value wins" : "Your station’s ID on Radio Cult",
230
+ children: [
231
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Station ID" }),
232
+ /* @__PURE__ */ jsxRuntime.jsx(
233
+ designSystem.TextInput,
234
+ {
235
+ value: settings.credentials.stationId,
236
+ disabled: settings.envLocks.stationId,
237
+ onChange: (e) => patch("credentials.stationId", e.target.value)
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
241
+ ]
242
+ }
243
+ ) }),
244
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(
245
+ designSystem.Field.Root,
246
+ {
247
+ hint: settings.envLocks.enabled ? "Set by RADIOCULT_ENABLED — the environment value wins" : "Turn the whole integration on or off",
248
+ children: [
249
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Enabled" }),
250
+ /* @__PURE__ */ jsxRuntime.jsx(
251
+ designSystem.Toggle,
252
+ {
253
+ onLabel: "On",
254
+ offLabel: "Off",
255
+ checked: settings.credentials.enabled,
256
+ disabled: settings.envLocks.enabled,
257
+ onChange: (e) => patch("credentials.enabled", e.target.checked)
258
+ }
259
+ ),
260
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
261
+ ]
262
+ }
263
+ ) }),
264
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "flex-start", justifyContent: "flex-end", children: [
265
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", wrap: "wrap", children: [
266
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", onClick: runTest, loading: testing, children: "Test connection" }),
267
+ testResult ? /* @__PURE__ */ jsxRuntime.jsx(
268
+ designSystem.Typography,
269
+ {
270
+ textColor: testResult.ok ? "success600" : "danger600",
271
+ fontWeight: "semiBold",
272
+ children: testResult.text
273
+ }
274
+ ) : null
275
+ ] }),
276
+ testResult && !testResult.ok ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: unsavedChanges ? "You have unsaved changes — the test uses the saved API key and station ID, so hit Save first, then test again." : "The test uses the saved API key and station ID. If you have just changed them, save your settings before testing again." }) }) : null
277
+ ] })
278
+ ] }) }),
279
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Field mapping", children: [
280
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Tell the uploader which content type holds your episodes and which fields to use. Fields are auto-matched when you pick a content type — check them before saving." }) }),
281
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
282
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { required: true, children: [
283
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Episode content type" }),
284
+ /* @__PURE__ */ jsxRuntime.jsx(
285
+ designSystem.SingleSelect,
286
+ {
287
+ value: settings.mapping.contentType ?? "",
288
+ onChange: (value) => setMappingType(String(value)),
289
+ placeholder: "Pick a content type",
290
+ children: contentTypes.map((ct) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: ct.uid, children: ct.displayName }, ct.uid))
291
+ }
292
+ )
293
+ ] }) }),
294
+ selectedType ? MAPPING_FIELDS.map((field) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: field.hint, required: field.required, children: [
295
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: field.label }),
296
+ /* @__PURE__ */ jsxRuntime.jsxs(
297
+ designSystem.SingleSelect,
298
+ {
299
+ value: settings.mapping[field.key] ?? "",
300
+ onChange: (value) => patch(`mapping.${field.key}`, value === "" ? null : String(value)),
301
+ placeholder: "—",
302
+ children: [
303
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "", children: "— not used —" }),
304
+ selectedType.attributes.filter((a) => field.types.includes(a.type)).map((a) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: a.name, children: a.name }, a.name))
305
+ ]
306
+ }
307
+ ),
308
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
309
+ ] }) }, field.key)) : null,
310
+ selectedType && settings.mapping.tagsRelation ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Which field on the tag entries holds the tag text", children: [
311
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Tag label field" }),
312
+ /* @__PURE__ */ jsxRuntime.jsx(
313
+ designSystem.SingleSelect,
314
+ {
315
+ value: settings.mapping.tagsLabelField ?? "",
316
+ onChange: (value) => patch("mapping.tagsLabelField", value === "" ? null : String(value)),
317
+ placeholder: "—",
318
+ children: targetAttributesFor("tagsRelation").map((name) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: name, children: name }, name))
319
+ }
320
+ ),
321
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
322
+ ] }) }) : null,
323
+ selectedType && settings.mapping.showRelation ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Which field on the show holds its name", children: [
324
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Show name field" }),
325
+ /* @__PURE__ */ jsxRuntime.jsx(
326
+ designSystem.SingleSelect,
327
+ {
328
+ value: settings.mapping.showLabelField ?? "",
329
+ onChange: (value) => patch("mapping.showLabelField", value === "" ? null : String(value)),
330
+ placeholder: "—",
331
+ children: targetAttributesFor("showRelation").map((name) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: name, children: name }, name))
332
+ }
333
+ ),
334
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
335
+ ] }) }) : null
336
+ ] })
337
+ ] }),
338
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Publish defaults", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
339
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Default track artist (e.g. your station name)", children: [
340
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Artist" }),
341
+ /* @__PURE__ */ jsxRuntime.jsx(
342
+ designSystem.TextInput,
343
+ {
344
+ value: settings.defaults.artist,
345
+ onChange: (e) => patch("defaults.artist", e.target.value)
346
+ }
347
+ ),
348
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
349
+ ] }) }),
350
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Default description for Mixcloud / SoundCloud publishes", children: [
351
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Description" }),
352
+ /* @__PURE__ */ jsxRuntime.jsx(
353
+ designSystem.Textarea,
354
+ {
355
+ value: settings.defaults.description,
356
+ onChange: (e) => patch("defaults.description", e.target.value)
357
+ }
358
+ ),
359
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
360
+ ] }) })
361
+ ] }) }),
362
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Uploader theme", children: [
363
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
364
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
365
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Preset" }),
366
+ /* @__PURE__ */ jsxRuntime.jsx(
367
+ designSystem.SingleSelect,
368
+ {
369
+ value: settings.theme.preset,
370
+ onChange: (value) => {
371
+ patch("theme.preset", String(value));
372
+ patch("theme.overrides", {});
373
+ },
374
+ children: Object.entries(tokens.PRESETS).map(([key, preset]) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: key, children: preset.label }, key))
375
+ }
376
+ )
377
+ ] }) }),
378
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Google Fonts are loaded on the uploader page as needed", children: [
379
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Font" }),
380
+ /* @__PURE__ */ jsxRuntime.jsx(
381
+ designSystem.SingleSelect,
382
+ {
383
+ value: settings.theme.fontFamily ?? "system",
384
+ onChange: (value) => patch("theme.fontFamily", String(value)),
385
+ children: Object.entries(tokens.FONTS).map(([key, font]) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: key, children: font.label }, key))
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
389
+ ] }) }),
390
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Shown under the page heading and in the tab", children: [
391
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Station name" }),
392
+ /* @__PURE__ */ jsxRuntime.jsx(
393
+ designSystem.TextInput,
394
+ {
395
+ value: settings.theme.stationName,
396
+ onChange: (e) => patch("theme.stationName", e.target.value)
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
400
+ ] }) }),
401
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, s: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { hint: "Upload it in the Media Library, then paste its URL here", children: [
402
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Logo URL" }),
403
+ /* @__PURE__ */ jsxRuntime.jsx(
404
+ designSystem.TextInput,
405
+ {
406
+ value: settings.theme.logoUrl ?? "",
407
+ placeholder: "/uploads/logo.svg",
408
+ onChange: (e) => patch("theme.logoUrl", e.target.value || null)
409
+ }
410
+ ),
411
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
412
+ ] }) })
413
+ ] }),
414
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingTop: 4, children: [
415
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "Colours" }),
416
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Tweak the handful of colours the uploader is built from — everything else (cards, borders, shadows, status colours) follows the preset." }) }),
417
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Root, { gap: 3, children: tokens.EDITABLE_TOKENS.map(({ key, label, hint }) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, s: 6, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsx(
418
+ ColorField,
419
+ {
420
+ label,
421
+ hint,
422
+ value: effectiveTokens[key],
423
+ overridden: settings.theme.overrides?.[key] !== void 0,
424
+ onChange: (value) => patch("theme.overrides", {
425
+ ...tokens.pickEditableOverrides(settings.theme.overrides),
426
+ [key]: value
427
+ })
428
+ }
429
+ ) }, key)) }) }),
430
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 3, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: () => patch("theme.overrides", {}), children: "Reset colours to preset" }) })
431
+ ] }),
432
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingTop: 6, children: [
433
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "Preview" }),
434
+ /* @__PURE__ */ jsxRuntime.jsxs(
435
+ "div",
436
+ {
437
+ style: {
438
+ ...previewVars,
439
+ marginTop: 8,
440
+ borderRadius: 12,
441
+ padding: 24,
442
+ fontFamily: tokens.resolveFont(settings.theme).stack,
443
+ background: `linear-gradient(160deg, ${effectiveTokens.pageBg}, ${effectiveTokens.pageBgDeep})`,
444
+ color: effectiveTokens.text
445
+ },
446
+ children: [
447
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 14, marginBottom: 16 }, children: [
448
+ settings.theme.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: settings.theme.logoUrl, alt: "logo", style: { width: 44, height: 44 } }) : /* @__PURE__ */ jsxRuntime.jsx(
449
+ "div",
450
+ {
451
+ style: {
452
+ width: 44,
453
+ height: 44,
454
+ borderRadius: "50%",
455
+ border: `3px solid ${effectiveTokens.text}`
456
+ }
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
460
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 800, fontSize: 18 }, children: "Episode Uploader" }),
461
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 12, color: effectiveTokens.textDim }, children: [
462
+ settings.theme.stationName || "Your station",
463
+ " · Example episode"
464
+ ] })
465
+ ] })
466
+ ] }),
467
+ /* @__PURE__ */ jsxRuntime.jsxs(
468
+ "div",
469
+ {
470
+ style: {
471
+ background: effectiveTokens.card,
472
+ border: `1px solid ${effectiveTokens.cardBorder}`,
473
+ borderRadius: 14,
474
+ padding: 18,
475
+ display: "flex",
476
+ alignItems: "center",
477
+ gap: 14
478
+ },
479
+ children: [
480
+ /* @__PURE__ */ jsxRuntime.jsx(
481
+ "div",
482
+ {
483
+ style: {
484
+ width: 30,
485
+ height: 30,
486
+ borderRadius: "50%",
487
+ display: "grid",
488
+ placeItems: "center",
489
+ fontWeight: 800,
490
+ fontSize: 13,
491
+ color: effectiveTokens.buttonText,
492
+ background: `linear-gradient(135deg, ${effectiveTokens.text}, ${effectiveTokens.contrast})`
493
+ },
494
+ children: "1"
495
+ }
496
+ ),
497
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 700, fontSize: 14 }, children: "Track details" }),
498
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { marginLeft: "auto", display: "flex", gap: 8 }, children: [
499
+ /* @__PURE__ */ jsxRuntime.jsx(
500
+ "button",
501
+ {
502
+ type: "button",
503
+ style: {
504
+ fontSize: 12,
505
+ fontWeight: 700,
506
+ color: effectiveTokens.buttonText,
507
+ background: `linear-gradient(135deg, ${effectiveTokens.text}, ${effectiveTokens.contrast})`,
508
+ border: "none",
509
+ borderRadius: 9,
510
+ padding: "8px 14px",
511
+ boxShadow: `0 4px 16px ${effectiveTokens.shadow}`
512
+ },
513
+ children: "Upload to Radio Cult"
514
+ }
515
+ ),
516
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: effectiveTokens.green, fontWeight: 600, alignSelf: "center" }, children: "✓ done" }),
517
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: effectiveTokens.amber, alignSelf: "center" }, children: "publishing…" })
518
+ ] })
519
+ ]
520
+ }
521
+ )
522
+ ]
523
+ }
524
+ )
525
+ ] })
526
+ ] }),
527
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Diagnostics", children: diagnostics ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
528
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: diagnostics.openAsBlob ? "success600" : "danger600", children: diagnostics.openAsBlob ? `✓ Node ${diagnostics.nodeVersion} supports streamed uploads` : `✗ Node ${diagnostics.nodeVersion} is too old — uploads need Node 20+` }),
529
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: diagnostics.bodyLimitOk ? "success600" : "warning600", children: diagnostics.bodyLimitOk ? "✓ Request body limit allows 750MB uploads" : `✗ The app's strapi::body maxFileSize is ${diagnostics.bodyMaxFileSize ? `${Math.round(diagnostics.bodyMaxFileSize / 1024 / 1024)}MB` : "unknown"} — raise it to 800MB in config/middlewares.ts to allow full-size uploads (see the plugin README)` }),
530
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: diagnostics.cronEnabled ? "success600" : "warning600", children: diagnostics.cronEnabled ? "✓ Cron is enabled — links are synced every 10 minutes" : "✗ Cron is disabled — published links are only picked up when a page checks" })
531
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Loading…" }) })
532
+ ] });
533
+ };
534
+ exports.default = Settings;
@@ -0,0 +1,8 @@
1
+ const en = {
2
+ "plugin.name": "Radio Cult Uploader",
3
+ "settings.section": "Radio Cult Uploader",
4
+ "settings.link": "Configuration"
5
+ };
6
+ export {
7
+ en as default
8
+ };
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const en = {
4
+ "plugin.name": "Radio Cult Uploader",
5
+ "settings.section": "Radio Cult Uploader",
6
+ "settings.link": "Configuration"
7
+ };
8
+ exports.default = en;