@roitium/expo-orpheus 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.
Files changed (61) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.md +11 -0
  3. package/android/build.gradle +51 -0
  4. package/android/src/main/AndroidManifest.xml +21 -0
  5. package/android/src/main/java/expo/modules/orpheus/ExpoOrpheusModule.kt +365 -0
  6. package/android/src/main/java/expo/modules/orpheus/NetworkModule.kt +46 -0
  7. package/android/src/main/java/expo/modules/orpheus/OrpheusConfig.kt +5 -0
  8. package/android/src/main/java/expo/modules/orpheus/OrpheusService.kt +142 -0
  9. package/android/src/main/java/expo/modules/orpheus/TrackRecord.kt +28 -0
  10. package/android/src/main/java/expo/modules/orpheus/bilibili/BilibiliApi.kt +24 -0
  11. package/android/src/main/java/expo/modules/orpheus/bilibili/BilibiliModels.kt +68 -0
  12. package/android/src/main/java/expo/modules/orpheus/bilibili/BilibiliRepository.kt +144 -0
  13. package/android/src/main/java/expo/modules/orpheus/bilibili/WbiUtil.kt +73 -0
  14. package/build/ExpoOrpheusModule.d.ts +98 -0
  15. package/build/ExpoOrpheusModule.d.ts.map +1 -0
  16. package/build/ExpoOrpheusModule.js +23 -0
  17. package/build/ExpoOrpheusModule.js.map +1 -0
  18. package/build/hooks/index.d.ts +7 -0
  19. package/build/hooks/index.d.ts.map +1 -0
  20. package/build/hooks/index.js +7 -0
  21. package/build/hooks/index.js.map +1 -0
  22. package/build/hooks/useCurrentTrack.d.ts +6 -0
  23. package/build/hooks/useCurrentTrack.d.ts.map +1 -0
  24. package/build/hooks/useCurrentTrack.js +41 -0
  25. package/build/hooks/useCurrentTrack.js.map +1 -0
  26. package/build/hooks/useIsPlaying.d.ts +2 -0
  27. package/build/hooks/useIsPlaying.d.ts.map +1 -0
  28. package/build/hooks/useIsPlaying.js +22 -0
  29. package/build/hooks/useIsPlaying.js.map +1 -0
  30. package/build/hooks/useOrpheus.d.ts +10 -0
  31. package/build/hooks/useOrpheus.d.ts.map +1 -0
  32. package/build/hooks/useOrpheus.js +20 -0
  33. package/build/hooks/useOrpheus.js.map +1 -0
  34. package/build/hooks/usePlaybackState.d.ts +3 -0
  35. package/build/hooks/usePlaybackState.d.ts.map +1 -0
  36. package/build/hooks/usePlaybackState.js +18 -0
  37. package/build/hooks/usePlaybackState.js.map +1 -0
  38. package/build/hooks/useProgress.d.ts +6 -0
  39. package/build/hooks/useProgress.d.ts.map +1 -0
  40. package/build/hooks/useProgress.js +59 -0
  41. package/build/hooks/useProgress.js.map +1 -0
  42. package/build/hooks/useShuffleMode.d.ts +2 -0
  43. package/build/hooks/useShuffleMode.d.ts.map +1 -0
  44. package/build/hooks/useShuffleMode.js +22 -0
  45. package/build/hooks/useShuffleMode.js.map +1 -0
  46. package/build/index.d.ts +3 -0
  47. package/build/index.d.ts.map +1 -0
  48. package/build/index.js +3 -0
  49. package/build/index.js.map +1 -0
  50. package/expo-module.config.json +6 -0
  51. package/package.json +44 -0
  52. package/src/ExpoOrpheusModule.ts +114 -0
  53. package/src/hooks/index.ts +6 -0
  54. package/src/hooks/useCurrentTrack.ts +46 -0
  55. package/src/hooks/useIsPlaying.ts +25 -0
  56. package/src/hooks/useOrpheus.ts +21 -0
  57. package/src/hooks/usePlaybackState.ts +21 -0
  58. package/src/hooks/useProgress.ts +71 -0
  59. package/src/hooks/useShuffleMode.ts +26 -0
  60. package/src/index.ts +2 -0
  61. package/tsconfig.json +9 -0
