@djangocfg/ui-nextjs 2.1.77 → 2.1.79
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/README.md +24 -4
- package/package.json +4 -4
- package/src/lib/index.ts +4 -1
- package/src/lib/logger/index.ts +10 -0
- package/src/lib/logger/logStore.ts +122 -0
- package/src/lib/logger/logger.ts +175 -0
- package/src/lib/logger/types.ts +82 -0
- package/src/stores/mediaCache.ts +10 -0
- package/src/tools/AudioPlayer/README.md +16 -1
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +9 -1
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +80 -4
- package/src/tools/AudioPlayer/hooks/index.ts +2 -0
- package/src/tools/AudioPlayer/hooks/useAudioSource.ts +139 -0
- package/src/tools/AudioPlayer/progressive/useAudioElement.ts +52 -1
- package/src/tools/AudioPlayer/types/audio.ts +14 -0
- package/src/tools/AudioPlayer/utils/debug.ts +12 -0
- package/src/tools/AudioPlayer/utils/index.ts +1 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +18 -4
- package/src/tools/ImageViewer/utils/debug.ts +12 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +60 -1
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +61 -3
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +67 -3
- package/src/tools/VideoPlayer/utils/debug.ts +12 -0
- package/src/tools/VideoPlayer/utils/index.ts +1 -0
package/README.md
CHANGED
|
@@ -57,11 +57,11 @@ All components from `@djangocfg/ui-core` are re-exported.
|
|
|
57
57
|
import { Hero } from '@djangocfg/ui-nextjs/blocks';
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
### Tools (
|
|
61
|
-
`JsonTree` `PrettyCode` `Mermaid` `LottiePlayer`
|
|
60
|
+
### Tools (7)
|
|
61
|
+
`JsonTree` `PrettyCode` `Mermaid` `LottiePlayer` `AudioPlayer` `VideoPlayer` `ImageViewer`
|
|
62
62
|
|
|
63
63
|
```tsx
|
|
64
|
-
import { PrettyCode } from '@djangocfg/ui-nextjs/tools';
|
|
64
|
+
import { PrettyCode, AudioPlayer, VideoPlayer } from '@djangocfg/ui-nextjs/tools';
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
## Hooks
|
|
@@ -108,6 +108,25 @@ function Example() {
|
|
|
108
108
|
import '@djangocfg/ui-nextjs/styles/globals';
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Logger
|
|
112
|
+
|
|
113
|
+
Universal logger with consola + zustand for Console panel integration.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { createLogger, createMediaLogger } from '@djangocfg/ui-nextjs/lib';
|
|
117
|
+
|
|
118
|
+
// Basic logger
|
|
119
|
+
const log = createLogger('MyComponent');
|
|
120
|
+
log.info('User logged in', { userId: 123 });
|
|
121
|
+
log.error('Failed to load', { error });
|
|
122
|
+
|
|
123
|
+
// Media logger (with seek/buffer helpers)
|
|
124
|
+
const mediaLog = createMediaLogger('AudioPlayer');
|
|
125
|
+
mediaLog.load(src, 'stream');
|
|
126
|
+
mediaLog.seek(from, to, duration);
|
|
127
|
+
mediaLog.buffer(buffered, duration);
|
|
128
|
+
```
|
|
129
|
+
|
|
111
130
|
## Exports
|
|
112
131
|
|
|
113
132
|
| Path | Content |
|
|
@@ -116,7 +135,8 @@ import '@djangocfg/ui-nextjs/styles/globals';
|
|
|
116
135
|
| `@djangocfg/ui-nextjs/components` | Components only |
|
|
117
136
|
| `@djangocfg/ui-nextjs/hooks` | Hooks only |
|
|
118
137
|
| `@djangocfg/ui-nextjs/blocks` | Landing page blocks |
|
|
119
|
-
| `@djangocfg/ui-nextjs/tools` | JsonTree, Mermaid,
|
|
138
|
+
| `@djangocfg/ui-nextjs/tools` | JsonTree, Mermaid, Media players |
|
|
139
|
+
| `@djangocfg/ui-nextjs/lib` | Logger, utilities |
|
|
120
140
|
| `@djangocfg/ui-nextjs/theme` | ThemeProvider, ThemeToggle |
|
|
121
141
|
| `@djangocfg/ui-nextjs/styles` | CSS |
|
|
122
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.79",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.79",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.79",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"wavesurfer.js": "^7.12.1"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
113
|
+
"@djangocfg/typescript-config": "^2.1.79",
|
|
114
114
|
"@types/node": "^24.7.2",
|
|
115
115
|
"eslint": "^9.37.0",
|
|
116
116
|
"tailwindcss-animate": "1.0.7",
|
package/src/lib/index.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Logger
|
|
3
|
+
*
|
|
4
|
+
* Combines console logging with zustand store for Console panel.
|
|
5
|
+
* Use createMediaLogger for media tools (AudioPlayer, VideoPlayer, ImageViewer).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { createLogger, createMediaLogger, logger, log } from './logger';
|
|
9
|
+
export { useLogStore, useFilteredLogs, useLogCount, useErrorCount } from './logStore';
|
|
10
|
+
export type { LogEntry, LogLevel, LogFilter, LogStore, Logger, MediaLogger } from './types';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Store
|
|
3
|
+
*
|
|
4
|
+
* Zustand store for log accumulation and filtering.
|
|
5
|
+
* Keeps logs in memory for Console panel display.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { create } from 'zustand';
|
|
11
|
+
import type { LogStore, LogEntry, LogFilter } from './types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FILTER: LogFilter = {
|
|
14
|
+
levels: ['debug', 'info', 'warn', 'error', 'success'],
|
|
15
|
+
component: undefined,
|
|
16
|
+
search: undefined,
|
|
17
|
+
since: undefined,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAX_LOGS = 1000;
|
|
21
|
+
|
|
22
|
+
function generateId(): string {
|
|
23
|
+
return `log-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function matchesFilter(entry: LogEntry, filter: LogFilter): boolean {
|
|
27
|
+
// Level filter
|
|
28
|
+
if (!filter.levels.includes(entry.level)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Component filter (case-insensitive partial match)
|
|
33
|
+
if (filter.component) {
|
|
34
|
+
const comp = filter.component.toLowerCase();
|
|
35
|
+
if (!entry.component.toLowerCase().includes(comp)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Search filter (case-insensitive, searches message and data)
|
|
41
|
+
if (filter.search) {
|
|
42
|
+
const search = filter.search.toLowerCase();
|
|
43
|
+
const inMessage = entry.message.toLowerCase().includes(search);
|
|
44
|
+
const inData = entry.data
|
|
45
|
+
? JSON.stringify(entry.data).toLowerCase().includes(search)
|
|
46
|
+
: false;
|
|
47
|
+
if (!inMessage && !inData) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Time filter
|
|
53
|
+
if (filter.since && entry.timestamp < filter.since) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const useLogStore = create<LogStore>((set, get) => ({
|
|
61
|
+
logs: [],
|
|
62
|
+
filter: DEFAULT_FILTER,
|
|
63
|
+
maxLogs: MAX_LOGS,
|
|
64
|
+
|
|
65
|
+
addLog: (entry) => {
|
|
66
|
+
const newEntry: LogEntry = {
|
|
67
|
+
...entry,
|
|
68
|
+
id: generateId(),
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
set((state) => {
|
|
73
|
+
const newLogs = [...state.logs, newEntry];
|
|
74
|
+
// Trim to max size
|
|
75
|
+
if (newLogs.length > state.maxLogs) {
|
|
76
|
+
return { logs: newLogs.slice(-state.maxLogs) };
|
|
77
|
+
}
|
|
78
|
+
return { logs: newLogs };
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
clearLogs: () => {
|
|
83
|
+
set({ logs: [] });
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
setFilter: (filter) => {
|
|
87
|
+
set((state) => ({
|
|
88
|
+
filter: { ...state.filter, ...filter },
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
resetFilter: () => {
|
|
93
|
+
set({ filter: DEFAULT_FILTER });
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getFilteredLogs: () => {
|
|
97
|
+
const { logs, filter } = get();
|
|
98
|
+
return logs.filter((entry) => matchesFilter(entry, filter));
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
exportLogs: () => {
|
|
102
|
+
const { logs } = get();
|
|
103
|
+
return JSON.stringify(logs, null, 2);
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// Selector hooks for performance
|
|
108
|
+
export const useFilteredLogs = () => {
|
|
109
|
+
const logs = useLogStore((state) => state.logs);
|
|
110
|
+
const filter = useLogStore((state) => state.filter);
|
|
111
|
+
return logs.filter((entry) => matchesFilter(entry, filter));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const useLogCount = () => {
|
|
115
|
+
return useLogStore((state) => state.logs.length);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const useErrorCount = () => {
|
|
119
|
+
return useLogStore((state) =>
|
|
120
|
+
state.logs.filter((log) => log.level === 'error').length
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger
|
|
3
|
+
*
|
|
4
|
+
* Universal logger with consola + zustand store integration.
|
|
5
|
+
* Logs are accumulated for Console panel display.
|
|
6
|
+
* In production, only logs to store (no console output).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import { consola, type ConsolaInstance } from 'consola';
|
|
12
|
+
import { useLogStore } from './logStore';
|
|
13
|
+
import type { Logger, LogLevel, MediaLogger } from './types';
|
|
14
|
+
|
|
15
|
+
// Check environment
|
|
16
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
17
|
+
const isBrowser = typeof window !== 'undefined';
|
|
18
|
+
|
|
19
|
+
// Create base consola instance
|
|
20
|
+
const baseConsola = consola.create({
|
|
21
|
+
level: isDev ? 4 : 2, // 4 = debug, 2 = warn
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract error details from unknown error type
|
|
26
|
+
*/
|
|
27
|
+
function extractErrorData(data?: Record<string, unknown>): {
|
|
28
|
+
cleanData: Record<string, unknown> | undefined;
|
|
29
|
+
stack: string | undefined;
|
|
30
|
+
} {
|
|
31
|
+
if (!data) return { cleanData: undefined, stack: undefined };
|
|
32
|
+
|
|
33
|
+
const cleanData = { ...data };
|
|
34
|
+
let stack: string | undefined;
|
|
35
|
+
|
|
36
|
+
// Extract stack from error object
|
|
37
|
+
if (data.error instanceof Error) {
|
|
38
|
+
stack = data.error.stack;
|
|
39
|
+
cleanData.error = {
|
|
40
|
+
name: data.error.name,
|
|
41
|
+
message: data.error.message,
|
|
42
|
+
};
|
|
43
|
+
} else if (typeof data.error === 'object' && data.error !== null) {
|
|
44
|
+
const errObj = data.error as Record<string, unknown>;
|
|
45
|
+
if (typeof errObj.stack === 'string') {
|
|
46
|
+
stack = errObj.stack;
|
|
47
|
+
}
|
|
48
|
+
if (typeof errObj.message === 'string') {
|
|
49
|
+
cleanData.error = errObj.message;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { cleanData, stack };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format buffer ranges for logging
|
|
58
|
+
*/
|
|
59
|
+
function formatBufferRanges(buffered: TimeRanges, duration: number): string {
|
|
60
|
+
if (buffered.length === 0) return 'empty';
|
|
61
|
+
|
|
62
|
+
const ranges: string[] = [];
|
|
63
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
64
|
+
const start = buffered.start(i);
|
|
65
|
+
const end = buffered.end(i);
|
|
66
|
+
const percent = ((end - start) / duration * 100).toFixed(1);
|
|
67
|
+
ranges.push(`${start.toFixed(1)}-${end.toFixed(1)}s (${percent}%)`);
|
|
68
|
+
}
|
|
69
|
+
return ranges.join(', ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a logger for a specific component/module
|
|
74
|
+
*/
|
|
75
|
+
export function createLogger(component: string): Logger {
|
|
76
|
+
const consolaTagged = baseConsola.withTag(component);
|
|
77
|
+
|
|
78
|
+
const log = (level: LogLevel, message: string, data?: Record<string, unknown>) => {
|
|
79
|
+
const { cleanData, stack } = extractErrorData(data);
|
|
80
|
+
|
|
81
|
+
// Add to store (for Console panel)
|
|
82
|
+
if (isBrowser) {
|
|
83
|
+
useLogStore.getState().addLog({
|
|
84
|
+
level,
|
|
85
|
+
component,
|
|
86
|
+
message,
|
|
87
|
+
data: cleanData,
|
|
88
|
+
stack,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Log to console via consola (in dev mode)
|
|
93
|
+
if (isDev) {
|
|
94
|
+
const consolaMethod = level === 'success' ? 'success' : level;
|
|
95
|
+
if (cleanData) {
|
|
96
|
+
consolaTagged[consolaMethod](message, cleanData);
|
|
97
|
+
} else {
|
|
98
|
+
consolaTagged[consolaMethod](message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
debug: (message, data) => log('debug', message, data),
|
|
105
|
+
info: (message, data) => log('info', message, data),
|
|
106
|
+
warn: (message, data) => log('warn', message, data),
|
|
107
|
+
error: (message, data) => log('error', message, data),
|
|
108
|
+
success: (message, data) => log('success', message, data),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a media-specific logger with helper methods
|
|
114
|
+
* for AudioPlayer, VideoPlayer, ImageViewer
|
|
115
|
+
*/
|
|
116
|
+
export function createMediaLogger(component: string): MediaLogger {
|
|
117
|
+
const baseLogger = createLogger(component);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...baseLogger,
|
|
121
|
+
|
|
122
|
+
load: (src: string, type?: string) => {
|
|
123
|
+
const typeStr = type ? ` (${type})` : '';
|
|
124
|
+
baseLogger.info(`LOAD: ${src}${typeStr}`);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
state: (state: string, details?: Record<string, unknown>) => {
|
|
128
|
+
baseLogger.debug(`STATE: ${state}`, details);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
seek: (from: number, to: number, duration: number) => {
|
|
132
|
+
baseLogger.debug(`SEEK: ${from.toFixed(2)}s -> ${to.toFixed(2)}s`, {
|
|
133
|
+
from,
|
|
134
|
+
to,
|
|
135
|
+
duration,
|
|
136
|
+
progress: `${((to / duration) * 100).toFixed(1)}%`,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
buffer: (buffered: TimeRanges, duration: number) => {
|
|
141
|
+
if (buffered.length > 0) {
|
|
142
|
+
baseLogger.debug(`BUFFER: ${formatBufferRanges(buffered, duration)}`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
event: (name: string, data?: unknown) => {
|
|
147
|
+
if (data !== undefined) {
|
|
148
|
+
baseLogger.debug(`EVENT: ${name}`, { data });
|
|
149
|
+
} else {
|
|
150
|
+
baseLogger.debug(`EVENT: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Global logger for non-component code
|
|
158
|
+
*/
|
|
159
|
+
export const logger = createLogger('App');
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Quick access for one-off logs
|
|
163
|
+
*/
|
|
164
|
+
export const log = {
|
|
165
|
+
debug: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
166
|
+
createLogger(component).debug(message, data),
|
|
167
|
+
info: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
168
|
+
createLogger(component).info(message, data),
|
|
169
|
+
warn: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
170
|
+
createLogger(component).warn(message, data),
|
|
171
|
+
error: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
172
|
+
createLogger(component).error(message, data),
|
|
173
|
+
success: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
174
|
+
createLogger(component).success(message, data),
|
|
175
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the universal logging system.
|
|
5
|
+
* Compatible with Console panel in FileWorkspace IDE layout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
|
|
9
|
+
|
|
10
|
+
export interface LogEntry {
|
|
11
|
+
/** Unique log ID */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Timestamp when log was created */
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
/** Log level */
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
/** Component/module name that created the log */
|
|
18
|
+
component: string;
|
|
19
|
+
/** Log message */
|
|
20
|
+
message: string;
|
|
21
|
+
/** Additional data (objects, errors, etc.) */
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
/** Error stack trace (for error level) */
|
|
24
|
+
stack?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LogFilter {
|
|
28
|
+
/** Filter by log levels */
|
|
29
|
+
levels: LogLevel[];
|
|
30
|
+
/** Filter by component name (partial match) */
|
|
31
|
+
component?: string;
|
|
32
|
+
/** Filter by message text (partial match) */
|
|
33
|
+
search?: string;
|
|
34
|
+
/** Filter by time range */
|
|
35
|
+
since?: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LogStore {
|
|
39
|
+
/** All accumulated logs */
|
|
40
|
+
logs: LogEntry[];
|
|
41
|
+
/** Current filter settings */
|
|
42
|
+
filter: LogFilter;
|
|
43
|
+
/** Maximum logs to keep */
|
|
44
|
+
maxLogs: number;
|
|
45
|
+
|
|
46
|
+
/** Add new log entry */
|
|
47
|
+
addLog: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
48
|
+
/** Clear all logs */
|
|
49
|
+
clearLogs: () => void;
|
|
50
|
+
/** Update filter settings */
|
|
51
|
+
setFilter: (filter: Partial<LogFilter>) => void;
|
|
52
|
+
/** Reset filter to defaults */
|
|
53
|
+
resetFilter: () => void;
|
|
54
|
+
/** Get filtered logs */
|
|
55
|
+
getFilteredLogs: () => LogEntry[];
|
|
56
|
+
/** Export logs as JSON string */
|
|
57
|
+
exportLogs: () => string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Logger {
|
|
61
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
62
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
63
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
64
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
65
|
+
success: (message: string, data?: Record<string, unknown>) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Media-specific log helper methods
|
|
70
|
+
*/
|
|
71
|
+
export interface MediaLogger extends Logger {
|
|
72
|
+
/** Log source load event */
|
|
73
|
+
load: (src: string, type?: string) => void;
|
|
74
|
+
/** Log state change */
|
|
75
|
+
state: (state: string, details?: Record<string, unknown>) => void;
|
|
76
|
+
/** Log seek event */
|
|
77
|
+
seek: (from: number, to: number, duration: number) => void;
|
|
78
|
+
/** Log buffer ranges */
|
|
79
|
+
buffer: (buffered: TimeRanges, duration: number) => void;
|
|
80
|
+
/** Log generic event */
|
|
81
|
+
event: (name: string, data?: unknown) => void;
|
|
82
|
+
}
|
package/src/stores/mediaCache.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import { create } from 'zustand';
|
|
4
4
|
import { persist, devtools } from 'zustand/middleware';
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow';
|
|
6
|
+
import { createLogger } from '../lib/logger';
|
|
7
|
+
|
|
8
|
+
const cacheDebug = createLogger('MediaCache');
|
|
6
9
|
|
|
7
10
|
// Types
|
|
8
11
|
interface BlobUrlEntry {
|
|
@@ -120,6 +123,7 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
120
123
|
const existing = get().blobUrls.get(key);
|
|
121
124
|
if (existing) {
|
|
122
125
|
// Increment ref count
|
|
126
|
+
cacheDebug.debug(`Blob URL reused: ${key}`, { refCount: existing.refCount + 1 });
|
|
123
127
|
set(
|
|
124
128
|
(state) => ({
|
|
125
129
|
blobUrls: new Map(state.blobUrls).set(key, {
|
|
@@ -136,6 +140,8 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
136
140
|
// Create new blob URL
|
|
137
141
|
const blob = new Blob([content], { type: mimeType });
|
|
138
142
|
const url = URL.createObjectURL(blob);
|
|
143
|
+
const sizeMB = (content.byteLength / 1024 / 1024).toFixed(2);
|
|
144
|
+
cacheDebug.debug(`Blob URL created: ${key}`, { mimeType, size: `${sizeMB}MB` });
|
|
139
145
|
set(
|
|
140
146
|
(state) => ({
|
|
141
147
|
blobUrls: new Map(state.blobUrls).set(key, {
|
|
@@ -156,6 +162,7 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
156
162
|
|
|
157
163
|
if (entry.refCount <= 1) {
|
|
158
164
|
// Last reference - revoke and remove
|
|
165
|
+
cacheDebug.debug(`Blob URL revoked: ${key}`);
|
|
159
166
|
URL.revokeObjectURL(entry.url);
|
|
160
167
|
set(
|
|
161
168
|
(state) => {
|
|
@@ -168,6 +175,7 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
168
175
|
);
|
|
169
176
|
} else {
|
|
170
177
|
// Decrement ref count
|
|
178
|
+
cacheDebug.debug(`Blob URL released: ${key}`, { refCount: entry.refCount - 1 });
|
|
171
179
|
set(
|
|
172
180
|
(state) => ({
|
|
173
181
|
blobUrls: new Map(state.blobUrls).set(key, {
|
|
@@ -315,6 +323,8 @@ export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
|
315
323
|
// ========== Global ==========
|
|
316
324
|
|
|
317
325
|
clearCache: () => {
|
|
326
|
+
const stats = get().getCacheStats();
|
|
327
|
+
cacheDebug.info('Clearing cache', stats);
|
|
318
328
|
// Revoke all blob URLs before clearing
|
|
319
329
|
get().blobUrls.forEach(({ url }) => URL.revokeObjectURL(url));
|
|
320
330
|
set(initialState, false, 'clearCache');
|
|
@@ -46,6 +46,7 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
|
46
46
|
| Prop | Type | Default | Description |
|
|
47
47
|
|------|------|---------|-------------|
|
|
48
48
|
| `src` | `string` | required | Audio URL |
|
|
49
|
+
| `prefetch` | `boolean` | `true` | Pre-fetch audio as blob (required for streaming URLs to enable seek) |
|
|
49
50
|
| `title` | `string` | - | Track title |
|
|
50
51
|
| `artist` | `string` | - | Artist name |
|
|
51
52
|
| `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
|
|
@@ -54,6 +55,7 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
|
54
55
|
| `showEqualizer` | `boolean` | `false` | Show equalizer bars |
|
|
55
56
|
| `showTimer` | `boolean` | `true` | Show time display |
|
|
56
57
|
| `showVolume` | `boolean` | `true` | Show volume control |
|
|
58
|
+
| `showLoop` | `boolean` | `true` | Show loop/repeat button |
|
|
57
59
|
| `reactiveCover` | `boolean` | `true` | Enable reactive effects |
|
|
58
60
|
| `variant` | `VisualizationVariant` | - | Effect variant |
|
|
59
61
|
| `intensity` | `EffectIntensity` | - | Effect intensity |
|
|
@@ -61,6 +63,8 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
|
61
63
|
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
62
64
|
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
|
63
65
|
|
|
66
|
+
> **Note:** The `prefetch` option is enabled by default. This fetches the entire audio file as a blob before loading into WaveSurfer, which is required for seeking to work correctly with streaming URLs. For very large files (> 50MB), consider using `prefetch={false}` and the Progressive player mode instead.
|
|
67
|
+
|
|
64
68
|
---
|
|
65
69
|
|
|
66
70
|
## Advanced Usage
|
|
@@ -99,7 +103,10 @@ Context provider for audio state. Wraps all audio components.
|
|
|
99
103
|
|
|
100
104
|
```tsx
|
|
101
105
|
<AudioProvider
|
|
102
|
-
source={{
|
|
106
|
+
source={{
|
|
107
|
+
uri: 'https://example.com/audio.mp3',
|
|
108
|
+
prefetch: true // Fetch as blob for seek support (default: false)
|
|
109
|
+
}}
|
|
103
110
|
containerRef={containerRef}
|
|
104
111
|
autoPlay={false}
|
|
105
112
|
waveformOptions={{
|
|
@@ -115,6 +122,13 @@ Context provider for audio state. Wraps all audio components.
|
|
|
115
122
|
</AudioProvider>
|
|
116
123
|
```
|
|
117
124
|
|
|
125
|
+
#### AudioSource Options
|
|
126
|
+
|
|
127
|
+
| Prop | Type | Default | Description |
|
|
128
|
+
|------|------|---------|-------------|
|
|
129
|
+
| `uri` | `string` | required | Audio URL |
|
|
130
|
+
| `prefetch` | `boolean` | `false` | Pre-fetch as blob (enables seek for streaming URLs) |
|
|
131
|
+
|
|
118
132
|
### AudioPlayer
|
|
119
133
|
|
|
120
134
|
Main player component with waveform and controls.
|
|
@@ -294,6 +308,7 @@ AudioPlayer/
|
|
|
294
308
|
│ └── effects.ts # Visualization effect types
|
|
295
309
|
├── hooks/
|
|
296
310
|
│ ├── index.ts
|
|
311
|
+
│ ├── useAudioSource.ts # Audio source loading with prefetch
|
|
297
312
|
│ ├── useAudioHotkeys.ts # Keyboard shortcuts
|
|
298
313
|
│ ├── useVisualization.tsx # Visualization settings
|
|
299
314
|
│ ├── useAudioAnalysis.ts # Web Audio frequency analysis
|
|
@@ -53,6 +53,13 @@ export interface SimpleAudioPlayerProps {
|
|
|
53
53
|
/** Audio source URL */
|
|
54
54
|
src: string;
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Pre-fetch audio as blob before loading into WaveSurfer.
|
|
58
|
+
* Required for streaming URLs because WaveSurfer needs complete file for seek to work.
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
prefetch?: boolean;
|
|
62
|
+
|
|
56
63
|
/** Track title */
|
|
57
64
|
title?: string;
|
|
58
65
|
|
|
@@ -132,6 +139,7 @@ export function SimpleAudioPlayer(props: SimpleAudioPlayerProps) {
|
|
|
132
139
|
|
|
133
140
|
function SimpleAudioPlayerContent({
|
|
134
141
|
src,
|
|
142
|
+
prefetch = true,
|
|
135
143
|
title,
|
|
136
144
|
artist,
|
|
137
145
|
coverArt,
|
|
@@ -190,7 +198,7 @@ function SimpleAudioPlayerContent({
|
|
|
190
198
|
|
|
191
199
|
return (
|
|
192
200
|
<AudioProvider
|
|
193
|
-
source={{ uri: src }}
|
|
201
|
+
source={{ uri: src, prefetch }}
|
|
194
202
|
containerRef={containerRef}
|
|
195
203
|
autoPlay={autoPlay}
|
|
196
204
|
waveformOptions={waveformOptions}
|