diff --git a/docs/qaul/flutter/windows.md b/docs/qaul/flutter/windows.md index 1366edc82..e9f3f901c 100644 --- a/docs/qaul/flutter/windows.md +++ b/docs/qaul/flutter/windows.md @@ -3,11 +3,11 @@ ## Prerequisits ### Install Visual Studio -In order to develop for the windows desktop platform you need to have 'Microsoft Visual Studio 2019' or 'Microsoft Visual Studio 2022' installed. +In order to develop for the windows desktop platform you need to have 'Microsoft Visual Studio 2022' installed. -In Microsoft Visual Studio 2019/2022, you need to have 'Desktop development with C++' workload installed. +In Microsoft Visual Studio 2022, you need to have 'Desktop development with C++' workload installed. ### Install Git for Windows @@ -73,7 +73,7 @@ cargo build # copy libqaul.dll to flutter runner ## this step is required in order to run flutter app ## create the location folder if it does not yet exist -cp .\target\debug\libqaul.dll ..\qaul_ui\build\windows\runner\Debug\ +cp .\target\debug\libqaul.dll ..\qaul_ui\build\windows\x64\runner\Debug\ ``` ### Build and Run Windows Desktop App diff --git a/qaul_ui/android/app/build.gradle b/qaul_ui/android/app/build.gradle index 914204e25..e119e43d6 100644 --- a/qaul_ui/android/app/build.gradle +++ b/qaul_ui/android/app/build.gradle @@ -36,7 +36,7 @@ group = "net.qaul.app" version = flutterVersionName android { - compileSdkVersion 33 + compileSdkVersion 34 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/qaul_ui/android/app/src/main/AndroidManifest.xml b/qaul_ui/android/app/src/main/AndroidManifest.xml index 199e74672..9e5a55034 100644 --- a/qaul_ui/android/app/src/main/AndroidManifest.xml +++ b/qaul_ui/android/app/src/main/AndroidManifest.xml @@ -38,6 +38,11 @@ + + + + + diff --git a/qaul_ui/ios/Podfile.lock b/qaul_ui/ios/Podfile.lock index 81dbb16d6..ec226b35e 100644 --- a/qaul_ui/ios/Podfile.lock +++ b/qaul_ui/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - audioplayers_darwin (0.0.1): + - Flutter - better_open_file (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -53,6 +55,9 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - record_darwin (1.0.0): + - Flutter + - FlutterMacOS - SDWebImage (5.15.2): - SDWebImage/Core (= 5.15.2) - SDWebImage/Core (5.15.2) @@ -66,6 +71,7 @@ PODS: - Flutter DEPENDENCIES: + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - better_open_file (from `.symlinks/plugins/better_open_file/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -77,6 +83,7 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - record_darwin (from `.symlinks/plugins/record_darwin/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -89,6 +96,8 @@ SPEC REPOS: - SwiftyGif EXTERNAL SOURCES: + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/ios" better_open_file: :path: ".symlinks/plugins/better_open_file/ios" device_info_plus: @@ -111,6 +120,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + record_darwin: + :path: ".symlinks/plugins/record_darwin/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" uni_links: @@ -119,6 +130,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac @@ -132,6 +144,7 @@ SPEC CHECKSUMS: integration_test: 13825b8a9334a850581300559b8839134b124670 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517 SDWebImage: 8ab87d4b3e5cc4927bd47f78db6ceb0b94442577 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f @@ -140,4 +153,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/qaul_ui/ios/Runner/Info.plist b/qaul_ui/ios/Runner/Info.plist index 4cc38798d..426846bae 100644 --- a/qaul_ui/ios/Runner/Info.plist +++ b/qaul_ui/ios/Runner/Info.plist @@ -66,5 +66,7 @@ You can send photos as attachments to your peers. For that, please grant access to the application. NSCameraUsageDescription You can take pictures and send them as attachments to your peers. For that, please grant access to the application. + NSMicrophoneUsageDescription + You can send audio messages to your peers. For that, please grant access to the application. diff --git a/qaul_ui/lib/qaul_app.dart b/qaul_ui/lib/qaul_app.dart index f0d05029a..9b3bc8783 100644 --- a/qaul_ui/lib/qaul_app.dart +++ b/qaul_ui/lib/qaul_app.dart @@ -21,6 +21,10 @@ class QaulApp extends PlatformAwareBuilder { return AdaptiveTheme( light: ThemeData( brightness: Brightness.light, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.lightBlue, + brightness: Brightness.light, + ), primarySwatch: Colors.lightBlue, scaffoldBackgroundColor: Colors.white, navigationBarTheme: const NavigationBarThemeData( @@ -70,6 +74,10 @@ class QaulApp extends PlatformAwareBuilder { ), dark: ThemeData( brightness: Brightness.dark, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.lightBlue, + brightness: Brightness.dark, + ), primarySwatch: Colors.lightBlue, visualDensity: VisualDensity.adaptivePlatformDensity, iconTheme: const IconThemeData(color: Colors.white), diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_message_widget.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_message_widget.dart new file mode 100644 index 000000000..f1f56e6ec --- /dev/null +++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_message_widget.dart @@ -0,0 +1,182 @@ +part of 'chat.dart'; + +class AudioMessageWidget extends StatefulWidget { + const AudioMessageWidget({ + Key? key, + required this.message, + required this.messageWidth, + this.isDefaultUser = false, + }) : super(key: key); + + final types.AudioMessage message; + + final int messageWidth; + + final bool isDefaultUser; + + @override + State createState() => _AudioMessageWidgetState(); +} + +class _AudioMessageWidgetState extends State { + double get _controlSize => (widget.messageWidth.toDouble()) / 10; + + final _audioPlayer = AudioPlayer()..setReleaseMode(ReleaseMode.stop); + + Duration? _position; + + Duration? _duration; + + String? audioPath; + + late StreamSubscription _playerStateChangedSubscription; + + late StreamSubscription _durationChangedSubscription; + + late StreamSubscription _positionChangedSubscription; + + Color get primaryColor => Theme.of(context).colorScheme.primary; + + Color get containerColor => Theme.of(context).colorScheme.primaryContainer; + + Color get backgroundColor => Theme.of(context).colorScheme.background; + + @override + void initState() { + _playerStateChangedSubscription = + _audioPlayer.onPlayerComplete.listen((state) async { + await stop(); + }); + _positionChangedSubscription = _audioPlayer.onPositionChanged.listen( + (position) => setState(() { + _position = position; + }), + ); + _durationChangedSubscription = _audioPlayer.onDurationChanged.listen( + (duration) => setState(() { + _duration = duration; + }), + ); + + audioPath = widget.message.uri; + _getDuration(); + _audioPlayer.setSource(_source); + super.initState(); + } + + @override + void dispose() { + _playerStateChangedSubscription.cancel(); + _positionChangedSubscription.cancel(); + _durationChangedSubscription.cancel(); + _audioPlayer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ttheme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 8, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: _controlSize / 2), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + audioControls(), + Expanded(child: audioSlider()), + ], + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: Text( + '${_duration?.inSeconds ?? 0.0} Seconds', + style: ttheme.labelLarge?.copyWith( + color: backgroundColor, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ); + } + + Widget audioControls() { + return ClipOval( + child: Material( + color: containerColor, + child: InkWell( + child: SizedBox( + width: _controlSize, + height: _controlSize, + child: Icon( + _audioPlayer.state == PlayerState.playing + ? Icons.pause + : Icons.play_arrow, + color: primaryColor, + ), + ), + onTap: () { + if (_audioPlayer.state == PlayerState.playing) { + pause(); + } else { + play(); + } + }, + ), + ), + ); + } + + Widget audioSlider() { + bool canSetValue = false; + final duration = _duration; + final position = _position; + + if (duration != null && position != null) { + canSetValue = position.inMilliseconds >= 0; + canSetValue &= position.inMilliseconds < duration.inMilliseconds; + } + + return Slider( + activeColor: primaryColor, + inactiveColor: backgroundColor, + onChanged: (v) { + if (duration != null) { + final position = v * duration.inMilliseconds; + _audioPlayer.seek(Duration(milliseconds: position.round())); + } + }, + value: canSetValue && duration != null && position != null + ? position.inMilliseconds / duration.inMilliseconds + : 0.0, + ); + } + + Future play() => _audioPlayer.play(_source); + + Future pause() async { + await _audioPlayer.pause(); + setState(() {}); + } + + Future stop() async { + await _audioPlayer.stop(); + setState(() {}); + } + + Future _getDuration() async { + await _audioPlayer.setSourceUrl(audioPath!); + final duration = await _audioPlayer.getDuration(); + _duration = duration; + } + + Source get _source => DeviceFileSource(audioPath!); +} diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_recording.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_recording.dart new file mode 100644 index 000000000..13d84a27f --- /dev/null +++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_recording.dart @@ -0,0 +1,208 @@ +part of 'chat.dart'; + +class _RecordAudioDialog extends StatefulHookConsumerWidget { + const _RecordAudioDialog({ + Key? key, + required this.room, + required this.onSendPressed, + this.partialMessage, + }) : super(key: key); + final ChatRoom room; + final String? partialMessage; + final Function(File f, types.PartialText desc) onSendPressed; + + @override + ConsumerState<_RecordAudioDialog> createState() => _RecordAudioDialogState(); +} + +class _RecordAudioDialogState extends ConsumerState<_RecordAudioDialog> { + final audioPlayer = AudioPlayer(); + final audioRecorder = AudioRecorder(); + + bool isRecording = false; + File? file; + String? audioPath; + late ComplexTimer _timer; + int _duration = 0; + + void incrementDuration(ComplexTimer _) { + setState(() => _duration += 1); + _timer.restart(); + } + + void onStopPressed() async { + _timer.pause(); + await stopRecording(); + if (audioPath != null) { + setState(() => file = File(audioPath!)); + } + } + + void onCancelPressed() async { + if (isRecording) { + await stopRecording(); + } + file?.deleteSync(); + if (mounted) Navigator.pop(context); + } + + void onSendPressed() { + final worker = ref.read(qaulWorkerProvider); + worker.sendFile( + pathName: file!.path, + conversationId: widget.room.conversationId, + description: 'audio message'); + + Navigator.pop(context); + } + + @override + void initState() { + super.initState(); + _timer = ComplexTimer(const Duration(seconds: 1)); + _timer.onTimeout = incrementDuration; + _timer.pause(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + startRecording(); + _timer.restart(); + }); + } + + @override + void dispose() { + _timer.cancel(); + audioPlayer.dispose(); + audioRecorder.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: IconButtonFactory.close(onPressed: onCancelPressed), + ), + if (isRecording) + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.mic_rounded, + size: 28, + color: Colors.redAccent.shade100, + ), + Text( + _messageDuration, + ), + ], + ), + IconButton( + iconSize: 48, + onPressed: onStopPressed, + color: Colors.redAccent.shade100, + icon: const Icon(Icons.stop_circle_outlined), + ), + ], + ), + if (file != null) ...[ + Row( + children: [ + const SizedBox(width: 20), + const Icon(Icons.insert_drive_file_outlined, size: 40), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(filesize(file!.lengthSync())), + Text( + _messageDuration, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OutlinedButton( + onPressed: onCancelPressed, + child: const Text('Cancel'), + ), + FilledButton( + onPressed: onSendPressed, + child: const Text('Send'), + ), + ], + ), + ), + ], + ], + ), + ); + } + + String get _messageDuration => Duration(seconds: _duration) + .toString() + .substring(2) + .replaceAll('.000000', ''); + + Future startRecording() async { + try { + if (await audioRecorder.hasPermission()) { + setState(() { + isRecording = true; + }); + final path = await getNewAudioFilePath(); + await audioRecorder.start(const RecordConfig(), path: path); + } + } catch (e) { + return; + } + } + + Future stopRecording() async { + try { + final path = await audioRecorder.stop(); + setState(() { + isRecording = false; + audioPath = path!; + }); + } catch (e) { + return; + } + } + + Future getNewAudioFilePath() async { + final dir = (Platform.isAndroid) + ? (await getExternalStorageDirectory()) + : (await getApplicationSupportDirectory()); + + if (dir == null) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.genericErrorMessage), + )); + } + return ""; + } + + return join(dir.path, 'audio_${DateTime.now().millisecondsSinceEpoch}.m4a'); + } +} diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart index 79579756e..5131f8d0b 100644 --- a/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart +++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'dart:io'; +import 'package:audioplayers/audioplayers.dart'; import 'package:better_open_file/better_open_file.dart'; import 'package:bubble/bubble.dart'; import 'package:collection/collection.dart'; @@ -22,7 +24,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' hide context, Context; +import 'package:path_provider/path_provider.dart'; import 'package:qaul_rpc/qaul_rpc.dart'; +import 'package:record/record.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:utils/utils.dart'; @@ -33,6 +37,10 @@ import '../../../../../widgets/widgets.dart'; import '../../tab.dart'; import 'conditional/conditional.dart'; +part 'audio_message_widget.dart'; + +part 'audio_recording.dart'; + part 'custom_input.dart'; part 'file_message_widget.dart'; @@ -227,7 +235,8 @@ class _ChatScreenState extends ConsumerState { sendButtonVisibilityMode: SendButtonVisibilityMode.always, ), avatarBuilder: (id) { - var user = room.members.firstWhereOrNull((u) => id.id == u.idBase58); + var user = + room.members.firstWhereOrNull((u) => id.id == u.idBase58); if (user == null) return const SizedBox(); return QaulAvatar.small(user: user, badgeEnabled: false); }, @@ -311,6 +320,37 @@ class _ChatScreenState extends ConsumerState { ); } }, + onSendAudioPressed: !(Platform.isAndroid || + Platform.isIOS || + Platform.isMacOS || + Platform.isWindows || + Platform.isLinux) + ? null + : (room.messages?.isEmpty ?? true) + ? null + : ({types.PartialText? text}) async { + // ignore: use_build_context_synchronously + if (!context.mounted) return; + showModalBottomSheet( + context: context, + enableDrag: false, + isDismissible: false, + builder: (_) { + return _RecordAudioDialog( + room: room, + partialMessage: text?.text, + onSendPressed: (file, description) { + final worker = ref.read(qaulWorkerProvider); + worker.sendFile( + pathName: file.path, + conversationId: room.conversationId, + description: description.text, + ); + }, + ); + }, + ); + }, ), onMessageTap: (context, message) async { if (message is! types.FileMessage || _isReceivingFile(message)) { @@ -386,6 +426,13 @@ class _ChatScreenState extends ConsumerState { isDefaultUser: message.author.id == user.idBase58, ); }, + audioMessageBuilder: (message, {required int messageWidth}) { + return AudioMessageWidget( + message: message, + messageWidth: messageWidth, + isDefaultUser: message.author.id == user.idBase58, + ); + }, theme: DefaultChatTheme( userAvatarNameColors: [ colorGenerationStrategy(otherUser?.idBase58 ?? room.idBase58), @@ -528,6 +575,21 @@ extension _MessageExtension on Message { 'messageState': status.toJson(), }, ); + } else if (mimeStr != null && RegExp('audio/.*').hasMatch(mimeStr)) { + return types.AudioMessage( + id: messageIdBase58, + duration: const Duration(seconds: 100), + author: author.toInternalUser(), + createdAt: receivedAt.millisecondsSinceEpoch, + status: mappedState, + uri: filePath, + size: (content as FileShareContent).size, + name: (content as FileShareContent).fileName, + metadata: { + 'description': (content as FileShareContent).description, + 'messageState': status.toJson(), + }, + ); } return types.FileMessage( diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart index ddaa27c6b..471bb94d9 100644 --- a/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart +++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart @@ -21,6 +21,7 @@ class _CustomInput extends StatefulWidget { required this.hintText, this.onAttachmentPressed, this.onPickImagePressed, + this.onSendAudioPressed, this.initialText, this.disabledMessage, this.isDisabled = false, @@ -32,6 +33,8 @@ class _CustomInput extends StatefulWidget { final Function({types.PartialText? text})? onPickImagePressed; + final Function({types.PartialText? text})? onSendAudioPressed; + final SendButtonVisibilityMode sendButtonVisibilityMode; final String? initialText; @@ -179,6 +182,11 @@ class _CustomInputState extends State<_CustomInput> { onPressed: () => _sendFilePressed( widget.onPickImagePressed), ), + if (widget.onSendAudioPressed != null) + _AttachmentButton( + icon: Icons.mic_none, + onPressed: widget.onSendAudioPressed, + ), ], ), ), diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart index 402031563..0c2262415 100644 --- a/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart +++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart @@ -47,8 +47,7 @@ class _SendFileDialog extends HookConsumerWidget { const SizedBox(height: 8), _CustomInput( initialText: partialMessage, - hintText: AppLocalizations.of(context)! - .chatEmptyMessageHint, + hintText: AppLocalizations.of(context)!.chatEmptyMessageHint, onSendPressed: (desc) { final worker = ref.read(qaulWorkerProvider); worker.sendFile( diff --git a/qaul_ui/lib/utils.dart b/qaul_ui/lib/utils.dart index 843f0ccaf..f7c14e485 100644 --- a/qaul_ui/lib/utils.dart +++ b/qaul_ui/lib/utils.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fluent_ui/fluent_ui.dart'; class Responsiveness { @@ -25,3 +27,123 @@ class Responsiveness { static bool isDesktop(BuildContext context) => MediaQuery.of(context).size.width >= kDesktopBreakpoint; } + +class ComplexTimer implements Timer { + final Zone _zone; + final Stopwatch _activeTimer = Stopwatch(); + Timer? _timer; + final Duration _originalDuration; + void Function(ComplexTimer)? _onTimeout; + int _tick = 0; + + ComplexTimer(Duration duration) + : _originalDuration = duration, + _zone = Zone.current { + _startTimer(); + } + + set onTimeout(void Function(ComplexTimer)? callback) { + _onTimeout = callback == null ? null : _zone.bindUnaryCallback(callback); + } + + @override + int get tick => _tick; + + // Whether the timer is actively counting. + @override + bool get isActive => _timer?.isActive ?? false; + + // Whether the timer is started, but not currently actively counting. + bool get isPaused { + var timer = _timer; + return timer != null && !timer.isActive; + } + + // Whether the timer has expired. + bool get isExpired => _timer == null; + + /// Pauses an active timer. + /// + /// Nothing happens if the timer is already paused or expired. + void pause() { + _pauseTimer(); + } + + /// Resumes counting when paused. + /// + /// Nothing happens if the timer is active or expired. + void resume() { + var timer = _timer; + if (timer == null || timer.isActive) return; + _startTimer(); + } + + /// Resets the timer. + /// + /// Sets the timer to its original duration. + /// Does not change whether the timer is active, paused or expired. + void reset() { + var timer = _timer; + if (timer == null) return; // is expired. + _activeTimer + ..stop() + ..reset(); + if (timer.isActive) { + timer.cancel(); + _startTimer(); + } + } + + /// Restarts the timer. + /// + /// Starts counting for the original duration. + /// Works whether the timer is active, paused or expired. + void restart() { + _timer?.cancel(); + _activeTimer + ..stop() + ..reset(); + _startTimer(); + } + + /// Stops and expires the current timer. + /// + /// Nothing happens if the timer is already expired. + @override + void cancel() { + _timer?.cancel(); + _expireTimer(); + } + + void _startTimer() { + var elapsed = _activeTimer.elapsedMilliseconds; + var duration = _originalDuration; + if (elapsed > 0) { + duration = + Duration(milliseconds: _originalDuration.inMilliseconds - elapsed); + } + _timer = _zone.createTimer(duration, _onTick); + _activeTimer.start(); + } + + void _expireTimer() { + _timer = null; + _activeTimer + ..stop() + ..reset(); + } + + void _pauseTimer() { + _timer?.cancel(); + _activeTimer.stop(); + } + + void _onTick() { + _tick++; + _expireTimer(); + var callback = _onTimeout; + if (callback != null) { + _zone.runUnary(callback, this); + } + } +} diff --git a/qaul_ui/macos/Podfile b/qaul_ui/macos/Podfile index 049abe295..9ec46f8cd 100644 --- a/qaul_ui/macos/Podfile +++ b/qaul_ui/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/qaul_ui/macos/Podfile.lock b/qaul_ui/macos/Podfile.lock index 7045780c6..edb3b8c95 100644 --- a/qaul_ui/macos/Podfile.lock +++ b/qaul_ui/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - audioplayers_darwin (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -13,6 +15,9 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - record_darwin (1.0.0): + - Flutter + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -20,6 +25,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`) @@ -27,10 +33,13 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: + audioplayers_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: @@ -45,12 +54,15 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + record_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730 @@ -58,9 +70,10 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 -PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 +PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 COCOAPODS: 1.15.2 diff --git a/qaul_ui/macos/Runner.xcodeproj/project.pbxproj b/qaul_ui/macos/Runner.xcodeproj/project.pbxproj index bd1a7f49f..eb4e60bd0 100644 --- a/qaul_ui/macos/Runner.xcodeproj/project.pbxproj +++ b/qaul_ui/macos/Runner.xcodeproj/project.pbxproj @@ -209,7 +209,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -440,8 +440,8 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.0.0-beta.17; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MARKETING_VERSION = "2.0.0-beta.17"; PRODUCT_BUNDLE_IDENTIFIER = net.qaul.app; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -576,8 +576,8 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.0.0-beta.17; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MARKETING_VERSION = "2.0.0-beta.17"; PRODUCT_BUNDLE_IDENTIFIER = net.qaul.app; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -607,8 +607,8 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.0.0-beta.17; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MARKETING_VERSION = "2.0.0-beta.17"; PRODUCT_BUNDLE_IDENTIFIER = net.qaul.app; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; diff --git a/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9e1de3a16..016d8f6de 100644 --- a/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ com.apple.security.cs.allow-jit - com.apple.security.network.server + com.apple.security.device.audio-input com.apple.security.network.client - + + com.apple.security.network.server + diff --git a/qaul_ui/macos/Runner/Info.plist b/qaul_ui/macos/Runner/Info.plist index e5bc2a248..350a87483 100644 --- a/qaul_ui/macos/Runner/Info.plist +++ b/qaul_ui/macos/Runner/Info.plist @@ -30,5 +30,7 @@ MainMenu NSPrincipalClass NSApplication + NSMicrophoneUsageDescription + You can send audio messages to your peers. For that, please grant access to the application. diff --git a/qaul_ui/macos/Runner/Release.entitlements b/qaul_ui/macos/Runner/Release.entitlements index 7a2230dc3..9f3c0e8c3 100644 --- a/qaul_ui/macos/Runner/Release.entitlements +++ b/qaul_ui/macos/Runner/Release.entitlements @@ -4,6 +4,8 @@ com.apple.security.app-sandbox + com.apple.security.device.audio-input + com.apple.security.network.client com.apple.security.network.server diff --git a/qaul_ui/packages/qaul_rpc/lib/src/utils.dart b/qaul_ui/packages/qaul_rpc/lib/src/utils.dart index 77ac20162..0a4813e40 100644 --- a/qaul_ui/packages/qaul_rpc/lib/src/utils.dart +++ b/qaul_ui/packages/qaul_rpc/lib/src/utils.dart @@ -26,6 +26,9 @@ abstract class FilePathResolverMixin { required String extension, }) { var storagePath = read(libqaulLogsStoragePath)!.replaceAll('/logs', ''); + if (Platform.isWindows){ + storagePath = read(libqaulLogsStoragePath)!.replaceAll('\\logs', ''); + } var uuid = read(defaultUserProvider)!.idBase58; return '$storagePath/$uuid/files/$id.$extension'; diff --git a/qaul_ui/pubspec.lock b/qaul_ui/pubspec.lock index bda21da1a..a4912557d 100644 --- a/qaul_ui/pubspec.lock +++ b/qaul_ui/pubspec.lock @@ -49,6 +49,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.dev" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.dev" + source: hosted + version: "3.1.0" badges: dependency: "direct main" description: @@ -1106,6 +1162,62 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + record: + dependency: "direct main" + description: + name: record + sha256: "5c8e12c692a4800b33f5f8b6c821ea083b12bfdbd031b36ba9322c40a4eeecc9" + url: "https://pub.dev" + source: hosted + version: "5.0.4" + record_android: + dependency: transitive + description: + name: record_android + sha256: "805ecaa232a671aff2ee9ec4730ef6addb97c548d2db6b1fbd5197f1d4f47a5a" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + record_darwin: + dependency: transitive + description: + name: record_darwin + sha256: ee8cb1bb1712d7ce38140ecabe70e5c286c02f05296d66043bee865ace7eb1b9 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "7d0e70cd51635128fe9d37d89bafd6011d7cbba9af8dc323079ae60f23546aef" + url: "https://pub.dev" + source: hosted + version: "0.7.1" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: "3a4b56e94ecd2a0b2b43eb1fa6f94c5b8484334f5d38ef43959c4bf97fb374cf" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + record_web: + dependency: transitive + description: + name: record_web + sha256: "24847cdbcf999f7a5762170792f622ac844858766becd0f2370ec8ae22f7526e" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "39998b3ea7d8d28b04159d82220e6e5e32a7c357c6fb2794f5736beea272f6c3" + url: "https://pub.dev" + source: hosted + version: "1.0.2" responsive_framework: dependency: "direct main" description: @@ -1319,6 +1431,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: diff --git a/qaul_ui/pubspec.yaml b/qaul_ui/pubspec.yaml index 17531db55..c9ff3125f 100644 --- a/qaul_ui/pubspec.yaml +++ b/qaul_ui/pubspec.yaml @@ -62,6 +62,8 @@ dependencies: uni_links: ^0.5.1 url_launcher: ^6.1.14 utils: { "path": "packages/utils" } + record: ^5.0.4 + audioplayers: ^5.2.1 dev_dependencies: flutter_test: