mirror of
https://github.com/foss42/apidash.git
synced 2025-10-17 19:52:17 +08:00
Feat: Audio Player
This commit is contained in:
@ -78,6 +78,7 @@ const kTabHeight = 45.0;
|
||||
const kHeaderHeight = 32.0;
|
||||
const kSegmentHeight = 24.0;
|
||||
const kTextButtonMinWidth = 36.0;
|
||||
const kSliderWidth = 160.0;
|
||||
|
||||
const kRandMax = 100000;
|
||||
|
||||
@ -424,6 +425,9 @@ const kUnexpectedRaiseIssue =
|
||||
const kImageError =
|
||||
"There seems to be an issue rendering this image. Please raise an issue in API Dash GitHub repo so that we can resolve it.";
|
||||
|
||||
const kAudioError =
|
||||
"There seems to be an issue playing this audio. Please raise an issue in API Dash GitHub repo so that we can resolve it.";
|
||||
|
||||
const kRaiseIssue =
|
||||
"\nPlease raise an issue in API Dash GitHub repo so that we can resolve it.";
|
||||
|
||||
|
@ -24,6 +24,14 @@ String humanizeDuration(Duration? duration) {
|
||||
}
|
||||
}
|
||||
|
||||
String audioPosition(Duration? duration) {
|
||||
if (duration == null) return "";
|
||||
var min = duration.inMinutes;
|
||||
var secs = duration.inSeconds.remainder(60);
|
||||
var secondsPadding = secs < 10 ? "0" : "";
|
||||
return "$min:$secondsPadding$secs";
|
||||
}
|
||||
|
||||
String capitalizeFirstLetter(String? text) {
|
||||
if (text == null || text == "") {
|
||||
return "";
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'error_message.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'uint8_audio_player.dart';
|
||||
|
||||
class Previewer extends StatefulWidget {
|
||||
const Previewer({
|
||||
@ -36,7 +37,14 @@ class _PreviewerState extends State<Previewer> {
|
||||
// TODO: PDF Viewer
|
||||
}
|
||||
if (widget.type == kTypeAudio) {
|
||||
// TODO: Audio Player
|
||||
return Uint8AudioPlayer(
|
||||
bytes: widget.bytes,
|
||||
type: widget.type,
|
||||
subtype: widget.subtype,
|
||||
errorBuilder: (context, _, stacktrace) {
|
||||
return const ErrorMessage(message: kAudioError);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (widget.type == kTypeVideo) {
|
||||
// TODO: Video Player
|
||||
|
230
lib/widgets/uint8_audio_player.dart
Normal file
230
lib/widgets/uint8_audio_player.dart
Normal file
@ -0,0 +1,230 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
typedef AudioErrorWidgetBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Object error,
|
||||
StackTrace? stackTrace,
|
||||
);
|
||||
|
||||
// Uint8List AudioSource for just_audio
|
||||
class Uint8AudioSource extends StreamAudioSource {
|
||||
Uint8AudioSource(this.bytes, {this.type = 'audio', this.subtype = 'mpeg'});
|
||||
|
||||
final List<int> bytes;
|
||||
final String? type;
|
||||
final String? subtype;
|
||||
|
||||
@override
|
||||
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
||||
start ??= 0;
|
||||
end ??= bytes.length;
|
||||
return StreamAudioResponse(
|
||||
sourceLength: bytes.length,
|
||||
contentLength: end - start,
|
||||
offset: start,
|
||||
stream: Stream.value(bytes.sublist(start, end)),
|
||||
contentType: '$type/$subtype',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Uint8AudioPlayer extends StatefulWidget {
|
||||
/// Creates a widget for playing audio obtained from a [Uint8List].
|
||||
const Uint8AudioPlayer({
|
||||
super.key,
|
||||
required this.bytes,
|
||||
this.type,
|
||||
this.subtype,
|
||||
required this.errorBuilder,
|
||||
});
|
||||
|
||||
final Uint8List bytes;
|
||||
final String? type;
|
||||
final String? subtype;
|
||||
final AudioErrorWidgetBuilder errorBuilder;
|
||||
|
||||
@override
|
||||
State<Uint8AudioPlayer> createState() => _Uint8AudioPlayerState();
|
||||
}
|
||||
|
||||
class _Uint8AudioPlayerState extends State<Uint8AudioPlayer> {
|
||||
final player = AudioPlayer();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
player.setAudioSource(Uint8AudioSource(widget.bytes,
|
||||
type: widget.type, subtype: widget.subtype));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<PlayerState>(
|
||||
stream: player.playerStateStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return widget.errorBuilder(
|
||||
context, snapshot.error!, snapshot.stackTrace);
|
||||
} else {
|
||||
final playerState = snapshot.data;
|
||||
final processingState = playerState?.processingState;
|
||||
if (processingState == ProcessingState.ready ||
|
||||
processingState == ProcessingState.completed ||
|
||||
processingState == ProcessingState.buffering) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Audio Player
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Duration Position Builder (time elapsed / total duration)
|
||||
_buildDuration(
|
||||
player.positionStream,
|
||||
maxDuration: player.duration,
|
||||
),
|
||||
|
||||
// Slider to view & change Duration Position
|
||||
_buildPositionBar(
|
||||
player.positionStream,
|
||||
maxDuration: player.duration,
|
||||
onChanged: (value) =>
|
||||
player.seek(Duration(seconds: value.toInt())),
|
||||
),
|
||||
|
||||
// Total Duration
|
||||
Text(
|
||||
audioPosition(player.duration),
|
||||
style: TextStyle(fontFamily: kCodeStyle.fontFamily),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Audio Player Controls
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Play/Pause Button
|
||||
_buildPlayButton(
|
||||
player.playingStream,
|
||||
play: player.play,
|
||||
pause: player.pause,
|
||||
restart: () => player.seek(Duration.zero),
|
||||
completed: processingState == ProcessingState.completed,
|
||||
),
|
||||
|
||||
// Mute/UnMute button
|
||||
_buildVolumeButton(
|
||||
player.volumeStream,
|
||||
mute: () => player.setVolume(0),
|
||||
unmute: () => player.setVolume(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<bool> _buildPlayButton(
|
||||
Stream<bool> stream, {
|
||||
VoidCallback? play,
|
||||
VoidCallback? pause,
|
||||
VoidCallback? restart,
|
||||
required bool completed,
|
||||
}) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: stream,
|
||||
builder: (context, snapshot) {
|
||||
final playing = snapshot.data;
|
||||
if (playing != true) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: play,
|
||||
);
|
||||
} else if (completed) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.replay),
|
||||
onPressed: restart,
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.pause),
|
||||
onPressed: pause,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<Duration> _buildDuration(
|
||||
Stream<Duration> stream, {
|
||||
Duration? maxDuration,
|
||||
}) {
|
||||
return StreamBuilder<Duration>(
|
||||
stream: stream,
|
||||
builder: (context, snapshot) {
|
||||
final position = snapshot.data;
|
||||
return Text(
|
||||
audioPosition(position),
|
||||
style: TextStyle(fontFamily: kCodeStyle.fontFamily),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<Duration> _buildPositionBar(
|
||||
Stream<Duration> stream, {
|
||||
Duration? maxDuration,
|
||||
ValueChanged<double>? onChanged,
|
||||
}) {
|
||||
return StreamBuilder<Duration>(
|
||||
stream: stream,
|
||||
builder: (context, snapshot) {
|
||||
return SizedBox(
|
||||
width: kSliderWidth,
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackShape: const RectangularSliderTrackShape(),
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 16.0),
|
||||
),
|
||||
child: Slider(
|
||||
value: snapshot.data?.inSeconds.toDouble() ?? 0,
|
||||
max: maxDuration?.inSeconds.toDouble() ?? 0,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<double> _buildVolumeButton(Stream<double> stream,
|
||||
{VoidCallback? mute, VoidCallback? unmute}) {
|
||||
return StreamBuilder<double>(
|
||||
stream: stream,
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.data == 0
|
||||
? IconButton(icon: const Icon(Icons.volume_off), onPressed: unmute)
|
||||
: IconButton(icon: const Icon(Icons.volume_up), onPressed: mute);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -16,3 +16,4 @@ export 'request_widgets.dart';
|
||||
export 'response_widgets.dart';
|
||||
export 'snackbars.dart';
|
||||
export 'markdown.dart';
|
||||
export 'uint8_audio_player.dart';
|
||||
|
40
pubspec.lock
40
pubspec.lock
@ -41,6 +41,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
audio_session:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_session
|
||||
sha256: "655343841a723646f74932215c5785ef7156b76d2de4b99bcd3205476f08dc61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.15"
|
||||
axis_layout:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -360,6 +368,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.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:
|
||||
@ -584,6 +616,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.27.7"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -33,6 +33,7 @@ dependencies:
|
||||
path: ^1.8.2
|
||||
flutter_markdown: ^0.6.14
|
||||
markdown: ^7.1.0
|
||||
just_audio: ^0.9.34
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user