@jjlmoya/utils-streaming 1.21.0 → 1.22.1

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/seo_translation_completeness.test.ts +69 -0
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/streamSceneCountdownClock/bibliography.astro +6 -0
  8. package/src/tool/streamSceneCountdownClock/bibliography.ts +12 -0
  9. package/src/tool/streamSceneCountdownClock/component.astro +180 -0
  10. package/src/tool/streamSceneCountdownClock/controller.ts +271 -0
  11. package/src/tool/streamSceneCountdownClock/design-state.ts +81 -0
  12. package/src/tool/streamSceneCountdownClock/dom-views.ts +53 -0
  13. package/src/tool/streamSceneCountdownClock/entry.ts +29 -0
  14. package/src/tool/streamSceneCountdownClock/evaluator.ts +28 -0
  15. package/src/tool/streamSceneCountdownClock/i18n/de.ts +47 -0
  16. package/src/tool/streamSceneCountdownClock/i18n/en.ts +253 -0
  17. package/src/tool/streamSceneCountdownClock/i18n/es.ts +47 -0
  18. package/src/tool/streamSceneCountdownClock/i18n/fr.ts +47 -0
  19. package/src/tool/streamSceneCountdownClock/i18n/id.ts +47 -0
  20. package/src/tool/streamSceneCountdownClock/i18n/it.ts +44 -0
  21. package/src/tool/streamSceneCountdownClock/i18n/ja.ts +44 -0
  22. package/src/tool/streamSceneCountdownClock/i18n/ko.ts +44 -0
  23. package/src/tool/streamSceneCountdownClock/i18n/locale-factory.ts +50 -0
  24. package/src/tool/streamSceneCountdownClock/i18n/nl.ts +44 -0
  25. package/src/tool/streamSceneCountdownClock/i18n/pl.ts +44 -0
  26. package/src/tool/streamSceneCountdownClock/i18n/pt.ts +44 -0
  27. package/src/tool/streamSceneCountdownClock/i18n/ru.ts +44 -0
  28. package/src/tool/streamSceneCountdownClock/i18n/sv.ts +44 -0
  29. package/src/tool/streamSceneCountdownClock/i18n/tr.ts +44 -0
  30. package/src/tool/streamSceneCountdownClock/i18n/zh.ts +44 -0
  31. package/src/tool/streamSceneCountdownClock/index.ts +13 -0
  32. package/src/tool/streamSceneCountdownClock/logic.test.ts +51 -0
  33. package/src/tool/streamSceneCountdownClock/logic.ts +108 -0
  34. package/src/tool/streamSceneCountdownClock/seo.astro +15 -0
  35. package/src/tool/streamSceneCountdownClock/settings-state.ts +24 -0
  36. package/src/tool/streamSceneCountdownClock/storage.ts +32 -0
  37. package/src/tool/streamSceneCountdownClock/stream-scene-countdown-clock.css +1745 -0
  38. package/src/tool/streamSceneCountdownClock/stream-url-view.ts +32 -0
  39. package/src/tool/streamSceneCountdownClock/text-input-state.ts +31 -0
  40. package/src/tool/streamSceneCountdownClock/ui.ts +69 -0
  41. package/src/tool/streamSceneCountdownClock/url-state.test.ts +48 -0
  42. package/src/tool/streamSceneCountdownClock/url-state.ts +118 -0
  43. package/src/tools.ts +2 -0
