diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 353b309..a6595fd 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -23,15 +23,13 @@ class MediaService { static Future handleAudioPlayback({ required dynamic audioSource, + required int id, + bool? isNetworkAudio, bool isVoiceMessage = true, }) async { - bool isNetworkAudio = audioSource.runtimeType == String; String tag; - if (isNetworkAudio) { - tag = audioSource; - } else { - tag = audioSource.path; - } + tag = '${isVoiceMessage ? 'message' : 'podcast'}-$id'; + isNetworkAudio ??= audioSource.runtimeType == String; if (audioPlayerTag == tag) { if (audioPlayer.playing) { await audioPlayer.pause(); @@ -51,10 +49,9 @@ class MediaService { ); } else { if (kIsWeb) { - await audioPlayer - .setUrl(audioSource!.uri.path.replaceAll('%3A', ':')); + await audioPlayer.setUrl(audioSource!.replaceAll('%3A', ':')); } else { - await audioPlayer.setFilePath(audioSource.path); + await audioPlayer.setFilePath(audioSource); } } audioPlayer.play(); diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart index 7501746..eeabccc 100644 --- a/lib/services/network/request.dart +++ b/lib/services/network/request.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'dart:developer'; -import 'dart:io'; +import 'package:didvan/services/storage/storage.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart' as parser; import 'package:permission_handler/permission_handler.dart'; @@ -164,11 +164,14 @@ class RequestService { } } - Future download() async { - Permission.manageExternalStorage.request(); + Future download(String fileName, String subDirectory) async { + Permission.storage.request(); final response = await http.get(Uri.parse(url)); - final file = await File('/storage/emulated/0/Download/file.mp3').create(); - await file.writeAsBytes(response.bodyBytes); + StorageService.createFile( + bytes: response.bodyBytes, + subDirectory: subDirectory, + name: fileName, + ); } void _handleResponse(http.Response? response) { diff --git a/lib/services/storage/storage.dart b/lib/services/storage/storage.dart index 012936c..6547633 100644 --- a/lib/services/storage/storage.dart +++ b/lib/services/storage/storage.dart @@ -1,3 +1,6 @@ +import 'dart:typed_data'; +import 'dart:io' as io; + import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:universal_html/html.dart'; @@ -6,6 +9,21 @@ class StorageService { static late String appTempsDir; static const FlutterSecureStorage _storage = FlutterSecureStorage(); + static Future createFile({ + required Uint8List bytes, + required String subDirectory, + required String name, + }) async { + final dir = io.Directory(appDocsDir + '/$subDirectory'); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + final file = await io.File( + appDocsDir + '/$subDirectory/podcast-$name.mp3', + ).create(recursive: true); + await file.writeAsBytes(bytes); + } + static Future setValue({ required String key, required dynamic value, diff --git a/lib/views/home/comments/comments.dart b/lib/views/home/comments/comments.dart index baf7d3e..d4c36aa 100644 --- a/lib/views/home/comments/comments.dart +++ b/lib/views/home/comments/comments.dart @@ -59,6 +59,7 @@ class _CommentsState extends State { child: Stack( children: [ DidvanScaffold( + physics: const BouncingScrollPhysics(), backgroundColor: Theme.of(context).colorScheme.surface, appBarData: _isPage ? AppBarData( @@ -77,6 +78,7 @@ class _CommentsState extends State { itemPadding: const EdgeInsets.symmetric(vertical: 16), childCount: state.comments.length, placeholder: const _CommentPlaceholder(), + centerEmptyState: _isPage, enableEmptyState: state.comments.isEmpty, emptyState: EmptyState( asset: Assets.emptyChat, diff --git a/lib/views/home/direct/direct_state.dart b/lib/views/home/direct/direct_state.dart index a4a14aa..346eec3 100644 --- a/lib/views/home/direct/direct_state.dart +++ b/lib/views/home/direct/direct_state.dart @@ -4,6 +4,7 @@ import 'package:didvan/models/enums.dart'; import 'package:didvan/models/message_data/message_data.dart'; import 'package:didvan/models/message_data/radar_attachment.dart'; import 'package:didvan/providers/core_provider.dart'; +import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:flutter/foundation.dart'; @@ -46,6 +47,7 @@ class DirectState extends CoreProvier { } Future startRecording() async { + text = null; await _recorder.hasPermission(); if (!kIsWeb) { Vibrate.feedback(FeedbackType.medium); @@ -88,6 +90,7 @@ class DirectState extends CoreProvier { Future sendMessage() async { if ((text == null || text!.isEmpty) && recordedFile == null) return; + MediaService.audioPlayer.stop(); messages.insert( 0, MessageData( diff --git a/lib/views/home/direct/widgets/audio_widget.dart b/lib/views/home/direct/widgets/audio_widget.dart index a33827c..5d99777 100644 --- a/lib/views/home/direct/widgets/audio_widget.dart +++ b/lib/views/home/direct/widgets/audio_widget.dart @@ -1,15 +1,22 @@ import 'dart:io'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/views/home/widgets/audio/audio_slider.dart'; -import 'package:didvan/views/home/widgets/player_controller_button.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:flutter/material.dart'; class AudioWidget extends StatelessWidget { final String? audioUrl; final File? audioFile; - const AudioWidget({Key? key, this.audioUrl, this.audioFile}) - : super(key: key); + final int id; + const AudioWidget({ + Key? key, + this.audioUrl, + this.audioFile, + required this.id, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -20,12 +27,13 @@ class AudioWidget extends StatelessWidget { children: [ Expanded( child: AudioSlider( - tag: audioUrl ?? audioFile!.path, + tag: 'message-$id', ), ), - AudioControllerButton( + _AudioControllerButton( audioFile: audioFile, audioUrl: audioUrl, + id: id, ), ], ); @@ -33,3 +41,33 @@ class AudioWidget extends StatelessWidget { ); } } + +class _AudioControllerButton extends StatelessWidget { + final String? audioUrl; + final File? audioFile; + final int id; + + const _AudioControllerButton( + {Key? key, this.audioUrl, this.audioFile, required this.id}) + : super(key: key); + + bool get _nowPlaying => MediaService.audioPlayerTag == 'message-$id'; + + @override + Widget build(BuildContext context) { + return DidvanIconButton( + icon: MediaService.audioPlayer.playing == true && _nowPlaying + ? DidvanIcons.pause_circle_solid + : DidvanIcons.play_circle_solid, + color: Theme.of(context).colorScheme.focusedBorder, + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: audioFile?.path ?? audioUrl, + id: id, + isNetworkAudio: audioFile == null, + isVoiceMessage: true, + ); + }, + ); + } +} diff --git a/lib/views/home/direct/widgets/message.dart b/lib/views/home/direct/widgets/message.dart index 49ab63d..ff90eb1 100644 --- a/lib/views/home/direct/widgets/message.dart +++ b/lib/views/home/direct/widgets/message.dart @@ -65,6 +65,7 @@ class Message extends StatelessWidget { AudioWidget( audioFile: message.audioFile, audioUrl: message.audio, + id: message.id, ), if (message.radar != null) const DidvanDivider(), if (message.radar != null) const SizedBox(height: 4), diff --git a/lib/views/home/direct/widgets/message_box.dart b/lib/views/home/direct/widgets/message_box.dart index c09d592..554e336 100644 --- a/lib/views/home/direct/widgets/message_box.dart +++ b/lib/views/home/direct/widgets/message_box.dart @@ -211,7 +211,10 @@ class _RecordChecking extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: AudioWidget(audioFile: state.recordedFile!), + child: AudioWidget( + audioFile: state.recordedFile!, + id: 0, + ), ), ), DidvanIconButton( diff --git a/lib/views/home/studio/studio_details/studio_details.mobile.dart b/lib/views/home/studio/studio_details/studio_details.mobile.dart index 5c726fb..378644e 100644 --- a/lib/views/home/studio/studio_details/studio_details.mobile.dart +++ b/lib/views/home/studio/studio_details/studio_details.mobile.dart @@ -9,7 +9,6 @@ import 'package:didvan/views/home/studio/studio_details/widgets/studio_details_w import 'package:didvan/views/home/widgets/bookmark_button.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; diff --git a/lib/views/home/studio/studio_details/studio_details.web.dart b/lib/views/home/studio/studio_details/studio_details.web.dart index 65b1da5..b23740d 100644 --- a/lib/views/home/studio/studio_details/studio_details.web.dart +++ b/lib/views/home/studio/studio_details/studio_details.web.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:ui' as ui; import 'package:didvan/config/design_config.dart'; diff --git a/lib/views/home/studio/studio_details/studio_details_state.dart b/lib/views/home/studio/studio_details/studio_details_state.dart index 2f8d867..b127d42 100644 --- a/lib/views/home/studio/studio_details/studio_details_state.dart +++ b/lib/views/home/studio/studio_details/studio_details_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; @@ -8,6 +9,7 @@ import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/services/storage/storage.dart'; class StudioDetailsState extends CoreProvier { late StudioDetailsData studio; @@ -17,6 +19,7 @@ class StudioDetailsState extends CoreProvier { late StudioRequestArgs args; final List relatedQueue = []; bool _positionListenerActivated = false; + final List downloadedFileIds = []; int _selectedDetailsIndex = 0; Timer? timer; @@ -42,6 +45,7 @@ class StudioDetailsState extends CoreProvier { if (MediaService.currentPodcast?.id == id && this.args.type == 'podcast') { return; } + _getDownloadsList(); _selectedDetailsIndex = 0; if (isForward != null) { if (isForward) { @@ -87,9 +91,14 @@ class StudioDetailsState extends CoreProvier { if (args.type == 'podcast') { MediaService.currentPodcast = studio; MediaService.podcastPlaylistArgs = args; + final downloaded = downloadedFileIds.contains(studio.id); await MediaService.handleAudioPlayback( - audioSource: studio.media, + audioSource: downloaded + ? StorageService.appDocsDir + '/podcasts/podcast-${studio.id}.mp3' + : studio.media, + id: studio.id, isVoiceMessage: false, + isNetworkAudio: !downloaded, ); if (nextStudio != null && !_positionListenerActivated) { _positionListenerActivated = true; @@ -104,6 +113,25 @@ class StudioDetailsState extends CoreProvier { } } + Future _getDownloadsList() async { + downloadedFileIds.clear(); + final dir = Directory( + StorageService.appDocsDir + ('/${args.type}s'), + ); + if (!await dir.exists()) { + await dir.create(); + } + dir.list(recursive: false).listen( + (event) { + downloadedFileIds.add( + int.parse( + event.path.split('/').last.split('-').last.split('.').first, + ), + ); + }, + ); + } + Future getRelatedContents() async { if (studio.relatedContents.isNotEmpty) return; relatedQueue.add(studio.id); diff --git a/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart b/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart index 6edbac4..832c9d3 100644 --- a/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart +++ b/lib/views/home/studio/studio_details/widgets/studio_details_widget.dart @@ -41,7 +41,7 @@ class StudioDetailsWidget extends StatelessWidget { isVideo: _isVideo, onCommentsTabSelected: onCommentsTabSelected ?? () {}, ), - if (state.selectedDetailsIndex != 1) const SizedBox(height: 16), + const SizedBox(height: 16), StateHandler( onRetry: () {}, state: state, @@ -93,7 +93,7 @@ class StudioDetailsWidget extends StatelessWidget { child: SizedBox( height: ds.height - ds.width * 9 / 16 - - 128 - + 144 - MediaQuery.of(context).padding.top, child: Comments( pageData: { diff --git a/lib/views/home/studio/studio_state.dart b/lib/views/home/studio/studio_state.dart index e6d9c66..e168e2d 100644 --- a/lib/views/home/studio/studio_state.dart +++ b/lib/views/home/studio/studio_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; @@ -8,10 +9,12 @@ import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/providers/user_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; +import 'package:didvan/services/storage/storage.dart'; class StudioState extends CoreProvier { final List studios = []; final List sliders = []; + final List downloadedFileIds = []; String search = ''; String lastSearch = ''; @@ -29,10 +32,31 @@ class StudioState extends CoreProvier { set videosSelected(bool value) { if (_videosSelected == value) return; _videosSelected = value; + selectedSortTypeIndex = 0; _getSliders(); + getDownloadsList(); getStudios(page: page); } + Future getDownloadsList() async { + downloadedFileIds.clear(); + final dir = Directory( + StorageService.appDocsDir + (videosSelected ? '/videos' : '/podcasts'), + ); + if (!await dir.exists()) { + await dir.create(); + } + dir.list(recursive: false).listen( + (event) { + downloadedFileIds.add( + int.parse( + event.path.split('/').last.split('-').last.split('.').first, + ), + ); + }, + ); + } + String get order { if (selectedSortTypeIndex == 0 || selectedSortTypeIndex == 1) return 'date'; if (selectedSortTypeIndex == 2) return 'view'; @@ -56,6 +80,7 @@ class StudioState extends CoreProvier { lastSearch = ''; _videosSelected = true; selectedSortTypeIndex = 0; + getDownloadsList(); Future.delayed(Duration.zero, () { _getSliders(); getStudios(page: 1); @@ -94,7 +119,6 @@ class StudioState extends CoreProvier { ), ), ); - await service.httpGet(); if (service.isSuccess) { if (page == 1) { @@ -122,8 +146,9 @@ class StudioState extends CoreProvier { notifyListeners(); } - void download(String url) { + Future download(String url, String fileName) async { final service = RequestService(url); - service.download(); + await service.download(fileName, videosSelected ? 'videos' : 'podcasts'); + notifyListeners(); } } diff --git a/lib/views/home/widgets/audio/audio_player_widget.dart b/lib/views/home/widgets/audio/audio_player_widget.dart index edc9675..10bbddc 100644 --- a/lib/views/home/widgets/audio/audio_player_widget.dart +++ b/lib/views/home/widgets/audio/audio_player_widget.dart @@ -57,7 +57,7 @@ class AudioPlayerWidget extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: AudioSlider( - tag: podcast.media, + tag: 'podcast-${podcast.id}', showTimer: true, duration: podcast.duration, ), @@ -117,6 +117,7 @@ class AudioPlayerWidget extends StatelessWidget { builder: (context, snapshot) { return _PlayPouseAnimatedIcon( audioSource: podcast.media, + id: podcast.id, ); }, ), @@ -239,7 +240,9 @@ class AudioPlayerWidget extends StatelessWidget { class _PlayPouseAnimatedIcon extends StatefulWidget { final String audioSource; - const _PlayPouseAnimatedIcon({Key? key, required this.audioSource}) + final int id; + const _PlayPouseAnimatedIcon( + {Key? key, required this.audioSource, required this.id}) : super(key: key); @override @@ -281,6 +284,11 @@ class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> MediaService.handleAudioPlayback( audioSource: widget.audioSource, isVoiceMessage: false, + id: widget.id, + isNetworkAudio: !context + .read() + .downloadedFileIds + .contains(widget.id), ); _handleAnimation(); }, diff --git a/lib/views/home/widgets/audio/audio_slider.dart b/lib/views/home/widgets/audio/audio_slider.dart index 093bcd2..a65e126 100644 --- a/lib/views/home/widgets/audio/audio_slider.dart +++ b/lib/views/home/widgets/audio/audio_slider.dart @@ -22,7 +22,7 @@ class AudioSlider extends StatelessWidget { @override Widget build(BuildContext context) { return IgnorePointer( - ignoring: MediaService.audioPlayerTag != tag, + ignoring: !_isPlaying, child: Directionality( textDirection: TextDirection.ltr, child: StreamBuilder( diff --git a/lib/views/home/widgets/overview/podcast.dart b/lib/views/home/widgets/overview/podcast.dart index cea07fd..b42eccf 100644 --- a/lib/views/home/widgets/overview/podcast.dart +++ b/lib/views/home/widgets/overview/podcast.dart @@ -1,15 +1,19 @@ import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/home/studio/studio_details/studio_details_state.dart'; +import 'package:didvan/views/home/studio/studio_state.dart'; import 'package:didvan/views/home/widgets/bookmark_button.dart'; import 'package:didvan/views/home/widgets/duration_widget.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -28,6 +32,7 @@ class PodcastOverview extends StatelessWidget { @override Widget build(BuildContext context) { + final state = context.read(); return DidvanCard( onTap: () { context @@ -75,13 +80,22 @@ class PodcastOverview extends StatelessWidget { children: [ DurationWidget(duration: podcast.duration!), const Spacer(), - // DidvanIconButton( - // gestureSize: 28, - // icon: DidvanIcons.download_regular, - // onPressed: () => - // context.read().download(podcast.media!), - // ), - // const SizedBox(width: 16), + if (!kIsWeb) ...[ + DidvanIconButton( + gestureSize: 28, + color: _isDownloaded(state) + ? Theme.of(context).colorScheme.primary + : null, + icon: _isDownloaded(state) + ? DidvanIcons.download_solid + : DidvanIcons.download_regular, + onPressed: _isDownloaded(state) + ? () {} + : () => + state.download(podcast.media!, podcast.id.toString()), + ), + const SizedBox(width: 16), + ], BookmarkButton( askForConfirmation: hasUnmarkConfirmation, gestureSize: 32, @@ -95,6 +109,10 @@ class PodcastOverview extends StatelessWidget { ); } + bool _isDownloaded(state) { + return state.downloadedFileIds.contains(podcast.id); + } + static Widget get placeholder => DidvanCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/views/home/widgets/player_controller_button.dart b/lib/views/home/widgets/player_controller_button.dart deleted file mode 100644 index 7e2cc14..0000000 --- a/lib/views/home/widgets/player_controller_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:io'; - -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; -import 'package:didvan/services/media/media.dart'; -import 'package:didvan/views/widgets/didvan/icon_button.dart'; -import 'package:flutter/material.dart'; - -class AudioControllerButton extends StatelessWidget { - final String? audioUrl; - final File? audioFile; - - const AudioControllerButton({Key? key, this.audioUrl, this.audioFile}) - : super(key: key); - - bool get _nowPlaying => - MediaService.audioPlayerTag == audioUrl || - audioFile != null && MediaService.audioPlayerTag == audioFile!.path; - - @override - Widget build(BuildContext context) { - return DidvanIconButton( - icon: MediaService.audioPlayer.playing == true && _nowPlaying - ? DidvanIcons.pause_circle_solid - : DidvanIcons.play_circle_solid, - color: Theme.of(context).colorScheme.focusedBorder, - onPressed: () { - MediaService.handleAudioPlayback( - audioSource: audioFile ?? audioUrl, - ); - }, - ); - } -} diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart index 78af00d..84eb6eb 100644 --- a/lib/views/widgets/didvan/bnb.dart +++ b/lib/views/widgets/didvan/bnb.dart @@ -24,10 +24,11 @@ class DidvanBNB extends StatelessWidget { : super(key: key); bool get _enablePlayerController => - MediaService.currentPodcast != null || MediaService.audioPlayer.playing; + MediaService.currentPodcast != null && MediaService.audioPlayer.playing; @override Widget build(BuildContext context) { + final state = context.read(); return StreamBuilder( stream: MediaService.audioPlayer.playingStream, builder: (context, snapshot) { @@ -108,7 +109,8 @@ class DidvanBNB extends StatelessWidget { : DidvanIcons.play_solid, onPressed: () { MediaService.handleAudioPlayback( - audioSource: MediaService.audioPlayerTag, + audioSource: state.studio.media, + id: state.studio.id, ); }, ), diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index 8e31440..38f9dde 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -9,6 +9,7 @@ class DidvanScaffold extends StatefulWidget { final EdgeInsets padding; final Color? backgroundColor; final bool reverse; + final ScrollPhysics? physics; final ScrollController? scrollController; final bool showSliversFirst; @@ -17,6 +18,7 @@ class DidvanScaffold extends StatefulWidget { this.slivers, required this.appBarData, this.children, + this.physics, this.padding = const EdgeInsets.symmetric(horizontal: 16), this.backgroundColor, this.reverse = false, @@ -49,6 +51,7 @@ class _DidvanScaffoldState extends State { child: Stack( children: [ CustomScrollView( + physics: widget.physics, controller: _scrollController, reverse: widget.reverse, slivers: [