react-native-mp3 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +111 -0
- package/android/src/main/AndroidManifest.xml +44 -0
- package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
- package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
- package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
- package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
- package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
- package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
- package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
- package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
- package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
- package/android/src/main/res/drawable/ic_music_note.xml +11 -0
- package/android/src/main/res/drawable/ic_pause.xml +11 -0
- package/android/src/main/res/drawable/ic_play.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
- package/android/src/main/res/drawable/ic_stop.xml +11 -0
- package/lib/components/MusicList.d.ts +0 -0
- package/lib/components/MusicList.js +1 -0
- package/lib/components/MusicPlayerUI.d.ts +0 -0
- package/lib/components/MusicPlayerUI.js +1 -0
- package/lib/hooks/useMusicPlayer.d.ts +38 -0
- package/lib/hooks/useMusicPlayer.js +242 -0
- package/lib/hooks/useMusicScanner.d.ts +27 -0
- package/lib/hooks/useMusicScanner.js +217 -0
- package/lib/hooks/usePermissions.d.ts +9 -0
- package/lib/hooks/usePermissions.js +55 -0
- package/lib/index.d.ts +144 -0
- package/lib/index.js +148 -0
- package/lib/types/common.types.d.ts +79 -0
- package/lib/types/common.types.js +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +2 -0
- package/lib/types/player.types.d.ts +35 -0
- package/lib/types/player.types.js +2 -0
- package/lib/types/scanner.types.d.ts +29 -0
- package/lib/types/scanner.types.js +2 -0
- package/lib/utils/constants.d.ts +31 -0
- package/lib/utils/constants.js +55 -0
- package/lib/utils/events.d.ts +0 -0
- package/lib/utils/events.js +1 -0
- package/package.json +62 -0
- package/src/components/MusicList.tsx +0 -0
- package/src/components/MusicPlayerUI.tsx +0 -0
- package/src/hooks/useMusicPlayer.ts +358 -0
- package/src/hooks/useMusicScanner.ts +286 -0
- package/src/hooks/usePermissions.ts +64 -0
- package/src/index.ts +214 -0
- package/src/types/common.types.ts +86 -0
- package/src/types/index.ts +4 -0
- package/src/types/player.types.ts +37 -0
- package/src/types/scanner.types.ts +31 -0
- package/src/utils/constants.ts +56 -0
- package/src/utils/events.ts +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Player, PlayerEvents } from '../index';
|
|
3
|
+
import type { Song, PlaybackState, PlayerOptions } from '../types/common.types';
|
|
4
|
+
|
|
5
|
+
interface UseMusicPlayerProps {
|
|
6
|
+
autoSetup?: boolean;
|
|
7
|
+
playerOptions?: PlayerOptions;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UseMusicPlayerReturn {
|
|
11
|
+
// State
|
|
12
|
+
playbackState: PlaybackState;
|
|
13
|
+
currentSong: Song | null;
|
|
14
|
+
queue: Song[];
|
|
15
|
+
isPlaying: boolean;
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
|
|
19
|
+
// Playback Controls
|
|
20
|
+
play: (uri: string) => Promise<void>;
|
|
21
|
+
playSong: (song: Song) => Promise<void>;
|
|
22
|
+
playFromQueue: (index: number) => void;
|
|
23
|
+
pause: () => void;
|
|
24
|
+
stop: () => void;
|
|
25
|
+
seekTo: (position: number) => void;
|
|
26
|
+
skipToNext: () => void;
|
|
27
|
+
skipToPrevious: () => void;
|
|
28
|
+
setVolume: (volume: number) => void;
|
|
29
|
+
setPlaybackRate: (rate: number) => void;
|
|
30
|
+
|
|
31
|
+
// Queue Management
|
|
32
|
+
setQueue: (songs: Song[]) => void;
|
|
33
|
+
addToQueue: (songs: Song[]) => void;
|
|
34
|
+
clearQueue: () => void;
|
|
35
|
+
getQueue: () => Promise<Song[]>;
|
|
36
|
+
|
|
37
|
+
// Playback Modes
|
|
38
|
+
setRepeatMode: (mode: 'none' | 'one' | 'all') => void;
|
|
39
|
+
setShuffleMode: (enabled: boolean) => void;
|
|
40
|
+
|
|
41
|
+
// Equalizer
|
|
42
|
+
setEqualizerPreset: (preset: string) => void;
|
|
43
|
+
setEqualizerBands: (bands: number[]) => void;
|
|
44
|
+
enableEqualizer: (enabled: boolean) => void;
|
|
45
|
+
|
|
46
|
+
// Background Playback
|
|
47
|
+
setupBackgroundPlayback: (config?: any) => void;
|
|
48
|
+
stopBackgroundPlayback: () => void;
|
|
49
|
+
|
|
50
|
+
// Sleep Timer
|
|
51
|
+
setSleepTimer: (minutes: number) => void;
|
|
52
|
+
cancelSleepTimer: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaultPlaybackState: PlaybackState = {
|
|
56
|
+
isPlaying: false,
|
|
57
|
+
currentTime: 0,
|
|
58
|
+
duration: 0,
|
|
59
|
+
buffered: 0,
|
|
60
|
+
playbackRate: 1.0,
|
|
61
|
+
volume: 1.0,
|
|
62
|
+
repeatMode: 'none',
|
|
63
|
+
shuffleMode: false,
|
|
64
|
+
currentSong: undefined,
|
|
65
|
+
queue: [],
|
|
66
|
+
queuePosition: -1,
|
|
67
|
+
error: undefined,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const useMusicPlayer = ({
|
|
71
|
+
autoSetup = true,
|
|
72
|
+
playerOptions,
|
|
73
|
+
}: UseMusicPlayerProps = {}): UseMusicPlayerReturn => {
|
|
74
|
+
const [playbackState, setPlaybackState] = useState<PlaybackState>(defaultPlaybackState);
|
|
75
|
+
const [currentSong, setCurrentSong] = useState<Song | null>(null);
|
|
76
|
+
const [queue, setQueue] = useState<Song[]>([]);
|
|
77
|
+
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
|
78
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
79
|
+
const [error, setError] = useState<string | null>(null);
|
|
80
|
+
|
|
81
|
+
// Initialize player
|
|
82
|
+
const initializePlayer = useCallback(async () => {
|
|
83
|
+
try {
|
|
84
|
+
setIsLoading(true);
|
|
85
|
+
setError(null);
|
|
86
|
+
|
|
87
|
+
// Setup background playback if options specify
|
|
88
|
+
if (playerOptions?.audioFocus !== false) {
|
|
89
|
+
Player.setupBackgroundPlayback({
|
|
90
|
+
keepAwake: playerOptions?.keepAwake ?? true,
|
|
91
|
+
ducking: playerOptions?.ducking ?? true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get initial state
|
|
96
|
+
const state = await Player.getPlaybackState();
|
|
97
|
+
updateStateFromNative(state);
|
|
98
|
+
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
setError(err.message || 'Failed to initialize player');
|
|
101
|
+
console.error('Error initializing player:', err);
|
|
102
|
+
} finally {
|
|
103
|
+
setIsLoading(false);
|
|
104
|
+
}
|
|
105
|
+
}, [playerOptions]);
|
|
106
|
+
|
|
107
|
+
// Update state from native response
|
|
108
|
+
const updateStateFromNative = useCallback((state: any) => {
|
|
109
|
+
setPlaybackState({
|
|
110
|
+
isPlaying: state.isPlaying,
|
|
111
|
+
currentTime: state.currentTime,
|
|
112
|
+
duration: state.duration,
|
|
113
|
+
playbackRate: state.playbackRate,
|
|
114
|
+
volume: state.volume,
|
|
115
|
+
repeatMode: state.repeatMode,
|
|
116
|
+
shuffleMode: state.shuffleMode,
|
|
117
|
+
currentSong: state.currentSong,
|
|
118
|
+
queue: state.queue || [],
|
|
119
|
+
queuePosition: state.queuePosition,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
setIsPlaying(state.isPlaying);
|
|
123
|
+
setCurrentSong(state.currentSong || null);
|
|
124
|
+
setQueue(state.queue || []);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
// Play a song by URI
|
|
128
|
+
const play = useCallback(async (uri: string) => {
|
|
129
|
+
try {
|
|
130
|
+
setIsLoading(true);
|
|
131
|
+
setError(null);
|
|
132
|
+
await Player.play(uri);
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
setError(err.message || 'Failed to play song');
|
|
135
|
+
console.error('Error playing song:', err);
|
|
136
|
+
throw err;
|
|
137
|
+
} finally {
|
|
138
|
+
setIsLoading(false);
|
|
139
|
+
}
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// Play a song object
|
|
143
|
+
const playSong = useCallback(async (song: Song) => {
|
|
144
|
+
await play(song.uri);
|
|
145
|
+
}, [play]);
|
|
146
|
+
|
|
147
|
+
// Play from queue
|
|
148
|
+
const playFromQueue = useCallback((index: number) => {
|
|
149
|
+
Player.playFromQueue(index);
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
// Pause playback
|
|
153
|
+
const pause = useCallback(() => {
|
|
154
|
+
Player.pause();
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
// Stop playback
|
|
158
|
+
const stop = useCallback(() => {
|
|
159
|
+
Player.stop();
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// Seek to position
|
|
163
|
+
const seekTo = useCallback((position: number) => {
|
|
164
|
+
Player.seekTo(position);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
// Skip to next
|
|
168
|
+
const skipToNext = useCallback(() => {
|
|
169
|
+
Player.skipToNext();
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
// Skip to previous
|
|
173
|
+
const skipToPrevious = useCallback(() => {
|
|
174
|
+
Player.skipToPrevious();
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
// Set volume
|
|
178
|
+
const setVolume = useCallback((volume: number) => {
|
|
179
|
+
Player.setVolume(volume);
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
// Set playback rate
|
|
183
|
+
const setPlaybackRate = useCallback((rate: number) => {
|
|
184
|
+
Player.setPlaybackRate(rate);
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// Set queue
|
|
188
|
+
const setQueueCallback = useCallback((songs: Song[]) => {
|
|
189
|
+
Player.setQueue(songs);
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
// Add to queue
|
|
193
|
+
const addToQueue = useCallback((songs: Song[]) => {
|
|
194
|
+
Player.addToQueue(songs);
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
// Clear queue
|
|
198
|
+
const clearQueue = useCallback(() => {
|
|
199
|
+
Player.clearQueue();
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
// Get queue
|
|
203
|
+
const getQueue = useCallback(async (): Promise<Song[]> => {
|
|
204
|
+
return await Player.getQueue();
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
// Set repeat mode
|
|
208
|
+
const setRepeatMode = useCallback((mode: 'none' | 'one' | 'all') => {
|
|
209
|
+
Player.setRepeatMode(mode);
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
// Set shuffle mode
|
|
213
|
+
const setShuffleMode = useCallback((enabled: boolean) => {
|
|
214
|
+
Player.setShuffleMode(enabled);
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
// Equalizer controls
|
|
218
|
+
const setEqualizerPreset = useCallback((preset: string) => {
|
|
219
|
+
Player.setEqualizerPreset(preset);
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
const setEqualizerBands = useCallback((bands: number[]) => {
|
|
223
|
+
Player.setEqualizerBands(bands);
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
const enableEqualizer = useCallback((enabled: boolean) => {
|
|
227
|
+
Player.enableEqualizer(enabled);
|
|
228
|
+
}, []);
|
|
229
|
+
|
|
230
|
+
// Background playback
|
|
231
|
+
const setupBackgroundPlayback = useCallback((config?: any) => {
|
|
232
|
+
Player.setupBackgroundPlayback(config);
|
|
233
|
+
}, []);
|
|
234
|
+
|
|
235
|
+
const stopBackgroundPlayback = useCallback(() => {
|
|
236
|
+
Player.stopBackgroundPlayback();
|
|
237
|
+
}, []);
|
|
238
|
+
|
|
239
|
+
// Sleep timer
|
|
240
|
+
const setSleepTimer = useCallback((minutes: number) => {
|
|
241
|
+
Player.setSleepTimer(minutes);
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const cancelSleepTimer = useCallback(() => {
|
|
245
|
+
Player.cancelSleepTimer();
|
|
246
|
+
}, []);
|
|
247
|
+
|
|
248
|
+
// Set up event listeners
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!autoSetup) return;
|
|
251
|
+
|
|
252
|
+
const playbackStateSub = Player.addEventListener(
|
|
253
|
+
PlayerEvents.PLAYBACK_STATE_CHANGED,
|
|
254
|
+
(data: any) => {
|
|
255
|
+
setPlaybackState(prev => ({
|
|
256
|
+
...prev,
|
|
257
|
+
isPlaying: data.isPlaying,
|
|
258
|
+
currentTime: data.currentTime,
|
|
259
|
+
duration: data.duration,
|
|
260
|
+
volume: data.volume,
|
|
261
|
+
playbackRate: data.playbackRate,
|
|
262
|
+
}));
|
|
263
|
+
setIsPlaying(data.isPlaying);
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const queueChangedSub = Player.addEventListener(
|
|
268
|
+
PlayerEvents.PLAYBACK_QUEUE_CHANGED,
|
|
269
|
+
(data: any) => {
|
|
270
|
+
setQueue(data.queue || []);
|
|
271
|
+
setPlaybackState(prev => ({
|
|
272
|
+
...prev,
|
|
273
|
+
queue: data.queue || [],
|
|
274
|
+
queuePosition: data.position,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const playbackErrorSub = Player.addEventListener(
|
|
280
|
+
PlayerEvents.PLAYBACK_ERROR,
|
|
281
|
+
(data: any) => {
|
|
282
|
+
setError(data.error || 'Playback error');
|
|
283
|
+
setPlaybackState(prev => ({
|
|
284
|
+
...prev,
|
|
285
|
+
error: data.error,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const playbackCompleteSub = Player.addEventListener(
|
|
291
|
+
PlayerEvents.PLAYBACK_COMPLETE,
|
|
292
|
+
() => {
|
|
293
|
+
// Auto-play next is handled by native module
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Initialize player
|
|
298
|
+
initializePlayer();
|
|
299
|
+
|
|
300
|
+
return () => {
|
|
301
|
+
// Remove specific listeners
|
|
302
|
+
playbackStateSub.remove();
|
|
303
|
+
queueChangedSub.remove();
|
|
304
|
+
playbackErrorSub.remove();
|
|
305
|
+
playbackCompleteSub.remove();
|
|
306
|
+
|
|
307
|
+
// Clean up background playback if it was set up
|
|
308
|
+
if (autoSetup && playerOptions?.audioFocus !== false) {
|
|
309
|
+
Player.stopBackgroundPlayback();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}, [autoSetup, initializePlayer, playerOptions]);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
// State
|
|
316
|
+
playbackState,
|
|
317
|
+
currentSong,
|
|
318
|
+
queue,
|
|
319
|
+
isPlaying,
|
|
320
|
+
isLoading,
|
|
321
|
+
error,
|
|
322
|
+
|
|
323
|
+
// Playback Controls
|
|
324
|
+
play,
|
|
325
|
+
playSong,
|
|
326
|
+
playFromQueue,
|
|
327
|
+
pause,
|
|
328
|
+
stop,
|
|
329
|
+
seekTo,
|
|
330
|
+
skipToNext,
|
|
331
|
+
skipToPrevious,
|
|
332
|
+
setVolume,
|
|
333
|
+
setPlaybackRate,
|
|
334
|
+
|
|
335
|
+
// Queue Management
|
|
336
|
+
setQueue: setQueueCallback,
|
|
337
|
+
addToQueue,
|
|
338
|
+
clearQueue,
|
|
339
|
+
getQueue,
|
|
340
|
+
|
|
341
|
+
// Playback Modes
|
|
342
|
+
setRepeatMode,
|
|
343
|
+
setShuffleMode,
|
|
344
|
+
|
|
345
|
+
// Equalizer
|
|
346
|
+
setEqualizerPreset,
|
|
347
|
+
setEqualizerBands,
|
|
348
|
+
enableEqualizer,
|
|
349
|
+
|
|
350
|
+
// Background Playback
|
|
351
|
+
setupBackgroundPlayback,
|
|
352
|
+
stopBackgroundPlayback,
|
|
353
|
+
|
|
354
|
+
// Sleep Timer
|
|
355
|
+
setSleepTimer,
|
|
356
|
+
cancelSleepTimer,
|
|
357
|
+
};
|
|
358
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Scanner, ScannerEvents } from '../index';
|
|
3
|
+
import type { Song, Album, Artist, ScannerOptions } from '../types/common.types';
|
|
4
|
+
|
|
5
|
+
interface UseMusicScannerProps {
|
|
6
|
+
autoScan?: boolean;
|
|
7
|
+
scanOptions?: ScannerOptions;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UseMusicScannerReturn {
|
|
11
|
+
// State
|
|
12
|
+
songs: Song[];
|
|
13
|
+
albums: Album[];
|
|
14
|
+
artists: Artist[];
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
isScanning: boolean;
|
|
17
|
+
scanProgress: number;
|
|
18
|
+
permissionGranted: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
|
|
21
|
+
// Methods
|
|
22
|
+
scanMusic: (options?: ScannerOptions) => Promise<void>;
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
search: (query: string) => Promise<Song[]>;
|
|
25
|
+
deleteSong: (songId: string) => Promise<boolean>;
|
|
26
|
+
addToFavorites: (songId: string) => Promise<void>;
|
|
27
|
+
removeFromFavorites: (songId: string) => Promise<void>;
|
|
28
|
+
requestPermission: () => Promise<boolean>;
|
|
29
|
+
checkPermission: () => Promise<boolean>;
|
|
30
|
+
|
|
31
|
+
// Event handlers
|
|
32
|
+
onScanProgress?: (progress: number) => void;
|
|
33
|
+
onScanComplete?: (totalSongs: number) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useMusicScanner = ({
|
|
37
|
+
autoScan = true,
|
|
38
|
+
scanOptions,
|
|
39
|
+
}: UseMusicScannerProps = {}): UseMusicScannerReturn => {
|
|
40
|
+
const [songs, setSongs] = useState<Song[]>([]);
|
|
41
|
+
const [albums, setAlbums] = useState<Album[]>([]);
|
|
42
|
+
const [artists, setArtists] = useState<Artist[]>([]);
|
|
43
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
44
|
+
const [isScanning, setIsScanning] = useState<boolean>(false);
|
|
45
|
+
const [scanProgress, setScanProgress] = useState<number>(0);
|
|
46
|
+
const [permissionGranted, setPermissionGranted] = useState<boolean>(false);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
// Load initial data
|
|
50
|
+
const loadInitialData = useCallback(async () => {
|
|
51
|
+
try {
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
|
|
55
|
+
const hasPermission = await Scanner.checkPermissionStatus();
|
|
56
|
+
setPermissionGranted(hasPermission);
|
|
57
|
+
|
|
58
|
+
if (hasPermission) {
|
|
59
|
+
const cachedSongs = await Scanner.getCachedSongs();
|
|
60
|
+
setSongs(cachedSongs);
|
|
61
|
+
|
|
62
|
+
const [albumsList, artistsList] = await Promise.all([
|
|
63
|
+
Scanner.getAlbums(),
|
|
64
|
+
Scanner.getArtists(),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
setAlbums(albumsList);
|
|
68
|
+
setArtists(artistsList);
|
|
69
|
+
}
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
setError(err.message || 'Failed to load music library');
|
|
72
|
+
console.error('Error loading music library:', err);
|
|
73
|
+
} finally {
|
|
74
|
+
setIsLoading(false);
|
|
75
|
+
}
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
// Scan music files
|
|
79
|
+
const scanMusic = useCallback(async (options?: ScannerOptions) => {
|
|
80
|
+
try {
|
|
81
|
+
setIsScanning(true);
|
|
82
|
+
setScanProgress(0);
|
|
83
|
+
setError(null);
|
|
84
|
+
|
|
85
|
+
const hasPermission = await Scanner.checkPermissionStatus();
|
|
86
|
+
if (!hasPermission) {
|
|
87
|
+
const granted = await Scanner.requestStoragePermission();
|
|
88
|
+
if (!granted) {
|
|
89
|
+
setError('Storage permission is required to scan music files');
|
|
90
|
+
setIsScanning(false);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setPermissionGranted(true);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const scanOpts = options || scanOptions || {};
|
|
97
|
+
const result = await Scanner.scanMusicFiles(scanOpts);
|
|
98
|
+
|
|
99
|
+
// Reload data after scan
|
|
100
|
+
await loadInitialData();
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
setError(err.message || 'Failed to scan music files');
|
|
105
|
+
console.error('Error scanning music files:', err);
|
|
106
|
+
throw err;
|
|
107
|
+
} finally {
|
|
108
|
+
setIsScanning(false);
|
|
109
|
+
setScanProgress(0);
|
|
110
|
+
}
|
|
111
|
+
}, [scanOptions, loadInitialData]);
|
|
112
|
+
|
|
113
|
+
// Refresh data
|
|
114
|
+
const refresh = useCallback(async () => {
|
|
115
|
+
await loadInitialData();
|
|
116
|
+
}, [loadInitialData]);
|
|
117
|
+
|
|
118
|
+
// Search songs
|
|
119
|
+
const search = useCallback(async (query: string): Promise<Song[]> => {
|
|
120
|
+
if (!query.trim()) {
|
|
121
|
+
return songs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const results = await Scanner.searchSongs(query);
|
|
126
|
+
return results;
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
setError(err.message || 'Search failed');
|
|
129
|
+
console.error('Error searching songs:', err);
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}, [songs]);
|
|
133
|
+
|
|
134
|
+
// Delete song
|
|
135
|
+
const deleteSong = useCallback(async (songId: string): Promise<boolean> => {
|
|
136
|
+
try {
|
|
137
|
+
const success = await Scanner.deleteSong(songId);
|
|
138
|
+
if (success) {
|
|
139
|
+
// Remove from local state
|
|
140
|
+
setSongs(prev => prev.filter(song => song.id !== songId));
|
|
141
|
+
// Update albums and artists
|
|
142
|
+
await refresh();
|
|
143
|
+
}
|
|
144
|
+
return success;
|
|
145
|
+
} catch (err: any) {
|
|
146
|
+
setError(err.message || 'Failed to delete song');
|
|
147
|
+
console.error('Error deleting song:', err);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}, [refresh]);
|
|
151
|
+
|
|
152
|
+
// Add to favorites
|
|
153
|
+
const addToFavorites = useCallback(async (songId: string) => {
|
|
154
|
+
try {
|
|
155
|
+
await Scanner.addToFavorites(songId);
|
|
156
|
+
// Update local state
|
|
157
|
+
setSongs(prev => prev.map(song =>
|
|
158
|
+
song.id === songId ? { ...song, isFavorite: true } : song
|
|
159
|
+
));
|
|
160
|
+
} catch (err: any) {
|
|
161
|
+
setError(err.message || 'Failed to add to favorites');
|
|
162
|
+
console.error('Error adding to favorites:', err);
|
|
163
|
+
}
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
// Remove from favorites
|
|
167
|
+
const removeFromFavorites = useCallback(async (songId: string) => {
|
|
168
|
+
try {
|
|
169
|
+
await Scanner.removeFromFavorites(songId);
|
|
170
|
+
// Update local state
|
|
171
|
+
setSongs(prev => prev.map(song =>
|
|
172
|
+
song.id === songId ? { ...song, isFavorite: false } : song
|
|
173
|
+
));
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
setError(err.message || 'Failed to remove from favorites');
|
|
176
|
+
console.error('Error removing from favorites:', err);
|
|
177
|
+
}
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
// Request permission
|
|
181
|
+
const requestPermission = useCallback(async (): Promise<boolean> => {
|
|
182
|
+
try {
|
|
183
|
+
const granted = await Scanner.requestStoragePermission();
|
|
184
|
+
setPermissionGranted(granted);
|
|
185
|
+
return granted;
|
|
186
|
+
} catch (err: any) {
|
|
187
|
+
setError(err.message || 'Failed to request permission');
|
|
188
|
+
console.error('Error requesting permission:', err);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
// Check permission
|
|
194
|
+
const checkPermission = useCallback(async (): Promise<boolean> => {
|
|
195
|
+
try {
|
|
196
|
+
const granted = await Scanner.checkPermissionStatus();
|
|
197
|
+
setPermissionGranted(granted);
|
|
198
|
+
return granted;
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
setError(err.message || 'Failed to check permission');
|
|
201
|
+
console.error('Error checking permission:', err);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
// Set up event listeners
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const scanProgressSub = Scanner.addEventListener(
|
|
209
|
+
ScannerEvents.SCAN_PROGRESS,
|
|
210
|
+
(data: any) => {
|
|
211
|
+
setScanProgress(data.percentage);
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const scanCompleteSub = Scanner.addEventListener(
|
|
216
|
+
ScannerEvents.SCAN_COMPLETE,
|
|
217
|
+
() => {
|
|
218
|
+
loadInitialData();
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const fileAddedSub = Scanner.addEventListener(
|
|
223
|
+
ScannerEvents.FILE_ADDED,
|
|
224
|
+
() => {
|
|
225
|
+
loadInitialData();
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const fileDeletedSub = Scanner.addEventListener(
|
|
230
|
+
ScannerEvents.FILE_DELETED,
|
|
231
|
+
() => {
|
|
232
|
+
loadInitialData();
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const permissionSub = Scanner.addEventListener(
|
|
237
|
+
ScannerEvents.PERMISSION_STATUS,
|
|
238
|
+
(data: any) => {
|
|
239
|
+
setPermissionGranted(data.granted);
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Auto scan on mount if enabled
|
|
244
|
+
if (autoScan && permissionGranted) {
|
|
245
|
+
loadInitialData();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return () => {
|
|
249
|
+
// Remove specific listeners instead of all
|
|
250
|
+
scanProgressSub.remove();
|
|
251
|
+
scanCompleteSub.remove();
|
|
252
|
+
fileAddedSub.remove();
|
|
253
|
+
fileDeletedSub.remove();
|
|
254
|
+
permissionSub.remove();
|
|
255
|
+
|
|
256
|
+
// If you want to remove all listeners:
|
|
257
|
+
// Scanner.removeAllListeners(ScannerEvents.SCAN_PROGRESS);
|
|
258
|
+
// Scanner.removeAllListeners(ScannerEvents.SCAN_COMPLETE);
|
|
259
|
+
// Scanner.removeAllListeners(ScannerEvents.FILE_ADDED);
|
|
260
|
+
// Scanner.removeAllListeners(ScannerEvents.FILE_DELETED);
|
|
261
|
+
// Scanner.removeAllListeners(ScannerEvents.PERMISSION_STATUS);
|
|
262
|
+
};
|
|
263
|
+
}, [autoScan, loadInitialData, permissionGranted]);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
// State
|
|
267
|
+
songs,
|
|
268
|
+
albums,
|
|
269
|
+
artists,
|
|
270
|
+
isLoading,
|
|
271
|
+
isScanning,
|
|
272
|
+
scanProgress,
|
|
273
|
+
permissionGranted,
|
|
274
|
+
error,
|
|
275
|
+
|
|
276
|
+
// Methods
|
|
277
|
+
scanMusic,
|
|
278
|
+
refresh,
|
|
279
|
+
search,
|
|
280
|
+
deleteSong,
|
|
281
|
+
addToFavorites,
|
|
282
|
+
removeFromFavorites,
|
|
283
|
+
requestPermission,
|
|
284
|
+
checkPermission,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Scanner } from '../index';
|
|
3
|
+
|
|
4
|
+
interface UsePermissionsReturn {
|
|
5
|
+
permissionGranted: boolean;
|
|
6
|
+
isChecking: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
requestPermission: () => Promise<boolean>;
|
|
9
|
+
checkPermission: () => Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const usePermissions = (): UsePermissionsReturn => {
|
|
13
|
+
const [permissionGranted, setPermissionGranted] = useState<boolean>(false);
|
|
14
|
+
const [isChecking, setIsChecking] = useState<boolean>(false);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const checkPermission = async (): Promise<boolean> => {
|
|
18
|
+
try {
|
|
19
|
+
setIsChecking(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
|
|
22
|
+
const granted = await Scanner.checkPermissionStatus();
|
|
23
|
+
setPermissionGranted(granted);
|
|
24
|
+
|
|
25
|
+
return granted;
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
setError(err.message || 'Failed to check permission');
|
|
28
|
+
console.error('Error checking permission:', err);
|
|
29
|
+
return false;
|
|
30
|
+
} finally {
|
|
31
|
+
setIsChecking(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const requestPermission = async (): Promise<boolean> => {
|
|
36
|
+
try {
|
|
37
|
+
setIsChecking(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
|
|
40
|
+
const granted = await Scanner.requestStoragePermission();
|
|
41
|
+
setPermissionGranted(granted);
|
|
42
|
+
|
|
43
|
+
return granted;
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
setError(err.message || 'Failed to request permission');
|
|
46
|
+
console.error('Error requesting permission:', err);
|
|
47
|
+
return false;
|
|
48
|
+
} finally {
|
|
49
|
+
setIsChecking(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
checkPermission();
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
permissionGranted,
|
|
59
|
+
isChecking,
|
|
60
|
+
error,
|
|
61
|
+
requestPermission,
|
|
62
|
+
checkPermission,
|
|
63
|
+
};
|
|
64
|
+
};
|