@@ -0,0 +1,59 @@
1
+ import { useEffect, useState, useRef } from "react";
2
+ import { AppState } from "react-native";
3
+ import { Orpheus } from "../ExpoOrpheusModule";
4
+ export function useProgress() {
5
+ const [progress, setProgress] = useState({
6
+ position: 0,
7
+ duration: 0,
8
+ buffered: 0,
9
+ });
10
+ const listenerRef = useRef(null);
11
+ const startListening = () => {
12
+ if (listenerRef.current)
13
+ return;
14
+ listenerRef.current = Orpheus.addListener("onPositionUpdate", (event) => {
15
+ setProgress({
16
+ position: event.position,
17
+ duration: event.duration,
18
+ buffered: event.buffered,
19
+ });
20
+ });
21
+ };
22
+ const stopListening = () => {
23
+ if (listenerRef.current) {
24
+ listenerRef.current.remove();
25
+ listenerRef.current = null;
26
+ }
27
+ };
28
+ const manualSync = () => {
29
+ Promise.all([Orpheus.getPosition(), Orpheus.getDuration()])
30
+ .then(([pos, dur]) => {
31
+ setProgress((prev) => ({
32
+ ...prev,
33
+ position: pos,
34
+ duration: dur,
35
+ }));
36
+ })
37
+ .catch((e) => console.warn("同步最新进度失败", e));
38
+ };
39
+ useEffect(() => {
40
+ manualSync();
41
+ startListening();
42
+ // === 监听 App 前后台切换 ===
43
+ const subscription = AppState.addEventListener("change", (nextAppState) => {
44
+ if (nextAppState === "active") {
45
+ manualSync();
46
+ startListening();
47
+ }
48
+ else {
49
+ stopListening();
50
+ }
51
+ });
52
+ return () => {
53
+ stopListening();
54
+ subscription.remove();
55
+ };
56
+ }, []);
57
+ return progress;
58
+ }
59
+ //# sourceMappingURL=useProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useProgress.js","sourceRoot":"","sources":["../../src/hooks/useProgress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAI/C,MAAM,UAAU,WAAW;IACzB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC;QACvC,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;KACZ,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,MAAM,CAA6B,IAAI,CAAC,CAAC;IAE7D,MAAM,cAAc,GAAG,GAAG,EAAE;QAC1B,IAAI,WAAW,CAAC,OAAO;YAAE,OAAO;QAEhC,WAAW,CAAC,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,EAAE;YACtE,WAAW,CAAC;gBACV,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,GAAG,EAAE;QACzB,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;YACxB,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC7B,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;aACxD,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE;YACnB,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACrB,GAAG,IAAI;gBACP,QAAQ,EAAE,GAAG;gBACb,QAAQ,EAAE,GAAG;aACd,CAAC,CAAC,CAAC;QACN,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,EAAE,CAAC;QACb,cAAc,EAAE,CAAC;QAEjB,uBAAuB;QACvB,MAAM,YAAY,GAAG,QAAQ,CAAC,gBAAgB,CAC5C,QAAQ,EACR,CAAC,YAA4B,EAAE,EAAE;YAC/B,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;gBAC9B,UAAU,EAAE,CAAC;gBACb,cAAc,EAAE,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,aAAa,EAAE,CAAC;YAClB,CAAC;QACH,CAAC,CACF,CAAC;QAEF,OAAO,GAAG,EAAE;YACV,aAAa,EAAE,CAAC;YAChB,YAAY,CAAC,MAAM,EAAE,CAAC;QACxB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["import { useEffect, useState, useRef } from \"react\";\nimport { AppState, AppStateStatus } from \"react-native\";\nimport { Orpheus } from \"../ExpoOrpheusModule\";\n\ntype OrpheusSubscription = ReturnType<typeof Orpheus.addListener>;\n\nexport function useProgress() {\n const [progress, setProgress] = useState({\n position: 0,\n duration: 0,\n buffered: 0,\n });\n\n const listenerRef = useRef<null | OrpheusSubscription>(null);\n\n const startListening = () => {\n if (listenerRef.current) return;\n\n listenerRef.current = Orpheus.addListener(\"onPositionUpdate\", (event) => {\n setProgress({\n position: event.position,\n duration: event.duration,\n buffered: event.buffered,\n });\n });\n };\n\n const stopListening = () => {\n if (listenerRef.current) {\n listenerRef.current.remove();\n listenerRef.current = null;\n }\n };\n\n const manualSync = () => {\n Promise.all([Orpheus.getPosition(), Orpheus.getDuration()])\n .then(([pos, dur]) => {\n setProgress((prev) => ({\n ...prev,\n position: pos,\n duration: dur,\n }));\n })\n .catch((e) => console.warn(\"同步最新进度失败\", e));\n };\n\n useEffect(() => {\n manualSync();\n startListening();\n\n // === 监听 App 前后台切换 ===\n const subscription = AppState.addEventListener(\n \"change\",\n (nextAppState: AppStateStatus) => {\n if (nextAppState === \"active\") {\n manualSync();\n startListening();\n } else {\n stopListening();\n }\n }\n );\n\n return () => {\n stopListening();\n subscription.remove();\n };\n }, []);\n\n return progress;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function useShuffleMode(): readonly [boolean, () => Promise<void>];
2
+ //# sourceMappingURL=useShuffleMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useShuffleMode.d.ts","sourceRoot":"","sources":["../../src/hooks/useShuffleMode.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,4CAsB7B"}
@@ -0,0 +1,22 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Orpheus } from "../ExpoOrpheusModule";
3
+ export function useShuffleMode() {
4
+ const [shuffleMode, setShuffleMode] = useState(false);
5
+ const refresh = async () => {
6
+ const val = await Orpheus.getShuffleMode();
7
+ setShuffleMode(val);
8
+ };
9
+ useEffect(() => {
10
+ refresh();
11
+ const sub = Orpheus.addListener("onTrackTransition", refresh);
12
+ return () => sub.remove();
13
+ }, []);
14
+ const toggleShuffle = async () => {
15
+ const newVal = !shuffleMode;
16
+ setShuffleMode(newVal);
17
+ await Orpheus.setShuffleMode(newVal);
18
+ refresh();
19
+ };
20
+ return [shuffleMode, toggleShuffle];
21
+ }
22
+ //# sourceMappingURL=useShuffleMode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useShuffleMode.js","sourceRoot":"","sources":["../../src/hooks/useShuffleMode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,MAAM,UAAU,cAAc;IAC5B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;QAC3C,cAAc,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,CAAC;QACV,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;QAC9D,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC5B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,MAAM,MAAM,GAAG,CAAC,WAAW,CAAC;QAC5B,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,MAAM,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACrC,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,OAAO,CAAC,WAAW,EAAE,aAAa,CAAU,CAAC;AAC/C,CAAC","sourcesContent":["import { useState, useEffect } from \"react\";\nimport { Orpheus } from \"../ExpoOrpheusModule\";\n\nexport function useShuffleMode() {\n const [shuffleMode, setShuffleMode] = useState(false);\n\n const refresh = async () => {\n const val = await Orpheus.getShuffleMode();\n setShuffleMode(val);\n };\n\n useEffect(() => {\n refresh();\n const sub = Orpheus.addListener(\"onTrackTransition\", refresh);\n return () => sub.remove();\n }, []);\n\n const toggleShuffle = async () => {\n const newVal = !shuffleMode;\n setShuffleMode(newVal);\n await Orpheus.setShuffleMode(newVal);\n refresh();\n };\n\n return [shuffleMode, toggleShuffle] as const;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export * from "./ExpoOrpheusModule";
2
+ export * from './hooks';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./ExpoOrpheusModule";
2
+ export * from './hooks';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC","sourcesContent":["export * from \"./ExpoOrpheusModule\";\nexport * from './hooks';"]}
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["android"],
3
+ "android": {
4
+ "modules": ["expo.modules.orpheus.ExpoOrpheusModule"]
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@roitium/expo-orpheus",
3
+ "version": "0.1.0",
4
+ "description": "A player for bbplayer",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:android": "open -a \"Android Studio\" example/android"
16
+ },
17
+ "keywords": [
18
+ "react-native",
19
+ "expo",
20
+ "expo-orpheus",
21
+ "ExpoOrpheus"
22
+ ],
23
+ "repository": "https://github.com/bbplayer-app/orpheus",
24
+ "bugs": {
25
+ "url": "https://github.com/bbplayer-app/orpheus/issues"
26
+ },
27
+ "author": "Roitium <65794453+roitium@users.noreply.github.com> (https://github.com/roitium)",
28
+ "license": "MIT",
29
+ "homepage": "https://github.com/bbplayer-app/orpheus#readme",
30
+ "dependencies": {
31
+ "prettier": "^3.7.4"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "~19.1.0",
35
+ "expo": "^54.0.24",
36
+ "expo-module-scripts": "^5.0.7",
37
+ "react-native": "0.81.5"
38
+ },
39
+ "peerDependencies": {
40
+ "expo": "*",
41
+ "react": "*",
42
+ "react-native": "*"
43
+ }
44
+ }
@@ -0,0 +1,114 @@
1
+ import { requireNativeModule, NativeModule } from "expo-modules-core";
2
+
3
+ export enum PlaybackState {
4
+ IDLE = 1,
5
+ BUFFERING = 2,
6
+ READY = 3,
7
+ ENDED = 4,
8
+ }
9
+
10
+ export enum RepeatMode {
11
+ OFF = 0,
12
+ TRACK = 1,
13
+ QUEUE = 2,
14
+ }
15
+
16
+ export enum TransitionReason {
17
+ REPEAT = 0,
18
+ AUTO = 1,
19
+ SEEK = 2,
20
+ PLAYLIST_CHANGED = 3,
21
+ }
22
+
23
+ export interface Track {
24
+ id: string;
25
+ url: string;
26
+ title?: string;
27
+ artist?: string;
28
+ artwork?: string;
29
+ duration?: number;
30
+ [key: string]: any;
31
+ }
32
+
33
+ export type OrpheusEvents = {
34
+ onPlaybackStateChanged(event: { state: PlaybackState }): void;
35
+ onTrackTransition(event: {
36
+ currentTrackId: string;
37
+ previousTrackId?: string;
38
+ reason: TransitionReason;
39
+ }): void;
40
+ onPlayerError(event: { code: string; message: string }): void;
41
+ onPositionUpdate(event: {
42
+ position: number;
43
+ duration: number;
44
+ buffered: number;
45
+ }): void;
46
+ onIsPlayingChanged(event: { status: boolean }): void;
47
+ };
48
+
49
+ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
50
+ /**
51
+ * 获取当前进度(秒)
52
+ */
53
+ getPosition(): Promise<number>;
54
+
55
+ /**
56
+ * 获取总时长(秒)
57
+ */
58
+ getDuration(): Promise<number>;
59
+
60
+ /**
61
+ * 获取是否正在播放
62
+ */
63
+ getIsPlaying(): Promise<boolean>;
64
+
65
+ /**
66
+ * 获取当前播放索引
67
+ */
68
+ getCurrentIndex(): Promise<number>;
69
+
70
+ /**
71
+ * 获取当前播放的 Track 对象
72
+ */
73
+ getCurrentTrack(): Promise<Track | null>;
74
+
75
+ /**
76
+ * 获取随机模式状态
77
+ */
78
+ getShuffleMode(): Promise<boolean>;
79
+
80
+ /**
81
+ * 获取指定索引的 Track
82
+ */
83
+ getIndexTrack(index: number): Promise<Track | null>;
84
+
85
+ setBilibiliCookie(cookie: string): void;
86
+
87
+ play(): Promise<void>;
88
+
89
+ pause(): Promise<void>;
90
+
91
+ clear(): Promise<void>;
92
+
93
+ skipTo(index: number): Promise<void>;
94
+
95
+ skipToNext(): Promise<void>;
96
+
97
+ skipToPrevious(): Promise<void>;
98
+
99
+ /**
100
+ * 跳转进度
101
+ * @param seconds 秒数
102
+ */
103
+ seekTo(seconds: number): Promise<void>;
104
+
105
+ setRepeatMode(mode: RepeatMode): Promise<void>;
106
+
107
+ setShuffleMode(enabled: boolean): Promise<void>;
108
+
109
+ getQueue(): Promise<Track[]>;
110
+
111
+ add(tracks: Track[]): Promise<void>;
112
+ }
113
+
114
+ export const Orpheus = requireNativeModule<OrpheusModule>("Orpheus");
@@ -0,0 +1,6 @@
1
+ export * from './useProgress';
2
+ export * from './usePlaybackState';
3
+ export * from './useIsPlaying';
4
+ export * from './useCurrentTrack';
5
+ export * from './useShuffleMode';
6
+ export * from './useOrpheus';
@@ -0,0 +1,46 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Track, Orpheus } from "../ExpoOrpheusModule";
3
+
4
+ export function useCurrentTrack() {
5
+ const [track, setTrack] = useState<Track | null>(null);
6
+ const [index, setIndex] = useState<number>(-1);
7
+
8
+ const fetchTrack = async () => {
9
+ try {
10
+ const [currentTrack, currentIndex] = await Promise.all([
11
+ Orpheus.getCurrentTrack(),
12
+ Orpheus.getCurrentIndex(),
13
+ ]);
14
+ return { currentTrack, currentIndex };
15
+ } catch (e) {
16
+ console.warn("Failed to fetch current track", e);
17
+ return { currentTrack: null, currentIndex: -1 };
18
+ }
19
+ };
20
+
21
+ useEffect(() => {
22
+ let isMounted = true;
23
+
24
+ fetchTrack().then(({ currentTrack, currentIndex }) => {
25
+ if (isMounted) {
26
+ setTrack(currentTrack);
27
+ setIndex(currentIndex);
28
+ }
29
+ });
30
+
31
+ const sub = Orpheus.addListener("onTrackTransition", async () => {
32
+ const { currentTrack, currentIndex } = await fetchTrack();
33
+ if (isMounted) {
34
+ setTrack(currentTrack);
35
+ setIndex(currentIndex);
36
+ }
37
+ });
38
+
39
+ return () => {
40
+ isMounted = false;
41
+ sub.remove();
42
+ };
43
+ }, []);
44
+
45
+ return { track, index };
46
+ }
@@ -0,0 +1,25 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Orpheus } from "../ExpoOrpheusModule";
3
+
4
+ export function useIsPlaying() {
5
+ const [isPlaying, setIsPlaying] = useState(false);
6
+
7
+ useEffect(() => {
8
+ let isMounted = true;
9
+
10
+ Orpheus.getIsPlaying().then((val) => {
11
+ if (isMounted) setIsPlaying(val);
12
+ });
13
+
14
+ const sub = Orpheus.addListener("onIsPlayingChanged", (event) => {
15
+ if (isMounted) setIsPlaying(event.status);
16
+ });
17
+
18
+ return () => {
19
+ isMounted = false;
20
+ sub.remove();
21
+ };
22
+ }, []);
23
+
24
+ return isPlaying;
25
+ }
@@ -0,0 +1,21 @@
1
+ import { useCurrentTrack } from "./useCurrentTrack";
2
+ import { useIsPlaying } from "./useIsPlaying";
3
+ import { usePlaybackState } from "./usePlaybackState";
4
+ import { useProgress } from "./useProgress";
5
+
6
+ export function useOrpheus() {
7
+ const state = usePlaybackState();
8
+ const isPlaying = useIsPlaying();
9
+ const progress = useProgress();
10
+ const { track, index } = useCurrentTrack();
11
+
12
+ return {
13
+ state,
14
+ isPlaying,
15
+ position: progress.position,
16
+ duration: progress.duration,
17
+ buffered: progress.buffered,
18
+ currentTrack: track,
19
+ currentIndex: index,
20
+ };
21
+ }
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Orpheus, PlaybackState } from "../ExpoOrpheusModule";
3
+
4
+ export function usePlaybackState() {
5
+ const [state, setState] = useState<PlaybackState>(PlaybackState.IDLE);
6
+
7
+ useEffect(() => {
8
+ let isMounted = true;
9
+
10
+ const sub = Orpheus.addListener("onPlaybackStateChanged", (event) => {
11
+ if (isMounted) setState(event.state);
12
+ });
13
+
14
+ return () => {
15
+ isMounted = false;
16
+ sub.remove();
17
+ };
18
+ }, []);
19
+
20
+ return state;
21
+ }
@@ -0,0 +1,71 @@
1
+ import { useEffect, useState, useRef } from "react";
2
+ import { AppState, AppStateStatus } from "react-native";
3
+ import { Orpheus } from "../ExpoOrpheusModule";
4
+
5
+ type OrpheusSubscription = ReturnType<typeof Orpheus.addListener>;
6
+
7
+ export function useProgress() {
8
+ const [progress, setProgress] = useState({
9
+ position: 0,
10
+ duration: 0,
11
+ buffered: 0,
12
+ });
13
+
14
+ const listenerRef = useRef<null | OrpheusSubscription>(null);
15
+
16
+ const startListening = () => {
17
+ if (listenerRef.current) return;
18
+
19
+ listenerRef.current = Orpheus.addListener("onPositionUpdate", (event) => {
20
+ setProgress({
21
+ position: event.position,
22
+ duration: event.duration,
23
+ buffered: event.buffered,
24
+ });
25
+ });
26
+ };
27
+
28
+ const stopListening = () => {
29
+ if (listenerRef.current) {
30
+ listenerRef.current.remove();
31
+ listenerRef.current = null;
32
+ }
33
+ };
34
+
35
+ const manualSync = () => {
36
+ Promise.all([Orpheus.getPosition(), Orpheus.getDuration()])
37
+ .then(([pos, dur]) => {
38
+ setProgress((prev) => ({
39
+ ...prev,
40
+ position: pos,
41
+ duration: dur,
42
+ }));
43
+ })
44
+ .catch((e) => console.warn("同步最新进度失败", e));
45
+ };
46
+
47
+ useEffect(() => {
48
+ manualSync();
49
+ startListening();
50
+
51
+ // === 监听 App 前后台切换 ===
52
+ const subscription = AppState.addEventListener(
53
+ "change",
54
+ (nextAppState: AppStateStatus) => {
55
+ if (nextAppState === "active") {
56
+ manualSync();
57
+ startListening();
58
+ } else {
59
+ stopListening();
60
+ }
61
+ }
62
+ );
63
+
64
+ return () => {
65
+ stopListening();
66
+ subscription.remove();
67
+ };
68
+ }, []);
69
+
70
+ return progress;
71
+ }
@@ -0,0 +1,26 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Orpheus } from "../ExpoOrpheusModule";
3
+
4
+ export function useShuffleMode() {
5
+ const [shuffleMode, setShuffleMode] = useState(false);
6
+
7
+ const refresh = async () => {
8
+ const val = await Orpheus.getShuffleMode();
9
+ setShuffleMode(val);
10
+ };
11
+
12
+ useEffect(() => {
13
+ refresh();
14
+ const sub = Orpheus.addListener("onTrackTransition", refresh);
15
+ return () => sub.remove();
16
+ }, []);
17
+
18
+ const toggleShuffle = async () => {
19
+ const newVal = !shuffleMode;
20
+ setShuffleMode(newVal);
21
+ await Orpheus.setShuffleMode(newVal);
22
+ refresh();
23
+ };
24
+
25
+ return [shuffleMode, toggleShuffle] as const;
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./ExpoOrpheusModule";
2
+ export * from './hooks';
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
9
+ }