mirror of
https://github.com/foss42/apidash.git
synced 2025-06-03 08:16:25 +08:00
Merge branch 'add-feature-codegendropdown' of https://github.com/mmjsmohit/api-dash into add-feature-codegendropdown
This commit is contained in:
@ -319,7 +319,7 @@ const Map<String, Map<String, List<ResponseBodyView>>>
|
||||
kSubTypeSvg: kCodeRawBodyViewOptions,
|
||||
},
|
||||
kTypeAudio: {
|
||||
kSubTypeDefaultViewOptions: kNoBodyViewOptions,
|
||||
kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
|
||||
},
|
||||
kTypeVideo: {
|
||||
kSubTypeDefaultViewOptions: kNoBodyViewOptions,
|
||||
@ -432,6 +432,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, error, stacktrace) {
|
||||
return const ErrorMessage(message: kAudioError);
|
||||
},
|
||||
);
|
||||
}
|
||||
if (widget.type == kTypeVideo) {
|
||||
// TODO: Video Player
|
||||
|
240
lib/widgets/uint8_audio_player.dart
Normal file
240
lib/widgets/uint8_audio_player.dart
Normal file
@ -0,0 +1,240 @@
|
||||
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, {required this.contentType});
|
||||
|
||||
final List<int> bytes;
|
||||
final String contentType;
|
||||
|
||||
@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: contentType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Uint8AudioPlayer extends StatefulWidget {
|
||||
/// Creates a widget for playing audio obtained from a [Uint8List].
|
||||
const Uint8AudioPlayer({
|
||||
super.key,
|
||||
required this.bytes,
|
||||
required this.type,
|
||||
required 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,
|
||||
contentType: '${widget.type}/${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
|
||||
Padding(
|
||||
padding: kPh20v10,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Duration Position Builder (time elapsed)
|
||||
_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 if (processingState == ProcessingState.idle) {
|
||||
// Error in Loading AudioSource
|
||||
return widget.errorBuilder(
|
||||
context,
|
||||
ErrorDescription('${player.audioSource} Loading Error'),
|
||||
snapshot.stackTrace,
|
||||
);
|
||||
} 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 Flexible(
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -17,3 +17,4 @@ export 'response_widgets.dart';
|
||||
export 'snackbars.dart';
|
||||
export 'markdown.dart';
|
||||
export 'view_codepane.dart';
|
||||
export 'uint8_audio_player.dart';
|
||||
|
72
pubspec.lock
72
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:
|
||||
@ -153,6 +161,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
eventify:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: eventify
|
||||
sha256: b829429f08586cc2001c628e7499e3e3c2493a1d895fd73b00ecb23351aa5a66
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -360,6 +376,46 @@ 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_mpv:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio_mpv
|
||||
sha256: "98ac36712f3fe4fb0cf545f29c250fbd55e52c8445a4b0a4ee0bc9322f192797"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
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"
|
||||
just_audio_windows:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio_windows
|
||||
sha256: "7b8801f3987e98a2002cd23b5600b2daf162248ff1413266fb44c84448c1c0d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -432,6 +488,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
mpv_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mpv_dart
|
||||
sha256: a33bd9a68439b496b7a5f36fecd3aa3cf6cbf0176ae15b9b60b12ae96e58f5a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.1"
|
||||
multi_split_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -584,6 +648,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:
|
||||
|
@ -34,6 +34,9 @@ dependencies:
|
||||
path: ^1.8.2
|
||||
flutter_markdown: ^0.6.14
|
||||
markdown: ^7.1.0
|
||||
just_audio: ^0.9.34
|
||||
just_audio_mpv: ^0.1.6
|
||||
just_audio_windows: ^0.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:apidash/widgets/previewer.dart';
|
||||
import 'package:apidash/widgets/widgets.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../test_consts.dart';
|
||||
@ -33,9 +33,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text("${kMimeTypeRaiseIssueStart}audio/mpeg$kMimeTypeRaiseIssue"),
|
||||
findsOneWidget);
|
||||
expect(find.byType(Uint8AudioPlayer), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Testing when type/subtype is video/H264', (tester) async {
|
||||
@ -120,4 +118,21 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(kImageError), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Testing when type/subtype is audio/mpeg corrupted',
|
||||
(tester) async {
|
||||
Uint8List bytesAudioCorrupt =
|
||||
Uint8List.fromList(List.generate(100, (index) => index));
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
title: 'Previewer',
|
||||
home: Scaffold(
|
||||
body: Previewer(
|
||||
type: 'audio', subtype: 'mpeg', bytes: bytesAudioCorrupt),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(kAudioError), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user