Compare commits
2 Commits
fix-favori
...
feature-au
Author | SHA1 | Date | |
---|---|---|---|
cb3fd24cf7 | |||
3e8216923f |
@ -1,4 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.guozhigq.pilipala">
|
||||
<queries>
|
||||
<intent>
|
||||
@ -63,6 +64,27 @@
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- ADD THIS "SERVICE" element -->
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- ADD THIS "RECEIVER" element -->
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@ -265,4 +287,8 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
</manifest>
|
||||
|
@ -37,5 +37,11 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
'AUDIO_SESSION_MICROPHONE=0'
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,10 @@
|
||||
PODS:
|
||||
- appscheme (1.0.4):
|
||||
- Flutter
|
||||
- audio_service (0.0.1):
|
||||
- Flutter
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
@ -14,8 +18,13 @@ PODS:
|
||||
- FMDB/standard (2.7.5)
|
||||
- gt3_flutter_plugin (0.0.8):
|
||||
- Flutter
|
||||
<<<<<<< HEAD
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
=======
|
||||
- GT3Captcha-iOS
|
||||
- GT3Captcha-iOS (0.15.8.3)
|
||||
>>>>>>> main
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
@ -56,11 +65,18 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||
<<<<<<< HEAD
|
||||
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
|
||||
- just_audio (from `.symlinks/plugins/just_audio/ios`)
|
||||
=======
|
||||
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
|
||||
>>>>>>> main
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
@ -88,6 +104,10 @@ SPEC REPOS:
|
||||
EXTERNAL SOURCES:
|
||||
appscheme:
|
||||
:path: ".symlinks/plugins/appscheme/ios"
|
||||
audio_service:
|
||||
:path: ".symlinks/plugins/audio_service/ios"
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
@ -96,8 +116,15 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_volume_controller:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
<<<<<<< HEAD
|
||||
image_gallery_saver:
|
||||
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
||||
just_audio:
|
||||
:path: ".symlinks/plugins/just_audio/ios"
|
||||
=======
|
||||
gt3_flutter_plugin:
|
||||
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
|
||||
>>>>>>> main
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
@ -135,13 +162,20 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
<<<<<<< HEAD
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
|
||||
=======
|
||||
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
|
||||
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
||||
>>>>>>> main
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
@ -161,6 +195,6 @@ SPEC CHECKSUMS:
|
||||
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
|
||||
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
|
||||
PODFILE CHECKSUM: fc8a34c4ba2e14d31df90bf03cf419a764f2778c
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
|
@ -103,6 +103,10 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<<<<<<< HEAD
|
||||
<!-- audio service配置 -->
|
||||
=======
|
||||
>>>>>>> main
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
|
@ -1,5 +1,9 @@
|
||||
<<<<<<< HEAD
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
=======
|
||||
import 'dart:io';
|
||||
|
||||
>>>>>>> main
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@ -29,7 +33,22 @@ void main() async {
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||
.then((_) async {
|
||||
await GStrorage.init();
|
||||
<<<<<<< HEAD
|
||||
|
||||
await AudioService.init<AudioHandler>(
|
||||
builder: () => MyAudioHandler(),
|
||||
config: const AudioServiceConfig(
|
||||
androidNotificationChannelId: 'com.guozhigq.pilipala.channel.audio',
|
||||
androidNotificationChannelName: 'Music playback',
|
||||
androidNotificationOngoing: true,
|
||||
androidStopForegroundOnPause: true,
|
||||
androidNotificationIcon: 'drawable/audio_service_icon',
|
||||
),
|
||||
);
|
||||
|
||||
=======
|
||||
await setupServiceLocator();
|
||||
>>>>>>> main
|
||||
runApp(const MyApp());
|
||||
// 小白条、导航栏沉浸
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
@ -157,3 +176,34 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyAudioHandler extends BaseAudioHandler
|
||||
with
|
||||
QueueHandler, // mix in default queue callback implementations
|
||||
SeekHandler {
|
||||
// mix in default seek callback implementations
|
||||
|
||||
// The most common callbacks:
|
||||
@override
|
||||
Future<void> play() async {
|
||||
print('play');
|
||||
// All 'play' requests from all origins route to here. Implement this
|
||||
// callback to start playing audio appropriate to your app. e.g. music.
|
||||
}
|
||||
|
||||
///
|
||||
@override
|
||||
Future<void> pause() async {}
|
||||
|
||||
///
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
|
||||
///
|
||||
@override
|
||||
Future<void> seek(Duration position) async {}
|
||||
|
||||
///
|
||||
@override
|
||||
Future<void> skipToQueueItem(int i) async {}
|
||||
}
|
||||
|
0
lib/pages/audio/controller.dart
Normal file
0
lib/pages/audio/controller.dart
Normal file
4
lib/pages/audio/index.dart
Normal file
4
lib/pages/audio/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library pl_audio_player;
|
||||
|
||||
export './view.dart';
|
||||
export './controller.dart';
|
539
lib/pages/audio/view.dart
Normal file
539
lib/pages/audio/view.dart
Normal file
@ -0,0 +1,539 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
// import 'package:just_audio_background/just_audio_background.dart';
|
||||
|
||||
class AudioPlayerPage extends StatefulWidget {
|
||||
const AudioPlayerPage({super.key});
|
||||
|
||||
@override
|
||||
State<AudioPlayerPage> createState() => _AudioPlayerPageState();
|
||||
}
|
||||
|
||||
class _AudioPlayerPageState extends State<AudioPlayerPage> {
|
||||
static int _nextMediaId = 0;
|
||||
late AudioPlayer _player;
|
||||
final _playlist = ConcatenatingAudioSource(children: [
|
||||
ClippingAudioSource(
|
||||
start: const Duration(seconds: 0),
|
||||
end: const Duration(seconds: 90),
|
||||
child: AudioSource.uri(Uri.parse(
|
||||
"https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3")),
|
||||
tag: MediaItem(
|
||||
id: '${_nextMediaId++}',
|
||||
album: "Science Friday",
|
||||
title: "A Salute To Head-Scratching Science (30 seconds)",
|
||||
artUri: Uri.parse(
|
||||
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||
),
|
||||
),
|
||||
// AudioSource.uri(
|
||||
// Uri.parse(
|
||||
// "https://upos-sz-mirror08c.bilivideo.com/upgcxcode/05/52/1205825205/1205825205-1-16.mp4?e=ig8euxZM2rNcNbRVhwdVhwdlhWdVhwdVhoNvNC8BqJIzNbfq9rVEuxTEnE8L5F6VnEsSTx0vkX8fqJeYTj_lta53NCM=&uipk=5&nbs=1&deadline=1693821903&gen=playurlv2&os=08cbv&oi=1865700872&trid=bfc9c19f85c545dd8f4794ff97f4f57fh&mid=17340771&platform=html5&upsig=9bf98515091bb8a80e1950a03a2a0d68&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,mid,platform&bvc=vod&nettype=0&f=h_0_0&bw=49663&logo=80000000"),
|
||||
// headers: {
|
||||
// 'user-agent':
|
||||
// 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
|
||||
// 'referer': 'https://www.bilibili.com'
|
||||
// },
|
||||
// tag: MediaItem(
|
||||
// id: '${_nextMediaId++}',
|
||||
// album: "Science Friday",
|
||||
// title: "A Salute To Head-Scratching Science",
|
||||
// artUri: Uri.parse(
|
||||
// "https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||
// ),
|
||||
// ),
|
||||
AudioSource.uri(
|
||||
Uri.parse("https://s3.amazonaws.com/scifri-segments/scifri201711241.mp3"),
|
||||
tag: MediaItem(
|
||||
id: '${_nextMediaId++}',
|
||||
album: "Science Friday",
|
||||
title: "From Cat Rheology To Operatic Incompetence",
|
||||
artUri: Uri.parse(
|
||||
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||
),
|
||||
),
|
||||
AudioSource.uri(
|
||||
Uri.parse("asset:///audio/nature.mp3"),
|
||||
tag: MediaItem(
|
||||
id: '${_nextMediaId++}',
|
||||
album: "Public Domain",
|
||||
title: "Nature Sounds",
|
||||
artUri: Uri.parse(
|
||||
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_player = AudioPlayer();
|
||||
|
||||
_init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final session = await AudioSession.instance;
|
||||
await session.configure(const AudioSessionConfiguration.speech());
|
||||
// Listen to errors during playback.
|
||||
_player.playbackEventStream.listen((event) {},
|
||||
onError: (Object e, StackTrace stackTrace) {
|
||||
print('A stream error occurred: $e');
|
||||
});
|
||||
try {
|
||||
await _player.setAudioSource(_playlist);
|
||||
} catch (e, stackTrace) {
|
||||
// Catch load errors: 404, invalid url ...
|
||||
print("Error loading playlist: $e");
|
||||
print(stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// Stream<PositionData> get _positionDataStream =>
|
||||
// Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
|
||||
// _player.positionStream,
|
||||
// _player.bufferedPositionStream,
|
||||
// _player.durationStream,
|
||||
// (position, bufferedPosition, duration) => PositionData(
|
||||
// position, bufferedPosition, duration ?? Duration.zero));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: StreamBuilder<SequenceState?>(
|
||||
stream: _player.sequenceStateStream,
|
||||
builder: (context, snapshot) {
|
||||
final state = snapshot.data;
|
||||
if (state?.sequence.isEmpty ?? true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final metadata = state!.currentSource!.tag as MediaItem;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: Image.network(metadata.artUri.toString())),
|
||||
),
|
||||
),
|
||||
Text(metadata.album!,
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(metadata.title),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ControlButtons(_player),
|
||||
// StreamBuilder<PositionData>(
|
||||
// stream: _positionDataStream,
|
||||
// builder: (context, snapshot) {
|
||||
// final positionData = snapshot.data;
|
||||
// return SeekBar(
|
||||
// duration: positionData?.duration ?? Duration.zero,
|
||||
// position: positionData?.position ?? Duration.zero,
|
||||
// bufferedPosition:
|
||||
// positionData?.bufferedPosition ?? Duration.zero,
|
||||
// onChangeEnd: (newPosition) {
|
||||
// _player.seek(newPosition);
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
children: [
|
||||
StreamBuilder<LoopMode>(
|
||||
stream: _player.loopModeStream,
|
||||
builder: (context, snapshot) {
|
||||
final loopMode = snapshot.data ?? LoopMode.off;
|
||||
const icons = [
|
||||
Icon(Icons.repeat, color: Colors.grey),
|
||||
Icon(Icons.repeat, color: Colors.orange),
|
||||
Icon(Icons.repeat_one, color: Colors.orange),
|
||||
];
|
||||
const cycleModes = [
|
||||
LoopMode.off,
|
||||
LoopMode.all,
|
||||
LoopMode.one,
|
||||
];
|
||||
final index = cycleModes.indexOf(loopMode);
|
||||
return IconButton(
|
||||
icon: icons[index],
|
||||
onPressed: () {
|
||||
_player.setLoopMode(cycleModes[
|
||||
(cycleModes.indexOf(loopMode) + 1) %
|
||||
cycleModes.length]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Playlist",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
StreamBuilder<bool>(
|
||||
stream: _player.shuffleModeEnabledStream,
|
||||
builder: (context, snapshot) {
|
||||
final shuffleModeEnabled = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
icon: shuffleModeEnabled
|
||||
? const Icon(Icons.shuffle, color: Colors.orange)
|
||||
: const Icon(Icons.shuffle, color: Colors.grey),
|
||||
onPressed: () async {
|
||||
final enable = !shuffleModeEnabled;
|
||||
if (enable) {
|
||||
await _player.shuffle();
|
||||
}
|
||||
await _player.setShuffleModeEnabled(enable);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 240.0,
|
||||
child: StreamBuilder<SequenceState?>(
|
||||
stream: _player.sequenceStateStream,
|
||||
builder: (context, snapshot) {
|
||||
final state = snapshot.data;
|
||||
final sequence = state?.sequence ?? [];
|
||||
return ReorderableListView(
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
if (oldIndex < newIndex) newIndex--;
|
||||
_playlist.move(oldIndex, newIndex);
|
||||
},
|
||||
children: [
|
||||
for (var i = 0; i < sequence.length; i++)
|
||||
Dismissible(
|
||||
key: ValueKey(sequence[i]),
|
||||
background: Container(
|
||||
color: Colors.redAccent,
|
||||
alignment: Alignment.centerRight,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 8.0),
|
||||
child: Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
),
|
||||
onDismissed: (dismissDirection) {
|
||||
_playlist.removeAt(i);
|
||||
},
|
||||
child: Material(
|
||||
color: i == state!.currentIndex
|
||||
? Colors.grey.shade300
|
||||
: null,
|
||||
child: ListTile(
|
||||
title: Text(sequence[i].tag.title as String),
|
||||
onTap: () {
|
||||
_player.seek(Duration.zero, index: i);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ControlButtons extends StatelessWidget {
|
||||
final AudioPlayer player;
|
||||
|
||||
const ControlButtons(this.player, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.volume_up),
|
||||
onPressed: () {
|
||||
showSliderDialog(
|
||||
context: context,
|
||||
title: "Adjust volume",
|
||||
divisions: 10,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
stream: player.volumeStream,
|
||||
onChanged: player.setVolume,
|
||||
);
|
||||
},
|
||||
),
|
||||
StreamBuilder<SequenceState?>(
|
||||
stream: player.sequenceStateStream,
|
||||
builder: (context, snapshot) => IconButton(
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
onPressed: player.hasPrevious ? player.seekToPrevious : null,
|
||||
),
|
||||
),
|
||||
StreamBuilder<PlayerState>(
|
||||
stream: player.playerStateStream,
|
||||
builder: (context, snapshot) {
|
||||
final playerState = snapshot.data;
|
||||
final processingState = playerState?.processingState;
|
||||
final playing = playerState?.playing;
|
||||
if (processingState == ProcessingState.loading ||
|
||||
processingState == ProcessingState.buffering) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
width: 64.0,
|
||||
height: 64.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
} else if (playing != true) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
iconSize: 64.0,
|
||||
onPressed: player.play,
|
||||
);
|
||||
} else if (processingState != ProcessingState.completed) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.pause),
|
||||
iconSize: 64.0,
|
||||
onPressed: player.pause,
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.replay),
|
||||
iconSize: 64.0,
|
||||
onPressed: () => player.seek(Duration.zero,
|
||||
index: player.effectiveIndices!.first),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
StreamBuilder<SequenceState?>(
|
||||
stream: player.sequenceStateStream,
|
||||
builder: (context, snapshot) => IconButton(
|
||||
icon: const Icon(Icons.skip_next),
|
||||
onPressed: player.hasNext ? player.seekToNext : null,
|
||||
),
|
||||
),
|
||||
StreamBuilder<double>(
|
||||
stream: player.speedStream,
|
||||
builder: (context, snapshot) => IconButton(
|
||||
icon: Text("${snapshot.data?.toStringAsFixed(1)}x",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
onPressed: () {
|
||||
showSliderDialog(
|
||||
context: context,
|
||||
title: "Adjust speed",
|
||||
divisions: 10,
|
||||
min: 0.5,
|
||||
max: 1.5,
|
||||
stream: player.speedStream,
|
||||
onChanged: player.setSpeed,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showSliderDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required int divisions,
|
||||
required double min,
|
||||
required double max,
|
||||
String valueSuffix = '',
|
||||
required Stream<double> stream,
|
||||
required ValueChanged<double> onChanged,
|
||||
}) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title, textAlign: TextAlign.center),
|
||||
content: StreamBuilder<double>(
|
||||
stream: stream,
|
||||
builder: (context, snapshot) => SizedBox(
|
||||
height: 100.0,
|
||||
child: Column(
|
||||
children: [
|
||||
Text('${snapshot.data?.toStringAsFixed(1)}$valueSuffix',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fixed',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24.0)),
|
||||
Slider(
|
||||
divisions: divisions,
|
||||
min: min,
|
||||
max: max,
|
||||
value: snapshot.data ?? 1.0,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class PositionData {
|
||||
final Duration position;
|
||||
final Duration bufferedPosition;
|
||||
final Duration duration;
|
||||
|
||||
PositionData(this.position, this.bufferedPosition, this.duration);
|
||||
}
|
||||
|
||||
class SeekBar extends StatefulWidget {
|
||||
final Duration duration;
|
||||
final Duration position;
|
||||
final Duration bufferedPosition;
|
||||
final ValueChanged<Duration>? onChanged;
|
||||
final ValueChanged<Duration>? onChangeEnd;
|
||||
|
||||
const SeekBar({
|
||||
Key? key,
|
||||
required this.duration,
|
||||
required this.position,
|
||||
required this.bufferedPosition,
|
||||
this.onChanged,
|
||||
this.onChangeEnd,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
SeekBarState createState() => SeekBarState();
|
||||
}
|
||||
|
||||
class SeekBarState extends State<SeekBar> {
|
||||
double? _dragValue;
|
||||
late SliderThemeData _sliderThemeData;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
_sliderThemeData = SliderTheme.of(context).copyWith(
|
||||
trackHeight: 2.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: _sliderThemeData.copyWith(
|
||||
thumbShape: HiddenThumbComponentShape(),
|
||||
activeTrackColor: Colors.blue.shade100,
|
||||
inactiveTrackColor: Colors.grey.shade300,
|
||||
),
|
||||
child: ExcludeSemantics(
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
max: widget.duration.inMilliseconds.toDouble(),
|
||||
value: min(widget.bufferedPosition.inMilliseconds.toDouble(),
|
||||
widget.duration.inMilliseconds.toDouble()),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dragValue = value;
|
||||
});
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(Duration(milliseconds: value.round()));
|
||||
}
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
if (widget.onChangeEnd != null) {
|
||||
widget.onChangeEnd!(Duration(milliseconds: value.round()));
|
||||
}
|
||||
_dragValue = null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliderTheme(
|
||||
data: _sliderThemeData.copyWith(
|
||||
inactiveTrackColor: Colors.transparent,
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
max: widget.duration.inMilliseconds.toDouble(),
|
||||
value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(),
|
||||
widget.duration.inMilliseconds.toDouble()),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dragValue = value;
|
||||
});
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(Duration(milliseconds: value.round()));
|
||||
}
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
if (widget.onChangeEnd != null) {
|
||||
widget.onChangeEnd!(Duration(milliseconds: value.round()));
|
||||
}
|
||||
_dragValue = null;
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 16.0,
|
||||
bottom: 0.0,
|
||||
child: Text(
|
||||
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
|
||||
.firstMatch("$_remaining")
|
||||
?.group(1) ??
|
||||
'$_remaining',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Duration get _remaining => widget.duration - widget.position;
|
||||
}
|
||||
|
||||
class HiddenThumbComponentShape extends SliderComponentShape {
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {}
|
||||
}
|
@ -354,6 +354,28 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 105,
|
||||
bottom: 10,
|
||||
child: TextButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty
|
||||
.resolveWith((states) {
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer;
|
||||
}),
|
||||
),
|
||||
onPressed: () =>
|
||||
Get.toNamed('/audioPlayer'),
|
||||
icon: const Icon(
|
||||
Icons.multitrack_audio_outlined,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text('音频'),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: 10,
|
||||
@ -372,7 +394,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
Icons.play_circle_outline,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text('Play'),
|
||||
label: const Text('视频'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/pages/about/index.dart';
|
||||
import 'package:pilipala/pages/audio/index.dart';
|
||||
import 'package:pilipala/pages/blacklist/index.dart';
|
||||
import 'package:pilipala/pages/dynamics/deatil/index.dart';
|
||||
import 'package:pilipala/pages/dynamics/index.dart';
|
||||
@ -114,6 +115,10 @@ class Routes {
|
||||
name: '/displayModeSetting', page: () => const SetDiaplayMode()),
|
||||
// 关于
|
||||
CustomGetPage(name: '/about', page: () => const AboutPage()),
|
||||
<<<<<<< HEAD
|
||||
// 音频播放
|
||||
CustomGetPage(name: '/audioPlayer', page: () => const AudioPlayerPage()),
|
||||
=======
|
||||
//
|
||||
CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()),
|
||||
// 历史记录搜索
|
||||
@ -125,6 +130,7 @@ class Routes {
|
||||
CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()),
|
||||
// 登录页面
|
||||
CustomGetPage(name: '/loginPage', page: () => const LoginPage()),
|
||||
>>>>>>> main
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import connectivity_plus
|
||||
import device_info_plus
|
||||
import dynamic_color
|
||||
import flutter_volume_controller
|
||||
import just_audio
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
import package_info_plus
|
||||
@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||
FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
|
31
pubspec.lock
31
pubspec.lock
@ -69,10 +69,17 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_service
|
||||
<<<<<<< HEAD
|
||||
sha256: dc3d10682b688f4779e0f4662586f1385f0c09169a6bbc42dcc193b9d4b6113f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.11"
|
||||
=======
|
||||
sha256: a4d989f1225ea9621898d60f23236dcbfc04876fa316086c23c5c4af075dbac4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.12"
|
||||
>>>>>>> main
|
||||
audio_service_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -749,6 +756,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
just_audio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio
|
||||
sha256: "890cd0fc41a1a4530c171e375a2a3fb6a09d84e9d508c5195f40bcff54330327"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.34"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_platform_interface
|
||||
sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
just_audio_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_web
|
||||
sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.8"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -121,6 +121,8 @@ dependencies:
|
||||
ref: master
|
||||
# 状态栏图标控制
|
||||
status_bar_control: ^3.2.1
|
||||
# 音频播放
|
||||
just_audio: ^0.9.34
|
||||
# 代理
|
||||
system_proxy: ^0.1.0
|
||||
# pip
|
||||
|
Reference in New Issue
Block a user