@@ -0,0 +1,32 @@
1
+ import { buildStreamingUrl } from './url-state';
2
+ import type { SavedClockSettings } from './storage';
3
+ import type { StreamSceneCountdownClockUI } from './ui';
4
+
5
+ function getElement<T extends HTMLElement>(root: HTMLElement, selector: string): T | null {
6
+ return root.querySelector<T>(selector);
7
+ }
8
+
9
+ export function renderStreamingUrl(root: HTMLElement, settings: SavedClockSettings): void {
10
+ const element = getElement<HTMLAnchorElement>(root, '[data-stream-url]');
11
+ if (!element) return;
12
+ const url = buildStreamingUrl(settings, window.location.origin, window.location.pathname);
13
+ element.href = url;
14
+ element.textContent = url;
15
+ }
16
+
17
+ export function bindCopyUrlButton(root: HTMLElement, settings: () => SavedClockSettings, ui: StreamSceneCountdownClockUI): void {
18
+ const button = getElement<HTMLButtonElement>(root, '[data-copy-url]');
19
+ if (!button) return;
20
+ button.addEventListener('click', async () => {
21
+ const current = settings();
22
+ const url = buildStreamingUrl(current, window.location.origin, window.location.pathname);
23
+ try {
24
+ await navigator.clipboard.writeText(url);
25
+ } catch {}
26
+ const stateElement = getElement<HTMLElement>(root, '[data-copy-state]');
27
+ if (stateElement) stateElement.textContent = ui.copiedUrlText;
28
+ window.setTimeout(() => {
29
+ if (stateElement) stateElement.textContent = '';
30
+ }, 1800);
31
+ });
32
+ }
@@ -0,0 +1,31 @@
1
+ import { collectSettings } from './settings-state';
2
+ import { saveSettings, type SavedClockSettings } from './storage';
3
+ import type { StreamSceneCountdownClockUI } from './ui';
4
+
5
+ interface InputState {
6
+ settings: SavedClockSettings;
7
+ startAtMs: number | null;
8
+ }
9
+
10
+ interface InputContext {
11
+ root: HTMLElement;
12
+ state: InputState;
13
+ ui: StreamSceneCountdownClockUI;
14
+ render: (root: HTMLElement, state: InputState, ui: StreamSceneCountdownClockUI) => void;
15
+ }
16
+
17
+ function bindTextInput(context: InputContext, selector: string): void {
18
+ const element = context.root.querySelector<HTMLInputElement>(selector);
19
+ element?.addEventListener('input', () => {
20
+ context.state.settings = collectSettings(context.root, context.state.settings);
21
+ saveSettings(context.state.settings);
22
+ context.render(context.root, context.state, context.ui);
23
+ });
24
+ }
25
+
26
+ export function bindTextInputs(context: InputContext): void {
27
+ bindTextInput(context, '[data-scene-title]');
28
+ bindTextInput(context, '[data-message]');
29
+ bindTextInput(context, '[data-duration-input]');
30
+ bindTextInput(context, '[data-start-time-input]');
31
+ }
@@ -0,0 +1,69 @@
1
+ export type SceneKey = 'brb' | 'starting' | 'raid' | 'intermission';
2
+ export type DesignKey = 'aurora' | 'type' | 'pulse' | 'glitch' | 'sunset';
3
+
4
+ export interface StreamSceneCountdownClockUI extends Record<string, string | string[]> {
5
+ sceneLabel: string;
6
+ sceneBrb: string;
7
+ sceneStarting: string;
8
+ sceneRaid: string;
9
+ sceneIntermission: string;
10
+ sceneTitleLabel: string;
11
+ sceneTitlePlaceholder: string;
12
+ designLabel: string;
13
+ designAurora: string;
14
+ designType: string;
15
+ designPulse: string;
16
+ designGlitch: string;
17
+ designSunset: string;
18
+ accentColorLabel: string;
19
+ glowColorLabel: string;
20
+ messageLabel: string;
21
+ messagePlaceholder: string;
22
+ durationLabel: string;
23
+ duration60: string;
24
+ duration300: string;
25
+ duration600: string;
26
+ durationCustom: string;
27
+ secondsLabel: string;
28
+ startLabel: string;
29
+ startNow: string;
30
+ scheduleTime: string;
31
+ timeLabel: string;
32
+ startAction: string;
33
+ focusAction: string;
34
+ exitFocusAction: string;
35
+ resetAction: string;
36
+ flowText: string;
37
+ obsTitle: string;
38
+ obsText: string;
39
+ obsStepCopy: string;
40
+ obsStepAdd: string;
41
+ obsStepPaste: string;
42
+ copyUrlAction: string;
43
+ copiedUrlText: string;
44
+ streamUrlAria: string;
45
+ previewTitle: string;
46
+ previewHint: string;
47
+ previewAria: string;
48
+ stageEyebrow: string;
49
+ stageCaption: string;
50
+ readyBadge: string;
51
+ waitingBadge: string;
52
+ liveBadge: string;
53
+ endedBadge: string;
54
+ readyText: string;
55
+ waitingText: string;
56
+ liveText: string;
57
+ endedText: string;
58
+ remainingLabel: string;
59
+ startTimeLabel: string;
60
+ endTimeLabel: string;
61
+ progressLabel: string;
62
+ assumptionTitle: string;
63
+ assumptionText: string;
64
+ warningTitle: string;
65
+ warningText: string;
66
+ invalidTime: string;
67
+ clockAria: string;
68
+ statusAria: string;
69
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildStreamingUrl, readUrlState } from './url-state';
3
+
4
+ describe('stream scene countdown URL state', () => {
5
+ it('loads an overlay with case insensitive streaming flag and inputs', () => {
6
+ const state = readUrlState('?STREAMING&scene=raid&message=Back%20soon&duration=90');
7
+ expect(state.streaming).toBe(true);
8
+ expect(state.autoStart).toBe(true);
9
+ expect(state.settings).toEqual({ scene: 'raid', message: 'Back soon', durationSeconds: 90, startMode: 'now' });
10
+ });
11
+
12
+ it('supports a scheduled start in a browser source URL', () => {
13
+ const state = readUrlState('?streaming&scene=starting&start=scheduled&time=23%3A45');
14
+ expect(state.settings.startMode).toBe('scheduled');
15
+ expect(state.settings.startTime).toBe('23:45');
16
+ });
17
+
18
+ it('does not auto-start a normal page without a start flag', () => {
19
+ const state = readUrlState('?scene=brb&duration=600');
20
+ expect(state.streaming).toBe(false);
21
+ expect(state.autoStart).toBe(false);
22
+ });
23
+
24
+ it('loads a visual design and its custom colors', () => {
25
+ const state = readUrlState('?STREAMING&design=type&accent=%23ffb84d&glow=%23ff5f91');
26
+ expect(state.settings).toMatchObject({ design: 'type', accentColor: '#ffb84d', glowColor: '#ff5f91' });
27
+ });
28
+
29
+ it('loads a custom scene title for the browser source', () => {
30
+ const state = readUrlState('?STREAMING&scene=starting&title=Back%20in%20a%20moment');
31
+ expect(state.settings.sceneTitle).toBe('Back in a moment');
32
+ });
33
+
34
+ it('builds a ready-to-paste OBS URL from the streamer settings', () => {
35
+ const url = buildStreamingUrl({
36
+ scene: 'raid',
37
+ sceneTitle: 'Raid starts soon',
38
+ design: 'aurora',
39
+ accentColor: '#a78bfa',
40
+ glowColor: '#8ff5d7',
41
+ message: 'Raid incoming',
42
+ durationSeconds: 90,
43
+ startMode: 'scheduled',
44
+ startTime: '23:45',
45
+ }, 'https://example.com', '/en/stream-scene-countdown-clock/');
46
+ expect(url).toBe('https://example.com/en/stream-scene-countdown-clock/?STREAMING&scene=raid&duration=90&design=aurora&accent=%23a78bfa&glow=%238ff5d7&title=Raid+starts+soon&message=Raid+incoming&start=scheduled&time=23%3A45');
47
+ });
48
+ });
@@ -0,0 +1,118 @@
1
+ import { clampDuration } from './logic';
2
+ import type { DesignKey, SceneKey } from './ui';
3
+ import type { SavedClockSettings } from './storage';
4
+
5
+ export interface UrlClockState {
6
+ streaming: boolean;
7
+ autoStart: boolean;
8
+ settings: Partial<SavedClockSettings>;
9
+ }
10
+
11
+ const scenes: SceneKey[] = ['brb', 'starting', 'raid', 'intermission'];
12
+ const designs: DesignKey[] = ['aurora', 'type', 'pulse', 'glitch', 'sunset'];
13
+
14
+ function getParam(params: URLSearchParams, name: string): string | null {
15
+ for (const [key, value] of params.entries()) {
16
+ if (key.toLowerCase() === name) return value;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function getScene(value: string | null): SceneKey | null {
22
+ return value && scenes.includes(value as SceneKey) ? value as SceneKey : null;
23
+ }
24
+
25
+ function getDesign(value: string | null): DesignKey | null {
26
+ if (value === 'neon') return 'aurora';
27
+ if (value === 'cyber') return 'glitch';
28
+ if (value === 'studio') return 'glitch';
29
+ return value && designs.includes(value as DesignKey) ? value as DesignKey : null;
30
+ }
31
+
32
+ function getColor(value: string | null): string | null {
33
+ return value && /^#[\da-f]{6}$/i.test(value) ? value : null;
34
+ }
35
+
36
+ function getDuration(value: string | null): number | null {
37
+ if (!value) return null;
38
+ const duration = Number(value);
39
+ return Number.isFinite(duration) ? clampDuration(duration) : null;
40
+ }
41
+
42
+ function getStartSettings(params: URLSearchParams, streaming: boolean): { startMode?: SavedClockSettings['startMode']; startTime?: string; autoStart: boolean } {
43
+ const start = getParam(params, 'start');
44
+ const time = getParam(params, 'time');
45
+ if (start && /^\d{2}:\d{2}$/.test(start)) return { startMode: 'scheduled', startTime: start, autoStart: true };
46
+ if (start === 'scheduled' && time) return { startMode: 'scheduled', startTime: time, autoStart: true };
47
+ if (start === 'now' || streaming) return { startMode: 'now', autoStart: true };
48
+ return { autoStart: false };
49
+ }
50
+
51
+ function readSceneAndDesign(params: URLSearchParams): Partial<SavedClockSettings> {
52
+ const scene = getScene(getParam(params, 'scene'));
53
+ const design = getDesign(getParam(params, 'design') ?? getParam(params, 'style'));
54
+ return {
55
+ ...(scene ? { scene } : {}),
56
+ ...(design ? { design } : {}),
57
+ };
58
+ }
59
+
60
+ function readColors(params: URLSearchParams): Partial<SavedClockSettings> {
61
+ const accentColor = getColor(getParam(params, 'accent'));
62
+ const glowColor = getColor(getParam(params, 'glow'));
63
+ return {
64
+ ...(accentColor ? { accentColor } : {}),
65
+ ...(glowColor ? { glowColor } : {}),
66
+ };
67
+ }
68
+
69
+ function readContent(params: URLSearchParams): Partial<SavedClockSettings> {
70
+ const sceneTitle = getParam(params, 'title');
71
+ const message = getParam(params, 'message');
72
+ const durationSeconds = getDuration(getParam(params, 'duration'));
73
+ return {
74
+ ...(sceneTitle !== null ? { sceneTitle } : {}),
75
+ ...(message !== null ? { message } : {}),
76
+ ...(durationSeconds !== null ? { durationSeconds } : {}),
77
+ };
78
+ }
79
+
80
+ function readTimingSettings(timing: ReturnType<typeof getStartSettings>): Partial<SavedClockSettings> {
81
+ return {
82
+ ...(timing.startMode ? { startMode: timing.startMode } : {}),
83
+ ...(timing.startTime ? { startTime: timing.startTime } : {}),
84
+ };
85
+ }
86
+
87
+ export function readUrlState(search: string): UrlClockState {
88
+ const params = new URLSearchParams(search);
89
+ const streaming = Array.from(params.keys()).some((key) => key.toLowerCase() === 'streaming');
90
+ const timing = getStartSettings(params, streaming);
91
+ return {
92
+ streaming,
93
+ autoStart: timing.autoStart,
94
+ settings: {
95
+ ...readSceneAndDesign(params),
96
+ ...readColors(params),
97
+ ...readContent(params),
98
+ ...readTimingSettings(timing),
99
+ },
100
+ };
101
+ }
102
+
103
+ export function buildStreamingUrl(settings: SavedClockSettings, origin: string, pathname: string): string {
104
+ const params = new URLSearchParams({
105
+ scene: settings.scene,
106
+ duration: String(settings.durationSeconds),
107
+ design: settings.design,
108
+ accent: settings.accentColor,
109
+ glow: settings.glowColor,
110
+ });
111
+ if (settings.sceneTitle.trim()) params.set('title', settings.sceneTitle.trim());
112
+ if (settings.message.trim()) params.set('message', settings.message.trim());
113
+ if (settings.startMode === 'scheduled') {
114
+ params.set('start', 'scheduled');
115
+ params.set('time', settings.startTime);
116
+ }
117
+ return `${origin}${pathname}?STREAMING&${params.toString()}`;
118
+ }
package/src/tools.ts CHANGED
@@ -3,9 +3,11 @@ import type { ToolDefinition } from './types';
3
3
  import { SORTEO_TOOL } from './tool/sorteo/index';
4
4
  import { TEBAS_CHECK_TOOL } from './tool/tebasCheck/index';
5
5
  import { VIDEO_BITRATE_PLANNER_TOOL } from './tool/videoBitratePlanner/index';
6
+ import { STREAM_SCENE_COUNTDOWN_CLOCK_TOOL } from './tool/streamSceneCountdownClock/index';
6
7
 
7
8
  export const ALL_TOOLS: ToolDefinition[] = [
8
9
  SORTEO_TOOL,
9
10
  TEBAS_CHECK_TOOL,
10
11
  VIDEO_BITRATE_PLANNER_TOOL,
12
+ STREAM_SCENE_COUNTDOWN_CLOCK_TOOL,
11
13
  ];