mirror of
https://github.com/foss42/apidash.git
synced 2025-06-29 20:37:12 +08:00
Merge branch 'add-ui-tests' of https://github.com/sixtusagbo/apidash into add-ui-tests
This commit is contained in:
12
README.md
12
README.md
@ -157,7 +157,16 @@ Here is the complete list of mimetypes that can be directly previewed in API Das
|
|||||||
| File Type | Mimetype | Extension | Comment |
|
| File Type | Mimetype | Extension | Comment |
|
||||||
| --------- | -------------------------- | ----------------- | -------- |
|
| --------- | -------------------------- | ----------------- | -------- |
|
||||||
| PDF | `application/pdf` | `.pdf` | |
|
| PDF | `application/pdf` | `.pdf` | |
|
||||||
| CSV | `text/csv` | `.csv` | Can be improved |
|
| Video | `video/mp4` | `.mp4` | |
|
||||||
|
| Video | `video/webm` | `.webm` | |
|
||||||
|
| Video | `video/x-ms-wmv` | `.wmv` | |
|
||||||
|
| Video | `video/x-ms-asf` | `.wmv` | |
|
||||||
|
| Video | `video/avi` | `.avi` | |
|
||||||
|
| Video | `video/msvideo` | `.avi` | |
|
||||||
|
| Video | `video/x-msvideo` | `.avi` | |
|
||||||
|
| Video | `video/quicktime` | `.mov` | |
|
||||||
|
| Video | `video/x-quicktime` | `.mov` | |
|
||||||
|
| Video | `video/x-matroska` | `.mkv` | |
|
||||||
| Image | `image/apng` | `.apng` | Animated |
|
| Image | `image/apng` | `.apng` | Animated |
|
||||||
| Image | `image/avif` | `.avif` | |
|
| Image | `image/avif` | `.avif` | |
|
||||||
| Image | `image/bmp` | `.bmp` | |
|
| Image | `image/bmp` | `.bmp` | |
|
||||||
@ -188,6 +197,7 @@ Here is the complete list of mimetypes that can be directly previewed in API Das
|
|||||||
| Audio | `audio/x-m4a` | `.m4a` | |
|
| Audio | `audio/x-m4a` | `.m4a` | |
|
||||||
| Audio | `audio/wav` | `.wav` | |
|
| Audio | `audio/wav` | `.wav` | |
|
||||||
| Audio | `audio/wave` | `.wav` | |
|
| Audio | `audio/wave` | `.wav` | |
|
||||||
|
| CSV | `text/csv` | `.csv` | Can be improved |
|
||||||
|
|
||||||
We welcome PRs to add support for previewing other multimedia mimetypes. Please go ahead and raise an issue so that we can discuss the approach.
|
We welcome PRs to add support for previewing other multimedia mimetypes. Please go ahead and raise an issue so that we can discuss the approach.
|
||||||
We are adding support for other mimetypes with each release. But, if you are looking for any particular mimetype support, please go ahead and open an issue. We will prioritize it's addition.
|
We are adding support for other mimetypes with each release. But, if you are looking for any particular mimetype support, please go ahead and open an issue. We will prioritize it's addition.
|
||||||
|
@ -57,7 +57,7 @@ class _AppState extends ConsumerState<App> with WindowListener {
|
|||||||
bool isPreventClose = await windowManager.isPreventClose();
|
bool isPreventClose = await windowManager.isPreventClose();
|
||||||
if (isPreventClose) {
|
if (isPreventClose) {
|
||||||
if (ref.watch(
|
if (ref.watch(
|
||||||
settingsProvider.select((value) => value.promptBeforeClosing))) {
|
settingsProvider.select((value) => value.promptBeforeClosing)) && ref.watch(hasUnsavedChangesProvider)) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => AlertDialog(
|
builder: (_) => AlertDialog(
|
||||||
|
@ -21,7 +21,7 @@ final kIsLinux = !kIsWeb && Platform.isLinux;
|
|||||||
final kIsApple = !kIsWeb && (Platform.isIOS || Platform.isMacOS);
|
final kIsApple = !kIsWeb && (Platform.isIOS || Platform.isMacOS);
|
||||||
final kIsDesktop =
|
final kIsDesktop =
|
||||||
!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
|
!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
|
||||||
|
final kIsRunningTests = Platform.environment.containsKey('FLUTTER_TEST');
|
||||||
final kIsIOS = !kIsWeb && Platform.isIOS;
|
final kIsIOS = !kIsWeb && Platform.isIOS;
|
||||||
final kIsAndroid = !kIsWeb && Platform.isAndroid;
|
final kIsAndroid = !kIsWeb && Platform.isAndroid;
|
||||||
final kIsMobile = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
final kIsMobile = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||||
@ -403,7 +403,7 @@ const Map<String, Map<String, List<ResponseBodyView>>>
|
|||||||
kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
|
kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
|
||||||
},
|
},
|
||||||
kTypeVideo: {
|
kTypeVideo: {
|
||||||
kSubTypeDefaultViewOptions: kNoBodyViewOptions,
|
kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
|
||||||
},
|
},
|
||||||
kTypeText: {
|
kTypeText: {
|
||||||
kSubTypeDefaultViewOptions: kRawBodyViewOptions,
|
kSubTypeDefaultViewOptions: kRawBodyViewOptions,
|
||||||
@ -512,6 +512,9 @@ const kMimeTypeRaiseIssue =
|
|||||||
const kUnexpectedRaiseIssue =
|
const kUnexpectedRaiseIssue =
|
||||||
"\nIf the behaviour is unexpected, please raise an issue in API Dash GitHub repo so that we can resolve it.";
|
"\nIf the behaviour is unexpected, please raise an issue in API Dash GitHub repo so that we can resolve it.";
|
||||||
|
|
||||||
|
const kVideoError =
|
||||||
|
"There seems to be an issue playing this video. Please raise an issue in API Dash GitHub repo so that we can resolve it.";
|
||||||
|
|
||||||
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.";
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ class RequestModel {
|
|||||||
this.message,
|
this.message,
|
||||||
this.responseModel,
|
this.responseModel,
|
||||||
this.isWorking = false,
|
this.isWorking = false,
|
||||||
|
this.sendingTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
@ -50,6 +51,7 @@ class RequestModel {
|
|||||||
final String? message;
|
final String? message;
|
||||||
final ResponseModel? responseModel;
|
final ResponseModel? responseModel;
|
||||||
final bool isWorking;
|
final bool isWorking;
|
||||||
|
final DateTime? sendingTime;
|
||||||
|
|
||||||
List<NameValueModel>? get enabledRequestHeaders =>
|
List<NameValueModel>? get enabledRequestHeaders =>
|
||||||
getEnabledRows(requestHeaders, isHeaderEnabledList);
|
getEnabledRows(requestHeaders, isHeaderEnabledList);
|
||||||
@ -135,6 +137,7 @@ class RequestModel {
|
|||||||
String? message,
|
String? message,
|
||||||
ResponseModel? responseModel,
|
ResponseModel? responseModel,
|
||||||
bool? isWorking,
|
bool? isWorking,
|
||||||
|
DateTime? sendingTime,
|
||||||
}) {
|
}) {
|
||||||
var headers = requestHeaders ?? this.requestHeaders;
|
var headers = requestHeaders ?? this.requestHeaders;
|
||||||
var params = requestParams ?? this.requestParams;
|
var params = requestParams ?? this.requestParams;
|
||||||
@ -160,6 +163,7 @@ class RequestModel {
|
|||||||
message: message ?? this.message,
|
message: message ?? this.message,
|
||||||
responseModel: responseModel ?? this.responseModel,
|
responseModel: responseModel ?? this.responseModel,
|
||||||
isWorking: isWorking ?? this.isWorking,
|
isWorking: isWorking ?? this.isWorking,
|
||||||
|
sendingTime: sendingTime ?? this.sendingTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +66,7 @@ class CollectionStateNotifier
|
|||||||
.read(requestSequenceProvider.notifier)
|
.read(requestSequenceProvider.notifier)
|
||||||
.update((state) => [id, ...state]);
|
.update((state) => [id, ...state]);
|
||||||
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
|
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reorder(int oldIdx, int newIdx) {
|
void reorder(int oldIdx, int newIdx) {
|
||||||
@ -73,6 +74,7 @@ class CollectionStateNotifier
|
|||||||
final itemId = itemIds.removeAt(oldIdx);
|
final itemId = itemIds.removeAt(oldIdx);
|
||||||
itemIds.insert(newIdx, itemId);
|
itemIds.insert(newIdx, itemId);
|
||||||
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove(String id) {
|
void remove(String id) {
|
||||||
@ -95,6 +97,7 @@ class CollectionStateNotifier
|
|||||||
var map = {...state!};
|
var map = {...state!};
|
||||||
map.remove(id);
|
map.remove(id);
|
||||||
state = map;
|
state = map;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearResponse(String? id) {
|
void clearResponse(String? id) {
|
||||||
@ -108,6 +111,7 @@ class CollectionStateNotifier
|
|||||||
var map = {...state!};
|
var map = {...state!};
|
||||||
map[id] = newModel;
|
map[id] = newModel;
|
||||||
state = map;
|
state = map;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void duplicate(String id) {
|
void duplicate(String id) {
|
||||||
@ -127,6 +131,7 @@ class CollectionStateNotifier
|
|||||||
|
|
||||||
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
||||||
ref.read(selectedIdStateProvider.notifier).state = newId;
|
ref.read(selectedIdStateProvider.notifier).state = newId;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(
|
void update(
|
||||||
@ -168,6 +173,7 @@ class CollectionStateNotifier
|
|||||||
var map = {...state!};
|
var map = {...state!};
|
||||||
map[id] = newModel;
|
map[id] = newModel;
|
||||||
state = map;
|
state = map;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendRequest(String id) async {
|
Future<void> sendRequest(String id) async {
|
||||||
@ -182,7 +188,10 @@ class CollectionStateNotifier
|
|||||||
|
|
||||||
// set current model's isWorking to true and update state
|
// set current model's isWorking to true and update state
|
||||||
var map = {...state!};
|
var map = {...state!};
|
||||||
map[id] = requestModel.copyWith(isWorking: true);
|
map[id] = requestModel.copyWith(
|
||||||
|
isWorking: true,
|
||||||
|
sendingTime: DateTime.now(),
|
||||||
|
);
|
||||||
state = map;
|
state = map;
|
||||||
|
|
||||||
(http.Response?, Duration?, String?)? responseRec = await request(
|
(http.Response?, Duration?, String?)? responseRec = await request(
|
||||||
@ -214,6 +223,7 @@ class CollectionStateNotifier
|
|||||||
map = {...state!};
|
map = {...state!};
|
||||||
map[id] = newRequestModel;
|
map[id] = newRequestModel;
|
||||||
state = map;
|
state = map;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearData() async {
|
Future<void> clearData() async {
|
||||||
@ -223,6 +233,7 @@ class CollectionStateNotifier
|
|||||||
ref.read(clearDataStateProvider.notifier).state = false;
|
ref.read(clearDataStateProvider.notifier).state = false;
|
||||||
ref.read(requestSequenceProvider.notifier).state = [];
|
ref.read(requestSequenceProvider.notifier).state = [];
|
||||||
state = {};
|
state = {};
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool loadData() {
|
bool loadData() {
|
||||||
@ -263,6 +274,7 @@ class CollectionStateNotifier
|
|||||||
}
|
}
|
||||||
await hiveHandler.removeUnused();
|
await hiveHandler.removeUnused();
|
||||||
ref.read(saveDataStateProvider.notifier).state = false;
|
ref.read(saveDataStateProvider.notifier).state = false;
|
||||||
|
ref.read(hasUnsavedChangesProvider.notifier).state = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> exportDataToHAR() async {
|
Future<Map<String, dynamic>> exportDataToHAR() async {
|
||||||
|
@ -6,6 +6,8 @@ final selectedIdEditStateProvider = StateProvider<String?>((ref) => null);
|
|||||||
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
||||||
|
final hasUnsavedChangesProvider = StateProvider<bool>((ref) => false);
|
||||||
|
|
||||||
// final nameTextFieldControllerProvider =
|
// final nameTextFieldControllerProvider =
|
||||||
// StateProvider.autoDispose<TextEditingController>((ref) {
|
// StateProvider.autoDispose<TextEditingController>((ref) {
|
||||||
// TextEditingController controller = TextEditingController(text: "");
|
// TextEditingController controller = TextEditingController(text: "");
|
||||||
|
@ -15,6 +15,7 @@ class CollectionPane extends ConsumerWidget {
|
|||||||
final overlayWidget = OverlayWidgetTemplate(context: context);
|
final overlayWidget = OverlayWidgetTemplate(context: context);
|
||||||
final collection = ref.watch(collectionStateNotifierProvider);
|
final collection = ref.watch(collectionStateNotifierProvider);
|
||||||
final savingData = ref.watch(saveDataStateProvider);
|
final savingData = ref.watch(saveDataStateProvider);
|
||||||
|
final hasUnsavedChanges = ref.watch(hasUnsavedChangesProvider);
|
||||||
if (collection == null) {
|
if (collection == null) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
@ -31,7 +32,7 @@ class CollectionPane extends ConsumerWidget {
|
|||||||
alignment: WrapAlignment.spaceBetween,
|
alignment: WrapAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: savingData
|
onPressed: (savingData || !hasUnsavedChanges)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
overlayWidget.show(
|
overlayWidget.show(
|
||||||
|
@ -12,12 +12,17 @@ class ResponsePane extends ConsumerWidget {
|
|||||||
final isWorking = ref.watch(
|
final isWorking = ref.watch(
|
||||||
selectedRequestModelProvider.select((value) => value?.isWorking)) ??
|
selectedRequestModelProvider.select((value) => value?.isWorking)) ??
|
||||||
false;
|
false;
|
||||||
|
final startSendingTime = ref.watch(
|
||||||
|
selectedRequestModelProvider.select((value) => value?.sendingTime));
|
||||||
final responseStatus = ref.watch(
|
final responseStatus = ref.watch(
|
||||||
selectedRequestModelProvider.select((value) => value?.responseStatus));
|
selectedRequestModelProvider.select((value) => value?.responseStatus));
|
||||||
final message = ref
|
final message = ref
|
||||||
.watch(selectedRequestModelProvider.select((value) => value?.message));
|
.watch(selectedRequestModelProvider.select((value) => value?.message));
|
||||||
|
|
||||||
if (isWorking) {
|
if (isWorking) {
|
||||||
return const SendingWidget();
|
return SendingWidget(
|
||||||
|
startSendingTime: startSendingTime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (responseStatus == null) {
|
if (responseStatus == null) {
|
||||||
return const NotSentWidget();
|
return const NotSentWidget();
|
||||||
|
@ -8,6 +8,7 @@ import 'error_message.dart';
|
|||||||
import 'uint8_audio_player.dart';
|
import 'uint8_audio_player.dart';
|
||||||
import 'json_previewer.dart';
|
import 'json_previewer.dart';
|
||||||
import 'csv_previewer.dart';
|
import 'csv_previewer.dart';
|
||||||
|
import 'video_previewer.dart';
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
|
||||||
class Previewer extends StatefulWidget {
|
class Previewer extends StatefulWidget {
|
||||||
@ -86,7 +87,12 @@ class _PreviewerState extends State<Previewer> {
|
|||||||
return CsvPreviewer(body: widget.body);
|
return CsvPreviewer(body: widget.body);
|
||||||
}
|
}
|
||||||
if (widget.type == kTypeVideo) {
|
if (widget.type == kTypeVideo) {
|
||||||
// TODO: Video Player
|
try {
|
||||||
|
var preview = VideoPreviewer(videoBytes: widget.bytes);
|
||||||
|
return preview;
|
||||||
|
} catch (e) {
|
||||||
|
return const ErrorMessage(message: kVideoError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
String message = widget.hasRaw
|
String message = widget.hasRaw
|
||||||
? "$kMimeTypeRawRaiseIssueStart${widget.type}/${widget.subtype}$kMimeTypeRaiseIssue"
|
? "$kMimeTypeRawRaiseIssueStart${widget.type}/${widget.subtype}$kMimeTypeRaiseIssue"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
@ -33,18 +34,80 @@ class NotSentWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SendingWidget extends StatelessWidget {
|
class SendingWidget extends StatefulWidget {
|
||||||
const SendingWidget({super.key});
|
final DateTime? startSendingTime;
|
||||||
|
const SendingWidget({
|
||||||
|
super.key,
|
||||||
|
required this.startSendingTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SendingWidget> createState() => _SendingWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SendingWidgetState extends State<SendingWidget> {
|
||||||
|
int _millisecondsElapsed = 0;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.startSendingTime != null) {
|
||||||
|
_millisecondsElapsed =
|
||||||
|
(DateTime.now().difference(widget.startSendingTime!).inMilliseconds ~/
|
||||||
|
100) *
|
||||||
|
100;
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 100), _updateTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateTimer(Timer timer) {
|
||||||
|
setState(() {
|
||||||
|
_millisecondsElapsed += 100;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_timer != null && _timer!.isActive) _timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return Stack(
|
||||||
child: Column(
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Lottie.asset(kAssetSendingLottie),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: kPh20t40,
|
||||||
|
child: Visibility(
|
||||||
|
visible: _millisecondsElapsed >= 0,
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Lottie.asset(kAssetSendingLottie),
|
Icon(
|
||||||
|
Icons.alarm,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
softWrap: false,
|
||||||
|
style: kTextStyleButton.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
147
lib/widgets/video_previewer.dart
Normal file
147
lib/widgets/video_previewer.dart
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:apidash/consts.dart';
|
||||||
|
import 'package:fvp/fvp.dart' as fvp;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class VideoPreviewer extends StatefulWidget {
|
||||||
|
const VideoPreviewer({
|
||||||
|
super.key,
|
||||||
|
required this.videoBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Uint8List videoBytes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VideoPreviewer> createState() => _VideoPreviewerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPreviewerState extends State<VideoPreviewer> {
|
||||||
|
VideoPlayerController? _videoController;
|
||||||
|
bool _isPlaying = false;
|
||||||
|
File? _tempVideoFile;
|
||||||
|
bool _showControls = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
registerWithAllPlatforms();
|
||||||
|
_initializeVideoPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerWithAllPlatforms() {
|
||||||
|
try {
|
||||||
|
fvp.registerWith();
|
||||||
|
} catch (e) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeVideoPlayer() async {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
_tempVideoFile = File(
|
||||||
|
'${tempDir.path}/temp_video_${DateTime.now().millisecondsSinceEpoch}');
|
||||||
|
try {
|
||||||
|
await _tempVideoFile?.writeAsBytes(widget.videoBytes);
|
||||||
|
_videoController = VideoPlayerController.file(_tempVideoFile!)
|
||||||
|
..initialize().then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_videoController!.play();
|
||||||
|
_videoController!.setLooping(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final iconColor = Theme.of(context).iconTheme.color;
|
||||||
|
final progressBarColors = VideoProgressColors(
|
||||||
|
playedColor: iconColor!,
|
||||||
|
bufferedColor: iconColor.withOpacity(0.5),
|
||||||
|
backgroundColor: iconColor.withOpacity(0.3),
|
||||||
|
);
|
||||||
|
return Scaffold(
|
||||||
|
body: MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _showControls = true),
|
||||||
|
onExit: (_) => setState(() => _showControls = false),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: _videoController?.value.isInitialized == true
|
||||||
|
? AspectRatio(
|
||||||
|
aspectRatio: _videoController!.value.aspectRatio,
|
||||||
|
child: VideoPlayer(_videoController!),
|
||||||
|
)
|
||||||
|
: const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: _videoController?.value.isInitialized == true
|
||||||
|
? SizedBox(
|
||||||
|
height: 50.0,
|
||||||
|
child: VideoProgressIndicator(
|
||||||
|
_videoController!,
|
||||||
|
allowScrubbing: true,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
colors: progressBarColors,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(height: 0),
|
||||||
|
),
|
||||||
|
if (_showControls)
|
||||||
|
Center(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (_videoController!.value.isPlaying) {
|
||||||
|
_videoController!.pause();
|
||||||
|
} else {
|
||||||
|
_videoController!.play();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isPlaying = !_isPlaying;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Icon(
|
||||||
|
_isPlaying ? Icons.play_arrow : Icons.pause,
|
||||||
|
size: 64,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_videoController?.pause();
|
||||||
|
_videoController?.dispose();
|
||||||
|
if (!kIsRunningTests) {
|
||||||
|
Future.delayed(const Duration(seconds: 1), () async {
|
||||||
|
try {
|
||||||
|
if (_tempVideoFile != null) {
|
||||||
|
await _tempVideoFile!.delete();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
64
pubspec.lock
64
pubspec.lock
@ -225,6 +225,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
csv:
|
csv:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -504,6 +512,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
|
fvp:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fvp
|
||||||
|
sha256: "995328479ba4641da6760ddc84a168db157a3b9db4f0417fa68713d99344a146"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -552,6 +568,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.4"
|
||||||
html_unescape:
|
html_unescape:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1350,6 +1374,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.8.3"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.12"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.2"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -44,6 +44,9 @@ dependencies:
|
|||||||
package_info_plus: ^5.0.1
|
package_info_plus: ^5.0.1
|
||||||
flutter_typeahead: ^5.2.0
|
flutter_typeahead: ^5.2.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
|
fvp: ^0.14.0
|
||||||
|
video_player: ^2.3.2
|
||||||
|
video_player_platform_interface: ^6.2.2
|
||||||
json_data_explorer:
|
json_data_explorer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/foss42/json_data_explorer.git
|
url: https://github.com/foss42/json_data_explorer.git
|
||||||
|
@ -5,6 +5,7 @@ import 'package:apidash/consts.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:printing/printing.dart' show PdfPreview;
|
import 'package:printing/printing.dart' show PdfPreview;
|
||||||
import 'package:flutter_svg/flutter_svg.dart' show SvgPicture;
|
import 'package:flutter_svg/flutter_svg.dart' show SvgPicture;
|
||||||
|
import 'package:apidash/widgets/video_previewer.dart';
|
||||||
import '../test_consts.dart';
|
import '../test_consts.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -63,10 +64,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
expect(find.byType(VideoPreviewer), findsOneWidget);
|
||||||
expect(
|
|
||||||
find.text("${kMimeTypeRaiseIssueStart}video/H264$kMimeTypeRaiseIssue"),
|
|
||||||
findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Testing when type/subtype is model/step+xml', (tester) async {
|
testWidgets('Testing when type/subtype is model/step+xml', (tester) async {
|
||||||
|
@ -10,13 +10,15 @@ import 'package:apidash/models/models.dart';
|
|||||||
import '../test_consts.dart';
|
import '../test_consts.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Testing Sending Widget', (tester) async {
|
testWidgets('Testing Sending Widget Without Timer', (tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
title: 'Send',
|
title: 'Send',
|
||||||
theme: kThemeDataDark,
|
theme: kThemeDataDark,
|
||||||
home: const Scaffold(
|
home: const Scaffold(
|
||||||
body: SendingWidget(),
|
body: SendingWidget(
|
||||||
|
startSendingTime: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -24,6 +26,26 @@ void main() {
|
|||||||
expect(find.byType(Lottie), findsOneWidget);
|
expect(find.byType(Lottie), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Testing Sending Widget With Timer', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
title: 'Send',
|
||||||
|
theme: kThemeDataDark,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SendingWidget(
|
||||||
|
startSendingTime: DateTime.now(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(find.text('Time elapsed: 0 ms'), findsOneWidget);
|
||||||
|
expect(find.byType(Lottie), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(find.text('Time elapsed: 1.00 s'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Testing Not Sent Widget', (tester) async {
|
testWidgets('Testing Not Sent Widget', (tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
|
Reference in New Issue
Block a user