mirror of
https://github.com/foss42/apidash.git
synced 2025-06-06 11:28:07 +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,
|
kSubTypeSvg: kCodeRawBodyViewOptions,
|
||||||
},
|
},
|
||||||
kTypeAudio: {
|
kTypeAudio: {
|
||||||
kSubTypeDefaultViewOptions: kNoBodyViewOptions,
|
kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
|
||||||
},
|
},
|
||||||
kTypeVideo: {
|
kTypeVideo: {
|
||||||
kSubTypeDefaultViewOptions: kNoBodyViewOptions,
|
kSubTypeDefaultViewOptions: kNoBodyViewOptions,
|
||||||
@ -432,6 +432,9 @@ const kUnexpectedRaiseIssue =
|
|||||||
const kImageError =
|
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.";
|
"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 =
|
const kRaiseIssue =
|
||||||
"\nPlease raise an issue in API Dash GitHub repo so that we can resolve it.";
|
"\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) {
|
String capitalizeFirstLetter(String? text) {
|
||||||
if (text == null || text == "") {
|
if (text == null || text == "") {
|
||||||
return "";
|
return "";
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'error_message.dart';
|
import 'error_message.dart';
|
||||||
import 'package:apidash/consts.dart';
|
import 'package:apidash/consts.dart';
|
||||||
|
import 'uint8_audio_player.dart';
|
||||||
|
|
||||||
class Previewer extends StatefulWidget {
|
class Previewer extends StatefulWidget {
|
||||||
const Previewer({
|
const Previewer({
|
||||||
@ -36,7 +37,14 @@ class _PreviewerState extends State<Previewer> {
|
|||||||
// TODO: PDF Viewer
|
// TODO: PDF Viewer
|
||||||
}
|
}
|
||||||
if (widget.type == kTypeAudio) {
|
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) {
|
if (widget.type == kTypeVideo) {
|
||||||
// TODO: Video Player
|
// 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 'snackbars.dart';
|
||||||
export 'markdown.dart';
|
export 'markdown.dart';
|
||||||
export 'view_codepane.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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.0"
|
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:
|
axis_layout:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -153,6 +161,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
|
eventify:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: eventify
|
||||||
|
sha256: b829429f08586cc2001c628e7499e3e3c2493a1d895fd73b00ecb23351aa5a66
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -360,6 +376,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
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:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -432,6 +488,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
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:
|
multi_split_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -584,6 +648,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.3.4"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.27.7"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -34,6 +34,9 @@ dependencies:
|
|||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
markdown: ^7.1.0
|
markdown: ^7.1.0
|
||||||
|
just_audio: ^0.9.34
|
||||||
|
just_audio_mpv: ^0.1.6
|
||||||
|
just_audio_windows: ^0.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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:apidash/consts.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../test_consts.dart';
|
import '../test_consts.dart';
|
||||||
@ -33,9 +33,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(find.byType(Uint8AudioPlayer), findsOneWidget);
|
||||||
find.text("${kMimeTypeRaiseIssueStart}audio/mpeg$kMimeTypeRaiseIssue"),
|
|
||||||
findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Testing when type/subtype is video/H264', (tester) async {
|
testWidgets('Testing when type/subtype is video/H264', (tester) async {
|
||||||
@ -120,4 +118,21 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text(kImageError), findsOneWidget);
|
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