Merge branch 'add-feature-codegendropdown' of https://github.com/mmjsmohit/api-dash into add-feature-codegendropdown

This commit is contained in:
mmjsmohit
2023-06-05 09:40:56 +05:30
8 changed files with 356 additions and 6 deletions

View File

@ -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.";

View File

@ -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 "";

View File

@ -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

View 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);
},
);
}
}

View File

@ -17,3 +17,4 @@ export 'response_widgets.dart';
export 'snackbars.dart';
export 'markdown.dart';
export 'view_codepane.dart';
export 'uint8_audio_player.dart';

View File

@ -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:

View File

@ -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:

View File

@ -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);
});
}