@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.
- package/.eslintrc.js +5 -0
- package/README.md +11 -0
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +21 -0
- package/android/src/main/java/expo/modules/orpheus/ExpoOrpheusModule.kt +365 -0
- package/android/src/main/java/expo/modules/orpheus/NetworkModule.kt +46 -0
- package/android/src/main/java/expo/modules/orpheus/OrpheusConfig.kt +5 -0
- package/android/src/main/java/expo/modules/orpheus/OrpheusService.kt +142 -0
- package/android/src/main/java/expo/modules/orpheus/TrackRecord.kt +28 -0
- package/android/src/main/java/expo/modules/orpheus/bilibili/BilibiliApi.kt +24 -0
- package/android/src/main/java/expo/modules/orpheus/bilibili/BilibiliModels.kt +68 -0
- package/android/src/main/java/expo/modules/orpheus/bilibili/BilibiliRepository.kt +144 -0
- package/android/src/main/java/expo/modules/orpheus/bilibili/WbiUtil.kt +73 -0
- package/build/ExpoOrpheusModule.d.ts +98 -0
- package/build/ExpoOrpheusModule.d.ts.map +1 -0
- package/build/ExpoOrpheusModule.js +23 -0
- package/build/ExpoOrpheusModule.js.map +1 -0
- package/build/hooks/index.d.ts +7 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +7 -0
- package/build/hooks/index.js.map +1 -0
- package/build/hooks/useCurrentTrack.d.ts +6 -0
- package/build/hooks/useCurrentTrack.d.ts.map +1 -0
- package/build/hooks/useCurrentTrack.js +41 -0
- package/build/hooks/useCurrentTrack.js.map +1 -0
- package/build/hooks/useIsPlaying.d.ts +2 -0
- package/build/hooks/useIsPlaying.d.ts.map +1 -0
- package/build/hooks/useIsPlaying.js +22 -0
- package/build/hooks/useIsPlaying.js.map +1 -0
- package/build/hooks/useOrpheus.d.ts +10 -0
- package/build/hooks/useOrpheus.d.ts.map +1 -0
- package/build/hooks/useOrpheus.js +20 -0
- package/build/hooks/useOrpheus.js.map +1 -0
- package/build/hooks/usePlaybackState.d.ts +3 -0
- package/build/hooks/usePlaybackState.d.ts.map +1 -0
- package/build/hooks/usePlaybackState.js +18 -0
- package/build/hooks/usePlaybackState.js.map +1 -0
- package/build/hooks/useProgress.d.ts +6 -0
- package/build/hooks/useProgress.d.ts.map +1 -0
- package/build/hooks/useProgress.js +59 -0
- package/build/hooks/useProgress.js.map +1 -0
- package/build/hooks/useShuffleMode.d.ts +2 -0
- package/build/hooks/useShuffleMode.d.ts.map +1 -0
- package/build/hooks/useShuffleMode.js +22 -0
- package/build/hooks/useShuffleMode.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +44 -0
- package/src/ExpoOrpheusModule.ts +114 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useCurrentTrack.ts +46 -0
- package/src/hooks/useIsPlaying.ts +25 -0
- package/src/hooks/useOrpheus.ts +21 -0
- package/src/hooks/usePlaybackState.ts +21 -0
- package/src/hooks/useProgress.ts +71 -0
- package/src/hooks/useShuffleMode.ts +26 -0
- package/src/index.ts +2 -0
- 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 @@
|
|
|
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"]}
|
package/build/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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';"]}
|
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,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
package/tsconfig.json
ADDED