diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart index ada14c3..882f244 100644 --- a/lib/class/audiostate.dart +++ b/lib/class/audiostate.dart @@ -1,21 +1,432 @@ +import 'dart:typed_data'; +import 'dart:async'; +import 'dart:io'; + import 'package:flutter/foundation.dart'; import 'package:tsacdop/class/episodebrief.dart'; +import 'package:audiofileplayer/audiofileplayer.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:logging/logging.dart'; +import 'package:audiofileplayer/audio_system.dart'; +import 'package:tsacdop/local_storage/key_value_storage.dart'; +import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; -enum AudioState {load, play, pause, complete, error} +enum AudioState { load, play, pause, complete, error, stop } -class AudioPlay with ChangeNotifier { - +class PlayHistory { + String title; + String url; + double seconds; + double seekValue; + PlayHistory(this.title, this.url, this.seconds, this.seekValue); +} + +class Playlist { + String name; + DBHelper dbHelper = DBHelper(); + List urls; + List _playlist; + List get playlist => _playlist; + KeyValueStorage storage = KeyValueStorage('playlist'); + Playlist(this.name, {List urls}) : urls = urls ?? []; + + getPlaylist() async{ + List _urls = await storage.getPlayList(); + if (_urls.length == 0) { + _playlist = []; + } else { + _playlist = []; + await Future.forEach(_urls, (url) async{ + EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); + print(episode.title); + _playlist.add(episode); + }); + } + print(_playlist.length); + } + + savePlaylist() async { + urls = []; + urls.addAll(_playlist.map((e) => e.enclosureUrl)); + print(urls); + await storage.savePlaylist(urls); + } + + addToPlayList(EpisodeBrief episodeBrief) async { + _playlist.add(episodeBrief); + await savePlaylist(); + } + + delFromPlaylist(EpisodeBrief episodeBrief) { + _playlist.remove(episodeBrief); + savePlaylist(); + } +} + +class AudioPlayer extends ChangeNotifier { + static const String replay10ButtonId = 'replay10ButtonId'; + static const String newReleasesButtonId = 'newReleasesButtonId'; + static const String likeButtonId = 'likeButtonId'; + static const String pausenowButtonId = 'pausenowButtonId'; + static const String forwardButtonId = 'forwardButtonId'; + + DBHelper dbHelper = DBHelper(); EpisodeBrief _episode; - EpisodeBrief get episode => _episode; - set episodeLoad(EpisodeBrief episdoe){ - _episode = episdoe; - notifyListeners(); - } - - AudioState _audioState; + Playlist _queue = Playlist('now'); + bool _playerRunning = false; + Audio _backgroundAudio; + bool _backgroundAudioPlaying = false; + double _backgroundAudioDurationSeconds = 0; + double _backgroundAudioPositionSeconds = 0; + bool _remoteAudioLoading = false; + String _remoteErrorMessage; + double _seekSliderValue = 0.0; + final Logger _logger = Logger('audiofileplayer'); + + bool get backgroundAudioPlaying => _backgroundAudioPlaying; + bool get remoteAudioLoading => _remoteAudioLoading; + double get backgroundAudioDuration => _backgroundAudioDurationSeconds; + double get backgroundAudioPosition => _backgroundAudioPositionSeconds; + double get seekSliderValue => _seekSliderValue; + String get remoteErrorMessage => _remoteErrorMessage; + bool get playerRunning => _playerRunning; + + Playlist get queue => _queue; + + AudioState _audioState = AudioState.stop; AudioState get audioState => _audioState; - set audioState(AudioState state){ - _audioState = state; + + EpisodeBrief get episode => _episode; + + episodeLoad(EpisodeBrief episode) async { + AudioSystem.instance.addMediaEventListener(_mediaEventListener); + _episode = episode; + await _queue.getPlaylist(); + if (_queue.playlist.contains(_episode)) { + _queue.playlist.remove(_episode); + _queue.playlist.insert(0, _episode); + } else { + _queue.playlist.insert(0, _episode); + } + await _queue.savePlaylist(); + await _play(_episode); + _playerRunning = true; + _backgroundAudioPlaying = true; notifyListeners(); } -} \ No newline at end of file + + playNext() async { + PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, + backgroundAudioDuration, seekSliderValue); + await dbHelper.saveHistory(history); + await _queue.delFromPlaylist(_episode); + if (_queue.playlist.length > 0) { + _episode = _queue.playlist.first; + _play(_episode); + _backgroundAudioPlaying = true; + notifyListeners(); + } else { + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + notifyListeners(); + } + } + + addToPlaylist(EpisodeBrief episode) async { + _queue.addToPlayList(episode); + await _queue.getPlaylist(); + notifyListeners(); + } + + pauseAduio() async { + _pauseBackgroundAudio(); + _audioState = AudioState.pause; + notifyListeners(); + PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, + backgroundAudioDuration, seekSliderValue); + await dbHelper.saveHistory(history); + await _queue.delFromPlaylist(_episode); + } + + resumeAudio() { + _resumeBackgroundAudio(); + _audioState = AudioState.play; + notifyListeners(); + } + + forwardAudio(double s) { + _forwardBackgroundAudio(s); + notifyListeners(); + } + + sliderSeek(double val) { + _seekSliderValue = val; + notifyListeners(); + final double positionSeconds = val * _backgroundAudioDurationSeconds; + _backgroundAudio.seek(positionSeconds); + AudioSystem.instance.setPlaybackState(true, positionSeconds); + } + + disopse() { + AudioSystem.instance.removeMediaEventListener(_mediaEventListener); + _backgroundAudio?.dispose(); + } + + _play(EpisodeBrief episodeBrief) async { + String url = _queue.playlist.first.enclosureUrl; + _getFile(url).then((result) { + result == 'NotDownload' + ? _initbackgroundAudioPlayer(url) + : _initbackgroundAudioPlayerLocal(result); + }); + } + + Future _getFile(String url) async { + final task = await FlutterDownloader.loadTasksWithRawQuery( + query: "SELECT * FROM task WHERE url = '$url' AND status = 3"); + if (task.length != 0) { + String _filePath = task.first.savedDir + '/' + task.first.filename; + return _filePath; + } + return 'NotDownload'; + } + + ByteData _getAudio(String path) { + File audioFile = File(path); + Uint8List audio = audioFile.readAsBytesSync(); + return ByteData.view(audio.buffer); + } + + void _initbackgroundAudioPlayerLocal(String path) { + _remoteErrorMessage = null; + _remoteAudioLoading = true; + ByteData audio = _getAudio(path); + if (_backgroundAudioPlaying == true) { + _stopBackgroundAudio(); + } + _backgroundAudioPositionSeconds = 0; + _setNotification(false); + _backgroundAudio = + Audio.loadFromByteData(audio, onDuration: (double durationSeconds) { + _backgroundAudioDurationSeconds = durationSeconds; + _remoteAudioLoading = false; + _backgroundAudioPlaying = true; + _setNotification(true); + notifyListeners(); + }, onPosition: (double positionSeconds) { + if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) { + _seekSliderValue = + _backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds; + _backgroundAudioPositionSeconds = positionSeconds; + notifyListeners(); + } else { + _seekSliderValue = 1; + _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; + notifyListeners(); + } + }, onError: (String message) { + _remoteErrorMessage = message; + _backgroundAudio.dispose(); + _backgroundAudio = null; + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + }, onComplete: () { + playNext(); + }, looping: false, playInBackground: true) + ..play(); + } + + void _initbackgroundAudioPlayer(String url) { + _remoteErrorMessage = null; + _remoteAudioLoading = true; + notifyListeners(); + if (_backgroundAudioPlaying == true) { + _stopBackgroundAudio(); + } + _backgroundAudioPositionSeconds = 0; + _setNotification(false); + _backgroundAudio = + Audio.loadFromRemoteUrl(url, onDuration: (double durationSeconds) { + _backgroundAudioDurationSeconds = durationSeconds; + _remoteAudioLoading = false; + _backgroundAudioPlaying = true; + _setNotification(true); + notifyListeners(); + }, onPosition: (double positionSeconds) { + if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) { + _seekSliderValue = + _backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds; + _backgroundAudioPositionSeconds = positionSeconds; + notifyListeners(); + } else { + _seekSliderValue = 1; + _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; + notifyListeners(); + } + }, onError: (String message) { + _remoteErrorMessage = message; + _backgroundAudio.dispose(); + _backgroundAudio = null; + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + }, onComplete: () { + playNext(); + }, looping: false, playInBackground: true) + ..resume(); + } + + void _mediaEventListener(MediaEvent mediaEvent) { + _logger.info('App received media event of type: ${mediaEvent.type}'); + final MediaActionType type = mediaEvent.type; + if (type == MediaActionType.play) { + _resumeBackgroundAudio(); + } else if (type == MediaActionType.pause) { + _pauseBackgroundAudio(); + } else if (type == MediaActionType.playPause) { + _backgroundAudioPlaying + ? _pauseBackgroundAudio() + : _resumeBackgroundAudio(); + } else if (type == MediaActionType.stop) { + _stopBackgroundAudio(); + } else if (type == MediaActionType.seekTo) { + _backgroundAudio.seek(mediaEvent.seekToPositionSeconds); + AudioSystem.instance + .setPlaybackState(true, mediaEvent.seekToPositionSeconds); + } else if (type == MediaActionType.skipForward) { + final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; + _forwardBackgroundAudio(skipIntervalSeconds); + _logger.info( + 'Skip-forward event had skipIntervalSeconds $skipIntervalSeconds.'); + } else if (type == MediaActionType.skipBackward) { + final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; + _forwardBackgroundAudio(skipIntervalSeconds); + _logger.info( + 'Skip-backward event had skipIntervalSeconds $skipIntervalSeconds.'); + } else if (type == MediaActionType.custom) { + if (mediaEvent.customEventId == replay10ButtonId) { + _forwardBackgroundAudio(-10.0); + } else if (mediaEvent.customEventId == likeButtonId) { + _resumeBackgroundAudio(); + } else if (mediaEvent.customEventId == forwardButtonId) { + _forwardBackgroundAudio(30.0); + } else if (mediaEvent.customEventId == pausenowButtonId) { + _pauseBackgroundAudio(); + } + } + } + + Future _setNotification(bool b) async { + final Uint8List imageBytes = + File('${_episode.imagePath}').readAsBytesSync(); + AudioSystem.instance.setMetadata(AudioMetadata( + title: episode.title, + artist: episode.feedTitle, + album: episode.feedTitle, + genre: "Podcast", + durationSeconds: _backgroundAudioDurationSeconds, + artBytes: imageBytes)); + AudioSystem.instance.setPlaybackState(b, _backgroundAudioPositionSeconds); + AudioSystem.instance.setAndroidNotificationButtons([ + AndroidMediaButtonType.pause, + _forwardButton, + AndroidMediaButtonType.stop, + ], androidCompactIndices: [ + 0, + 1 + ]); + + AudioSystem.instance.setSupportedMediaActions({ + MediaActionType.playPause, + MediaActionType.pause, + MediaActionType.next, + MediaActionType.previous, + MediaActionType.skipForward, + MediaActionType.skipBackward, + MediaActionType.seekTo, + MediaActionType.custom, + }, skipIntervalSeconds: 30); + } + + Future _resumeBackgroundAudio() async { + _backgroundAudio.resume(); + + _backgroundAudioPlaying = true; + + final Uint8List imageBytes = + File('${_episode.imagePath}').readAsBytesSync(); + AudioSystem.instance.setMetadata(AudioMetadata( + title: _episode.title, + artist: _episode.feedTitle, + album: _episode.feedTitle, + genre: "Podcast", + durationSeconds: _backgroundAudioDurationSeconds, + artBytes: imageBytes)); + + AudioSystem.instance + .setPlaybackState(true, _backgroundAudioPositionSeconds); + + AudioSystem.instance.setAndroidNotificationButtons([ + AndroidMediaButtonType.pause, + _forwardButton, + AndroidMediaButtonType.stop, + ], androidCompactIndices: [ + 0, + 1 + ]); + + AudioSystem.instance.setSupportedMediaActions({ + MediaActionType.playPause, + MediaActionType.pause, + MediaActionType.next, + MediaActionType.previous, + MediaActionType.skipForward, + MediaActionType.skipBackward, + MediaActionType.seekTo, + MediaActionType.custom, + }, skipIntervalSeconds: 30); + } + + void _pauseBackgroundAudio() { + _backgroundAudio.pause(); + _backgroundAudioPlaying = false; + AudioSystem.instance + .setPlaybackState(false, _backgroundAudioPositionSeconds); + AudioSystem.instance.setAndroidNotificationButtons([ + AndroidMediaButtonType.play, + _forwardButton, + AndroidMediaButtonType.stop, + ], androidCompactIndices: [ + 0, + 1, + ]); + + AudioSystem.instance.setSupportedMediaActions({ + MediaActionType.playPause, + MediaActionType.play, + MediaActionType.next, + MediaActionType.previous, + }); + } + + void _stopBackgroundAudio() { + _backgroundAudio..pause(); + _backgroundAudio..dispose(); + _backgroundAudioPlaying = false; + AudioSystem.instance.stopBackgroundDisplay(); + } + + void _forwardBackgroundAudio(double seconds) { + final double forwardposition = _backgroundAudioPositionSeconds + seconds; + _backgroundAudio.seek(forwardposition); + //AudioSystem.instance.setPlaybackState(true, _backgroundAudioDurationSeconds); + } + + final _pauseButton = AndroidCustomMediaButton( + 'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled'); + final _replay10Button = AndroidCustomMediaButton( + 'replay10', replay10ButtonId, 'ic_stat_replay_10'); + final _forwardButton = AndroidCustomMediaButton( + 'forward', forwardButtonId, 'ic_stat_forward_30'); + final _playnowButton = AndroidCustomMediaButton( + 'playnow', likeButtonId, 'ic_stat_play_circle_filled'); +} diff --git a/lib/class/settingstate.dart b/lib/class/settingstate.dart index 8c9f1c1..362b5a6 100644 --- a/lib/class/settingstate.dart +++ b/lib/class/settingstate.dart @@ -8,10 +8,10 @@ class SettingState extends ChangeNotifier { int _theme; int get theme => _theme; - void setTheme(int theme) { + setTheme(int theme) async{ _theme = theme; notifyListeners(); - _saveTheme(theme); + await _saveTheme(theme); } @override diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index bf3a7d3..eb4e7b1 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -5,9 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:tsacdop/home/audioplayer.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:intl/intl.dart'; +import 'package:tuple/tuple.dart'; import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; @@ -66,116 +68,137 @@ class _EpisodeDetailState extends State { title: Text(widget.episodeItem.feedTitle), centerTitle: true, ), - body: Container( - color: Theme.of(context).primaryColor, - padding: EdgeInsets.all(10.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12.0), - alignment: Alignment.topLeft, - child: Text( - widget.episodeItem.title, - style: Theme.of(context).textTheme.headline5, - ), - ), - Container( - alignment: Alignment.centerLeft, - padding: EdgeInsets.symmetric(horizontal: 12.0), - height: 30.0, - child: Text( - 'Published ' + - DateFormat.yMMMd().format( - DateTime.fromMillisecondsSinceEpoch( - widget.episodeItem.pubDate)), - style: TextStyle(color: Colors.blue[500])), - ), - Container( - padding: EdgeInsets.all(12.0), - height: 50.0, - child: Row( - children: [ - (widget.episodeItem.explicit == 1) - ? Container( - decoration: BoxDecoration( - color: Colors.red[800], - shape: BoxShape.circle), - height: 25.0, - width: 25.0, - margin: EdgeInsets.only(right: 10.0), - alignment: Alignment.center, - child: Text('E', - style: TextStyle(color: Colors.white))) - : Center(), - Container( - decoration: BoxDecoration( - color: Colors.cyan[300], - borderRadius: - BorderRadius.all(Radius.circular(15.0))), - height: 30.0, - margin: EdgeInsets.only(right: 10.0), - padding: EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: Text( - (widget.episodeItem.duration).toString() + - 'mins', - style: textstyle), + body: Stack( + children: [ + Container( + color: Theme.of(context).primaryColor, + padding: EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.0), + alignment: Alignment.topLeft, + child: Text( + widget.episodeItem.title, + style: Theme.of(context).textTheme.headline5, ), - Container( - decoration: BoxDecoration( - color: Colors.lightBlue[300], - borderRadius: - BorderRadius.all(Radius.circular(15.0))), - height: 30.0, - margin: EdgeInsets.only(right: 10.0), - padding: EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: Text( - ((widget.episodeItem.enclosureLength) ~/ - 1000000) - .toString() + - 'MB', - style: textstyle), + ), + Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 12.0), + height: 30.0, + child: Text( + 'Published ' + + DateFormat.yMMMd().format( + DateTime.fromMillisecondsSinceEpoch( + widget.episodeItem.pubDate)), + style: TextStyle(color: Colors.blue[500])), + ), + Container( + padding: EdgeInsets.all(12.0), + height: 50.0, + child: Row( + children: [ + (widget.episodeItem.explicit == 1) + ? Container( + decoration: BoxDecoration( + color: Colors.red[800], + shape: BoxShape.circle), + height: 25.0, + width: 25.0, + margin: EdgeInsets.only(right: 10.0), + alignment: Alignment.center, + child: Text('E', + style: + TextStyle(color: Colors.white))) + : Center(), + Container( + decoration: BoxDecoration( + color: Colors.cyan[300], + borderRadius: BorderRadius.all( + Radius.circular(15.0))), + height: 30.0, + margin: EdgeInsets.only(right: 10.0), + padding: + EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text( + (widget.episodeItem.duration).toString() + + 'mins', + style: textstyle), + ), + Container( + decoration: BoxDecoration( + color: Colors.lightBlue[300], + borderRadius: BorderRadius.all( + Radius.circular(15.0))), + height: 30.0, + margin: EdgeInsets.only(right: 10.0), + padding: + EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text( + ((widget.episodeItem.enclosureLength) ~/ + 1000000) + .toString() + + 'MB', + style: textstyle), + ), + ], ), - ], - ), + ), + ], ), - ], - ), - ), - Expanded( - child: Container( - padding: EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0), - child: SingleChildScrollView( - child: _loaddes - ? (widget.episodeItem.description.contains('<')) - ? Html( - data: widget.episodeItem.description, - onLinkTap: (url) { - _launchUrl(url); - }, - useRichText: true, - ) - : Container( - alignment: Alignment.topLeft, - child: Text(widget.episodeItem.description)) - : Center(), ), - ), + Expanded( + child: Container( + padding: + EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0), + child: SingleChildScrollView( + child: _loaddes + ? (widget.episodeItem.description.contains('<')) + ? Html( + data: widget.episodeItem.description, + onLinkTap: (url) { + _launchUrl(url); + }, + useRichText: true, + ) + : Container( + alignment: Alignment.topLeft, + child: + Text(widget.episodeItem.description)) + : Center(), + ), + ), + ), + ], ), - MenuBar( - episodeItem: widget.episodeItem, - heroTag: widget.heroTag, - ), - ], - ), + ), + Selector( + selector: (_, audio) => audio.playerRunning, + builder: (_, data, __) { + return Container( + alignment: Alignment.bottomCenter, + padding: EdgeInsets.only( + left: 5.0, + right: 5.0, + bottom: data == true ? 80.0 : 10.0), + child: MenuBar( + episodeItem: widget.episodeItem, + heroTag: widget.heroTag, + ), + ); + }), + Container(child: PlayerWidget()), + ], ), ), ), @@ -233,39 +256,37 @@ class _MenuBarState extends State { @override Widget build(BuildContext context) { - final audioplay = Provider.of(context); + var audio = Provider.of(context, listen: false); return Container( height: 50.0, decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, + border: Border.all( + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey[200] + : Theme.of(context).scaffoldBackgroundColor, + ), borderRadius: BorderRadius.all(Radius.circular(10.0)), ), child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - (widget.episodeItem.title == audioplay.episode?.title && - audioplay.audioState == AudioState.play) - ? ImageRotate( - title: widget.episodeItem.feedTitle, - path: widget.episodeItem.imagePath, - ) - : Hero( - tag: widget.episodeItem.enclosureUrl + widget.heroTag, - child: Container( - padding: EdgeInsets.all(10.0), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(15.0)), - child: Container( - height: 30.0, - width: 30.0, - color: Theme.of(context).scaffoldBackgroundColor, - child: - Image.file(File("${widget.episodeItem.imagePath}")), - ), - ), - ), + Hero( + tag: widget.episodeItem.enclosureUrl + widget.heroTag, + child: Container( + padding: EdgeInsets.symmetric(horizontal:10.0), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(15.0)), + child: Container( + height: 30.0, + width: 30.0, + color: Theme.of(context).scaffoldBackgroundColor, + child: Image.file(File("${widget.episodeItem.imagePath}")), ), + ), + ), + ), (_like == 0 && !_liked) ? _buttonOnMenu( Icon( @@ -273,72 +294,86 @@ class _MenuBarState extends State { color: Colors.grey[700], ), () => saveLiked(widget.episodeItem.enclosureUrl)) - : Stack( - alignment: Alignment.center, - children: [ - LoveOpen(), - _buttonOnMenu( - Icon( - Icons.favorite, - color: Colors.red, - ), - () => setUnliked(widget.episodeItem.enclosureUrl)), - ], - ), + : (_like == 1 && !_liked) + ? _buttonOnMenu( + Icon( + Icons.favorite, + color: Colors.red, + ), + () => setUnliked(widget.episodeItem.enclosureUrl)) + : Stack( + alignment: Alignment.center, + children: [ + LoveOpen(), + _buttonOnMenu( + Icon( + Icons.favorite, + color: Colors.red, + ), + () => setUnliked(widget.episodeItem.enclosureUrl)), + ], + ), DownloadButton(episodeBrief: widget.episodeItem), _buttonOnMenu(Icon(Icons.playlist_add, color: Colors.grey[700]), () { Fluttertoast.showToast( - msg: 'Not support yet', + msg: 'Added to playlist', gravity: ToastGravity.BOTTOM, ); - /*TODO*/ + audio.addToPlaylist(widget.episodeItem); }), Spacer(), - (widget.episodeItem.title != audioplay.episode?.title) - ? Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.only( - topRight: Radius.circular(5.0), - bottomRight: Radius.circular(5.0)), - onTap: () { - audioplay.episodeLoad = widget.episodeItem; - }, - child: Container( - alignment: Alignment.center, - height: 50.0, - padding: EdgeInsets.symmetric(horizontal: 20.0), - child: Row( - children: [ - Text('Play Now', - style: TextStyle( + // Text(audio.audioState.toString()), + Selector>( + selector: (_, audio) => + Tuple2(audio.episode, audio.backgroundAudioPlaying), + builder: (_, data, __) { + return (widget.episodeItem.title != data.item1?.title) + ? Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.only( + topRight: Radius.circular(5.0), + bottomRight: Radius.circular(5.0)), + onTap: () { + audio.episodeLoad(widget.episodeItem); + }, + child: Container( + alignment: Alignment.center, + height: 50.0, + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Row( + children: [ + Text('Play Now', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 15, + fontWeight: FontWeight.bold, + )), + Icon( + Icons.play_arrow, color: Theme.of(context).accentColor, - fontSize: 15, - fontWeight: FontWeight.bold, - )), - Icon( - Icons.play_arrow, - color: Theme.of(context).accentColor, + ), + ], ), - ], + ), ), - ), - ), - ) - : (widget.episodeItem.title == audioplay.episode?.title && - audioplay.audioState == AudioState.play) - ? Container( - padding: EdgeInsets.only(right: 30), - child: - SizedBox(width: 20, height: 15, child: WaveLoader())) - : Container( - padding: EdgeInsets.only(right: 30), - child: SizedBox( - width: 20, - height: 15, - child: LineLoader(), - ), - ), + ) + : (widget.episodeItem.title == data.item1?.title && + data.item2 == true) + ? Container( + padding: EdgeInsets.only(right: 30), + child: SizedBox( + width: 20, height: 15, child: WaveLoader())) + : Container( + padding: EdgeInsets.only(right: 30), + child: SizedBox( + width: 20, + height: 15, + child: LineLoader(), + ), + ); + }, + ), ], ), ); diff --git a/lib/home/appbar/addpodcast.dart b/lib/home/appbar/addpodcast.dart index b135784..930972e 100644 --- a/lib/home/appbar/addpodcast.dart +++ b/lib/home/appbar/addpodcast.dart @@ -256,7 +256,7 @@ class _SearchResultState extends State { String _uuid = Uuid().v4(); File("${dir.path}/$_uuid.png") ..writeAsBytesSync(img.encodePng(thumbnail)); - + String _imagePath = "${dir.path}/$_uuid.png"; String _primaryColor = await getColor(File("${dir.path}/$_uuid.png")); String _author = _p.itunes.author ?? _p.author ?? ''; @@ -303,6 +303,11 @@ class _SearchResultState extends State { } return Container( + decoration: BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context), + ), + ), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -363,18 +368,19 @@ class _SearchResultState extends State { ? Container( alignment: Alignment.centerLeft, decoration: BoxDecoration( - color: Theme.of(context).primaryColorDark, + color: Theme.of(context).accentColor, borderRadius: BorderRadius.only( topRight: Radius.circular(15.0), bottomLeft: Radius.circular(15.0), bottomRight: Radius.circular(15.0), )), - margin: EdgeInsets.only(left: 70, right: 50), - padding: EdgeInsets.all(10.0), + margin: EdgeInsets.only(left: 70, right: 50, bottom: 10.0), + padding: EdgeInsets.all(15.0), child: Text( widget.onlinePodcast.description.trim(), maxLines: 3, overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText1.copyWith(color: Colors.white), ), ) : Center(), diff --git a/lib/home/audio_player.dart b/lib/home/audio_player.dart deleted file mode 100644 index b4531d7..0000000 --- a/lib/home/audio_player.dart +++ /dev/null @@ -1,718 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:provider/provider.dart'; - -import 'package:audiofileplayer/audiofileplayer.dart'; -import 'package:audiofileplayer/audio_system.dart'; -import 'package:logging/logging.dart'; -import 'package:flutter/material.dart'; -import 'package:marquee/marquee.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; -import 'package:tsacdop/class/audiostate.dart'; -import 'package:tsacdop/class/episodebrief.dart'; -import 'package:tsacdop/episodes/episodedetail.dart'; -import 'package:tsacdop/home/audiopanel.dart'; -import 'package:tsacdop/util/pageroute.dart'; - -final Logger _logger = Logger('audiofileplayer'); - -class PlayerWidget extends StatefulWidget { - @override - _PlayerWidgetState createState() => _PlayerWidgetState(); -} - -class _PlayerWidgetState extends State { - static const String replay10ButtonId = 'replay10ButtonId'; - static const String newReleasesButtonId = 'newReleasesButtonId'; - static const String likeButtonId = 'likeButtonId'; - static const String pausenowButtonId = 'pausenowButtonId'; - static const String forwardButtonId = 'forwardButtonId'; - - Audio _backgroundAudio; - bool _backgroundAudioPlaying; - double _backgroundAudioDurationSeconds; - double _backgroundAudioPositionSeconds = 0; - bool _remoteAudioLoading; - String _remoteErrorMessage; - double _seekSliderValue = 0.0; - String url; - bool _isLoading; - Color _c; - EpisodeBrief episode; - - @override - void initState() { - super.initState(); - AudioSystem.instance.addMediaEventListener(_mediaEventListener); - _isLoading = false; - _remoteAudioLoading = true; - } - - //open episoddetail page - _gotoEpisode() async { - Navigator.push( - context, - SlideUptRoute( - page: EpisodeDetail(episodeItem: episode, heroTag: 'playpanel')), - ); - } - - //init audio player from url - void _initbackgroundAudioPlayer(String url) { - _remoteErrorMessage = null; - _remoteAudioLoading = true; - Provider.of(context, listen: false).audioState = AudioState.load; - - if (_backgroundAudioPlaying == true) { - _backgroundAudio?.pause(); - AudioSystem.instance.stopBackgroundDisplay(); - } - _backgroundAudio?.dispose(); - _backgroundAudio = Audio.loadFromRemoteUrl(url, - onDuration: (double durationSeconds) { - setState(() { - _backgroundAudioDurationSeconds = durationSeconds; - _remoteAudioLoading = false; - Provider.of(context, listen: false).audioState = - AudioState.play; - }); - _setNotification(); - }, - onPosition: (double positionSeconds) { - setState(() { - if (_backgroundAudioPositionSeconds < - _backgroundAudioDurationSeconds) { - _seekSliderValue = _backgroundAudioPositionSeconds / - _backgroundAudioDurationSeconds; - _backgroundAudioPositionSeconds = positionSeconds; - } else { - _seekSliderValue = 1; - _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; - } - }); - }, - onError: (String message) => setState(() { - _remoteErrorMessage = message; - _backgroundAudio.dispose(); - _backgroundAudio = null; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - Provider.of(context, listen: false).audioState = - AudioState.error; - }), - onComplete: () => setState(() { - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - Provider.of(context, listen: false).audioState = - AudioState.complete; - }), - looping: false, - playInBackground: true) - ..play(); - } - - //init audio player form local file - void _initbackgroundAudioPlayerLocal(String path) { - _remoteErrorMessage = null; - _remoteAudioLoading = true; - ByteData audio = getAudio(path); - Provider.of(context, listen: false).audioState = AudioState.load; - if (_backgroundAudioPlaying == true) { - _backgroundAudio?.pause(); - AudioSystem.instance.stopBackgroundDisplay(); - } - _backgroundAudio?.dispose(); - _backgroundAudio = Audio.loadFromByteData(audio, - onDuration: (double durationSeconds) { - setState(() { - _backgroundAudioDurationSeconds = durationSeconds; - _remoteAudioLoading = false; - }); - _setNotification(); - Provider.of(context, listen: false).audioState = - AudioState.play; - }, - onPosition: (double positionSeconds) { - setState(() { - if (_backgroundAudioPositionSeconds < - _backgroundAudioDurationSeconds) { - _seekSliderValue = _backgroundAudioPositionSeconds / - _backgroundAudioDurationSeconds; - _backgroundAudioPositionSeconds = positionSeconds; - } else { - _seekSliderValue = 1; - _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; - } - }); - }, - onError: (String message) => setState(() { - _remoteErrorMessage = message; - _backgroundAudio.dispose(); - _backgroundAudio = null; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - Provider.of(context, listen: false).audioState = - AudioState.error; - }), - onComplete: () => setState(() { - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - Provider.of(context, listen: false).audioState = - AudioState.complete; - }), - looping: false, - playInBackground: true) - ..play(); - } - - //if downloaded - Future _getFile(String url) async { - final task = await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE url = '$url' AND status = 3"); - if (task.length != 0) { - String _filePath = task.first.savedDir + '/' + task.first.filename; - return _filePath; - } - return 'NotDownload'; - } - - //get local audio file - ByteData getAudio(String path) { - File audioFile = File(path); - Uint8List audio = audioFile.readAsBytesSync(); - return ByteData.view(audio.buffer); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - final url = Provider.of(context).episode?.enclosureUrl; - if (url != this.url) { - setState(() { - this.url = url; - episode = Provider.of(context).episode; - _backgroundAudioPlaying = true; - _isLoading = true; - _getFile(url).then((result) { - result == 'NotDownload' - ? _initbackgroundAudioPlayer(url) - : _initbackgroundAudioPlayerLocal(result); - }); - }); - } - } - - @override - void dispose() { - AudioSystem.instance.removeMediaEventListener(_mediaEventListener); - _backgroundAudio?.dispose(); - super.dispose(); - } - - static String _stringForSeconds(double seconds) { - if (seconds == null) return null; - return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; - } - - void _mediaEventListener(MediaEvent mediaEvent) { - _logger.info('App received media event of type: ${mediaEvent.type}'); - final MediaActionType type = mediaEvent.type; - if (type == MediaActionType.play) { - _resumeBackgroundAudio(); - } else if (type == MediaActionType.pause) { - _pauseBackgroundAudio(); - } else if (type == MediaActionType.playPause) { - _backgroundAudioPlaying - ? _pauseBackgroundAudio() - : _resumeBackgroundAudio(); - } else if (type == MediaActionType.stop) { - _stopBackgroundAudio(); - } else if (type == MediaActionType.seekTo) { - _backgroundAudio.seek(mediaEvent.seekToPositionSeconds); - AudioSystem.instance - .setPlaybackState(true, mediaEvent.seekToPositionSeconds); - } else if (type == MediaActionType.skipForward) { - final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; - _forwardBackgroundAudio(skipIntervalSeconds); - _logger.info( - 'Skip-forward event had skipIntervalSeconds $skipIntervalSeconds.'); - } else if (type == MediaActionType.skipBackward) { - final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; - _forwardBackgroundAudio(skipIntervalSeconds); - _logger.info( - 'Skip-backward event had skipIntervalSeconds $skipIntervalSeconds.'); - } else if (type == MediaActionType.custom) { - if (mediaEvent.customEventId == replay10ButtonId) { - _forwardBackgroundAudio(-10.0); - } else if (mediaEvent.customEventId == likeButtonId) { - _resumeBackgroundAudio(); - } else if (mediaEvent.customEventId == forwardButtonId) { - _forwardBackgroundAudio(30.0); - } else if (mediaEvent.customEventId == pausenowButtonId) { - _pauseBackgroundAudio(); - } - } - } - - final _pauseButton = AndroidCustomMediaButton( - 'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled'); - final _replay10Button = AndroidCustomMediaButton( - 'replay10', replay10ButtonId, 'ic_stat_replay_10'); - final _forwardButton = AndroidCustomMediaButton( - 'forward', forwardButtonId, 'ic_stat_forward_30'); - final _playnowButton = AndroidCustomMediaButton( - 'playnow', likeButtonId, 'ic_stat_play_circle_filled'); - - Future _setNotification() async { - final Uint8List imageBytes = File('${episode.imagePath}').readAsBytesSync(); - AudioSystem.instance.setMetadata(AudioMetadata( - title: episode.title, - artist: episode.feedTitle, - album: episode.feedTitle, - genre: "Podcast", - durationSeconds: _backgroundAudioDurationSeconds, - artBytes: imageBytes)); - AudioSystem.instance - .setPlaybackState(true, _backgroundAudioPositionSeconds); - AudioSystem.instance.setAndroidNotificationButtons([ - AndroidMediaButtonType.pause, - _forwardButton, - AndroidMediaButtonType.stop, - ], androidCompactIndices: [ - 0, - 1 - ]); - - AudioSystem.instance.setSupportedMediaActions({ - MediaActionType.playPause, - MediaActionType.pause, - MediaActionType.next, - MediaActionType.previous, - MediaActionType.skipForward, - MediaActionType.skipBackward, - MediaActionType.seekTo, - MediaActionType.custom, - }, skipIntervalSeconds: 30); - } - - Future _resumeBackgroundAudio() async { - _backgroundAudio.resume(); - setState(() { - _backgroundAudioPlaying = true; - Provider.of(context, listen: false).audioState = - AudioState.play; - }); - final Uint8List imageBytes = File('${episode.imagePath}').readAsBytesSync(); - AudioSystem.instance.setMetadata(AudioMetadata( - title: episode.title, - artist: episode.feedTitle, - album: episode.feedTitle, - genre: "Podcast", - durationSeconds: _backgroundAudioDurationSeconds, - artBytes: imageBytes)); - - AudioSystem.instance - .setPlaybackState(true, _backgroundAudioPositionSeconds); - - AudioSystem.instance.setAndroidNotificationButtons([ - AndroidMediaButtonType.pause, - _forwardButton, - AndroidMediaButtonType.stop, - ], androidCompactIndices: [ - 0, - 1 - ]); - - AudioSystem.instance.setSupportedMediaActions({ - MediaActionType.playPause, - MediaActionType.pause, - MediaActionType.next, - MediaActionType.previous, - MediaActionType.skipForward, - MediaActionType.skipBackward, - MediaActionType.seekTo, - MediaActionType.custom, - }, skipIntervalSeconds: 30); - } - - void _pauseBackgroundAudio() { - _backgroundAudio.pause(); - setState(() { - _backgroundAudioPlaying = false; - Provider.of(context, listen: false).audioState = - AudioState.pause; - }); - - AudioSystem.instance - .setPlaybackState(false, _backgroundAudioPositionSeconds); - - AudioSystem.instance.setAndroidNotificationButtons([ - AndroidMediaButtonType.play, - _forwardButton, - AndroidMediaButtonType.stop, - ], androidCompactIndices: [ - 0, - 1, - ]); - - AudioSystem.instance.setSupportedMediaActions({ - MediaActionType.playPause, - MediaActionType.play, - MediaActionType.next, - MediaActionType.previous, - }); - } - - void _stopBackgroundAudio() { - _backgroundAudio.pause(); - setState(() => _backgroundAudioPlaying = false); - AudioSystem.instance.stopBackgroundDisplay(); - } - - void _forwardBackgroundAudio(double seconds) { - final double forwardposition = _backgroundAudioPositionSeconds + seconds; - _backgroundAudio.seek(forwardposition); - AudioSystem.instance.setPlaybackState(true, forwardposition); - } - - Widget _expandedPanel(BuildContext context) => Container( - height: 300, - color: Theme.of(context).primaryColor, - padding: EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 80.0, - padding: EdgeInsets.all(20), - alignment: Alignment.center, - child: (episode.title.length > 10) - ? Marquee( - text: episode.title, - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 18), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 30.0, - velocity: 50.0, - pauseAfterRound: Duration(seconds: 1), - startPadding: 30.0, - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.linear, - decelerationDuration: Duration(milliseconds: 500), - decelerationCurve: Curves.easeOut, - ) - : Text( - episode.title, - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 20), - ), - ), - Container( - padding: EdgeInsets.only(left: 30, right: 30), - child: SliderTheme( - data: SliderTheme.of(context).copyWith( - activeTrackColor: Colors.blue[100], - inactiveTrackColor: Colors.grey[300], - trackHeight: 3.0, - thumbColor: Colors.blue[400], - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6.0), - overlayColor: Colors.blue.withAlpha(32), - overlayShape: RoundSliderOverlayShape(overlayRadius: 14.0), - ), - child: Slider( - value: _seekSliderValue, - onChanged: (double val) { - setState(() => _seekSliderValue = val); - final double positionSeconds = - val * _backgroundAudioDurationSeconds; - _backgroundAudio.seek(positionSeconds); - AudioSystem.instance - .setPlaybackState(true, positionSeconds); - }), - ), - ), - Container( - height: 20.0, - padding: EdgeInsets.symmetric(horizontal: 50.0), - child: Row( - children: [ - Text( - _stringForSeconds(_backgroundAudioPositionSeconds) ?? '', - style: TextStyle(fontSize: 10), - ), - Expanded( - child: Container( - alignment: Alignment.center, - child: _remoteErrorMessage != null - ? Text(_remoteErrorMessage, - style: const TextStyle( - color: const Color(0xFFFF0000))) - : Text( - _remoteAudioLoading ? 'Buffring...' : '', - style: TextStyle( - color: Theme.of(context).accentColor), - ), - ), - ), - Text( - _stringForSeconds(_backgroundAudioDurationSeconds) ?? '', - style: TextStyle(fontSize: 10), - ), - ], - ), - ), - Container( - height: 100, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: _backgroundAudioPlaying - ? () => _forwardBackgroundAudio(-10) - : null, - iconSize: 32.0, - icon: Icon(Icons.replay_10), - color: Theme.of(context).tabBarTheme.labelColor), - ), - _backgroundAudioPlaying - ? Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: _backgroundAudioPlaying - ? () { - _pauseBackgroundAudio(); - } - : null, - iconSize: 40.0, - icon: Icon(Icons.pause_circle_filled), - color: - Theme.of(context).tabBarTheme.labelColor), - ) - : Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: _backgroundAudioPlaying - ? null - : () { - _resumeBackgroundAudio(); - }, - iconSize: 40.0, - icon: Icon(Icons.play_circle_filled), - color: - Theme.of(context).tabBarTheme.labelColor), - ), - Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: _backgroundAudioPlaying - ? () => _forwardBackgroundAudio(30) - : null, - iconSize: 32.0, - icon: Icon(Icons.forward_30), - color: Theme.of(context).tabBarTheme.labelColor), - ), - ], - ), - ), - Spacer(), - Container( - height: 50.0, - margin: EdgeInsets.symmetric(vertical: 10.0), - padding: EdgeInsets.symmetric(horizontal: 10.0), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.all(10.0), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(15.0)), - child: Container( - height: 30.0, - width: 30.0, - child: Image.file(File("${episode.imagePath}"))), - ), - ), - Spacer(), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _gotoEpisode(), - child: Icon(Icons.info), - ), - ), - ], - )) - ]), - ); - Widget _miniPanel(double width, BuildContext context) { - var color = json.decode(episode?.primaryColor); - if (color) { - if (Theme.of(context).brightness == Brightness.light) { - _c = (color[0] > 200 && color[1] > 200 && color[2] > 200) - ? Color.fromRGBO( - (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) - : Color.fromRGBO(color[0], color[1], color[2], 1.0); - } else { - _c = (color[0] < 50 && color[1] < 50 && color[2] < 50) - ? Color.fromRGBO( - (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) - : Color.fromRGBO(color[0], color[1], color[2], 1.0); - } - } - return Container( - height: 60, - color: Theme.of(context).primaryColor, - child: - Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - SizedBox( - height: 2, - child: LinearProgressIndicator( - value: _seekSliderValue, - backgroundColor: Theme.of(context).primaryColor, - valueColor: AlwaysStoppedAnimation(_c), - )), - Expanded( - child: Container( - padding: EdgeInsets.only(left: 15, right: 10), - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 4, - child: Container( - padding: EdgeInsets.symmetric(vertical: 5), - alignment: Alignment.centerLeft, - child: (episode.title.length > 55) - ? Marquee( - text: episode.title, - style: TextStyle(fontWeight: FontWeight.bold), - scrollAxis: Axis.vertical, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 30.0, - velocity: 50.0, - pauseAfterRound: Duration(seconds: 1), - startPadding: 30.0, - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.linear, - decelerationDuration: Duration(milliseconds: 500), - decelerationCurve: Curves.easeOut, - ) - : Text( - episode.title, - style: TextStyle(fontWeight: FontWeight.bold), - maxLines: 2, - ), - ), - ), - Expanded( - flex: 2, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10), - alignment: Alignment.center, - child: _remoteAudioLoading - ? Text( - 'Buffring...', - style: - TextStyle(color: Theme.of(context).accentColor), - ) - : Row( - children: [ - Text( - _stringForSeconds( - _backgroundAudioDurationSeconds - - _backgroundAudioPositionSeconds) ?? - '', - style: TextStyle(color: _c), - ), - Text( - ' Left', - style: TextStyle(color: _c), - ), - ], - ), - ), - ), - Expanded( - flex: 2, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _backgroundAudioPlaying - ? Material( - color: Colors.transparent, - child: IconButton( - onPressed: _backgroundAudioPlaying - ? () { - _pauseBackgroundAudio(); - } - : null, - iconSize: 25.0, - icon: Icon(Icons.pause_circle_filled), - color: - Theme.of(context).tabBarTheme.labelColor), - ) - : Material( - color: Colors.transparent, - child: IconButton( - onPressed: _backgroundAudioPlaying - ? null - : () { - _resumeBackgroundAudio(); - }, - iconSize: 25.0, - icon: Icon(Icons.play_circle_filled), - color: - Theme.of(context).tabBarTheme.labelColor), - ), - Material( - color: Colors.transparent, - child: IconButton( - onPressed: _backgroundAudioPlaying - ? () => _forwardBackgroundAudio(30) - : null, - iconSize: 25.0, - icon: Icon(Icons.forward_30), - color: Theme.of(context).tabBarTheme.labelColor), - ), - ], - ), - ), - ], - ), - ), - ), - ]), - ); - } - - @override - Widget build(BuildContext context) { - double _width = MediaQuery.of(context).size.width; - - return !_isLoading - ? Center() - : AudioPanel( - miniPanel: _miniPanel(_width, context), - expandedPanel: _expandedPanel(context)); - } -} diff --git a/lib/home/audiopanel.dart b/lib/home/audiopanel.dart index 01e160b..ce7ac7f 100644 --- a/lib/home/audiopanel.dart +++ b/lib/home/audiopanel.dart @@ -46,7 +46,9 @@ class _AudioPanelState extends State child: GestureDetector( onTap: () => _backToMini(), child: Container( - color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5), + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.5), ), ), ) @@ -75,7 +77,16 @@ class _AudioPanelState extends State ), ) : Container( - color: Theme.of(context).primaryColor, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + boxShadow: [ + BoxShadow( + offset: Offset(0, -1), + blurRadius: 4, + color: Colors.grey[400], + ), + ], + ), child: SingleChildScrollView( child: Opacity( opacity: _animation.value < (maxSize - 50) @@ -99,7 +110,7 @@ class _AudioPanelState extends State setState(() { _animation = Tween(begin: initSize, end: minSize).animate(_controller); - initSize = minSize; + initSize = minSize; }); _controller.forward(); } diff --git a/lib/home/home.dart b/lib/home/home.dart index 4faeae6..dfd5e85 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'hometab.dart'; import 'package:tsacdop/home/appbar/importompl.dart'; -import 'package:tsacdop/home/audio_player.dart'; +import 'package:tsacdop/home/audioplayer.dart'; import 'homescroll.dart'; class Home extends StatelessWidget { diff --git a/lib/home/homescroll.dart b/lib/home/homescroll.dart index 2de0a7e..95d3dfa 100644 --- a/lib/home/homescroll.dart +++ b/lib/home/homescroll.dart @@ -263,7 +263,10 @@ class _ScrollPodcastsState extends State { .map((PodcastLocal podcastLocal) { return Container( decoration: BoxDecoration( - color: Theme.of(context).primaryColor), + color: Theme.of(context).brightness == + Brightness.light + ? Theme.of(context).primaryColor + : Colors.black12), margin: EdgeInsets.symmetric(horizontal: 5.0), key: ObjectKey(podcastLocal.title), child: PodcastPreview( @@ -426,19 +429,23 @@ class ShowEpisode extends StatelessWidget { }, child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - color: Theme.of(context).scaffoldBackgroundColor, - border: Border.all( - color: Theme.of(context).primaryColor, - width: 3.0, - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).primaryColor, - blurRadius: 1.0, - spreadRadius: 0.5, - ), - ]), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.of(context).scaffoldBackgroundColor, + border: Border.all( + color: Theme.of(context).brightness == Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).scaffoldBackgroundColor, + // color: Theme.of(context).primaryColor, + width: 3.0, + ), + // boxShadow: [ + // BoxShadow( + // color: Theme.of(context).primaryColor, + // blurRadius: 1.0, + // spreadRadius: 0.5, + // ), + // ] + ), alignment: Alignment.center, padding: EdgeInsets.all(10.0), child: Column( diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index cf74521..5556b9a 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -42,4 +42,16 @@ class KeyValueStorage { if(prefs.getInt(key) == null) await prefs.setInt(key, 0); return prefs.getInt(key); } + + Future savePlaylist(List playList) async{ + SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.setStringList(key, playList); + } + + Future> getPlayList() async{ + SharedPreferences prefs = await SharedPreferences.getInstance(); + if(prefs.getStringList(key) == null) {await prefs.setStringList(key, []);} + print(prefs.getStringList(key).toString()); + return prefs.getStringList(key); + } } diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 9101f07..d6525ff 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -7,6 +7,7 @@ import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:path_provider/path_provider.dart'; import 'package:dio/dio.dart'; import 'package:tsacdop/class/podcastlocal.dart'; +import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/webfeed/webfeed.dart'; @@ -36,12 +37,14 @@ class DBHelper { description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER, duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0, downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)"""); + await db.execute( + """CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE, + seconds INTEGER, seek_value INTEGER, add_date INTEGER)"""); } Future> getPodcastLocal(List podcasts) async { var dbClient = await database; List podcastLocal = List(); - await Future.forEach(podcasts, (s) async { List list; list = await dbClient.rawQuery( @@ -66,7 +69,7 @@ class DBHelper { var dbClient = await database; List list = await dbClient.rawQuery( 'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath, email, provider, link FROM PodcastLocal ORDER BY add_date DESC'); - + List podcastLocal = List(); for (int i = 0; i < list.length; i++) { @@ -78,7 +81,7 @@ class DBHelper { list[i]['author'], list[i]['id'], list[i]['imagePath'], - list.first['email'], + list.first['email'], list.first['provider'], list.first['link'])); } @@ -132,6 +135,24 @@ class DBHelper { await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]); } + Future saveHistory(PlayHistory history) async { + var dbClient = await database; + int _milliseconds = DateTime.now().millisecondsSinceEpoch; + int result = await dbClient.transaction((txn) async { + return await txn.rawInsert( + """REPLACE INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date) + VALUES (?, ?, ?, ?, ?) """, + [ + history.title, + history.url, + history.seconds, + history.seekValue, + _milliseconds + ]); + }); + return result; + } + DateTime _parsePubDate(String pubDate) { if (pubDate == null) return DateTime.now(); print(pubDate); @@ -156,11 +177,13 @@ class DBHelper { if (year != null && time != null && month != null) { date = DateFormat('dd MMM yyyy HH:mm', 'en_US') .parse(month + year + time); - } else if(year!=null && time!=null && month == null){ + } else if (year != null && time != null && month == null) { String month = mmDd.stringMatch(pubDate); date = DateFormat('mm-dd yyyy HH:mm', 'en_US') - .parse(month +' ' + year +' '+ time); - } else {date = DateTime.now();} + .parse(month + ' ' + year + ' ' + time); + } else { + date = DateTime.now(); + } } } } @@ -488,8 +511,8 @@ class DBHelper { Future getFeedDescription(String id) async { var dbClient = await database; - List list = await dbClient.rawQuery( - 'SELECT description FROM PodcastLocal WHERE id = ?', [id]); + List list = await dbClient + .rawQuery('SELECT description FROM PodcastLocal WHERE id = ?', [id]); String description = list[0]['description']; return description; } diff --git a/lib/main.dart b/lib/main.dart index 97aa55b..6baf747 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ void main() async { runApp( MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => AudioPlay()), + ChangeNotifierProvider(create: (context) => AudioPlayer()), ChangeNotifierProvider(create: (context) => ImportOmpl()), ChangeNotifierProvider(create: (context) => SettingState()), ChangeNotifierProvider(create: (context) => GroupList()), @@ -29,10 +29,11 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { var theme = Provider.of(context).theme; + print(theme); return MaterialApp( themeMode: theme == 0 ? ThemeMode.system - : theme == 1 ? ThemeMode.dark : ThemeMode.light, + : theme == 1 ? ThemeMode.light: ThemeMode.dark, debugShowCheckedModeBanner: false, title: 'TsacDop', theme: ThemeData( diff --git a/lib/podcasts/podcastdetail.dart b/lib/podcasts/podcastdetail.dart index cecc2c5..4d8da1a 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -15,6 +15,7 @@ import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/util/pageroute.dart'; +import 'package:tsacdop/home/audioplayer.dart'; class PodcastDetail extends StatefulWidget { PodcastDetail({Key key, this.podcastLocal}) : super(key: key); @@ -73,11 +74,11 @@ class _PodcastDetailState extends State { double _width = MediaQuery.of(context).size.width; Color _color; var color = json.decode(widget.podcastLocal.primaryColor); - (color[0] > 200 && color[1] > 200 && color[2] > 200) - ? _color = Color.fromRGBO( - (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) - : _color = Color.fromRGBO(color[0], color[1], color[2], 1.0); - + (color[0] > 200 && color[1] > 200 && color[2] > 200) + ? _color = Color.fromRGBO( + (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) + : _color = Color.fromRGBO(color[0], color[1], color[2], 1.0); + return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Brightness.light, @@ -91,286 +92,317 @@ class _PodcastDetailState extends State { key: _refreshIndicatorKey, color: Theme.of(context).accentColor, onRefresh: () => _updateRssItem(widget.podcastLocal), - child: FutureBuilder>( - future: _getRssItem(widget.podcastLocal), - builder: (context, snapshot) { - if (snapshot.hasError) print(snapshot.error); - return (snapshot.hasData) - ? CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - primary: true, - slivers: [ - SliverAppBar( - elevation: 0, - iconTheme: IconThemeData(color: Colors.white), - expandedHeight: 170, - backgroundColor: _color, - floating: true, - pinned: true, - flexibleSpace: LayoutBuilder(builder: - (BuildContext context, - BoxConstraints constraints) { - top = constraints.biggest.height; - return FlexibleSpaceBar( - background: Stack( - children: [ - Container( - margin: EdgeInsets.only(top: 120), - padding: EdgeInsets.only(left: 80, right: 120), - color: Colors.white10, - alignment: Alignment.centerLeft, - child: Column( - mainAxisAlignment: - MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(widget.podcastLocal.author ?? '', - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Colors.grey[300])), - Text(widget.podcastLocal.provider ?? '', - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Colors.grey[300])) - ], - ), + child: Stack( + children: [ + FutureBuilder>( + future: _getRssItem(widget.podcastLocal), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return (snapshot.hasData) + ? CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + primary: true, + slivers: [ + SliverAppBar( + elevation: 0, + iconTheme: IconThemeData(color: Colors.white), + expandedHeight: 170, + backgroundColor: _color, + floating: true, + pinned: true, + flexibleSpace: LayoutBuilder(builder: + (BuildContext context, + BoxConstraints constraints) { + top = constraints.biggest.height; + return FlexibleSpaceBar( + background: Stack( + children: [ + Container( + margin: EdgeInsets.only(top: 120), + padding: EdgeInsets.only( + left: 80, right: 120), + color: Colors.white10, + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + widget.podcastLocal.author ?? + '', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith( + color: + Colors.grey[300])), + Text( + widget.podcastLocal.provider ?? + '', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith( + color: + Colors.grey[300])) + ], + ), + ), + Container( + alignment: Alignment.centerRight, + padding: EdgeInsets.only(right: 10), + child: SizedBox( + height: 120, + child: Image.file(File( + "${widget.podcastLocal.imagePath}")), + ), + ), + Container( + alignment: Alignment.center, + child: podcastInfo(context), + ), + ], ), - Container( - alignment: Alignment.centerRight, - padding: EdgeInsets.only(right: 10), - child: SizedBox( - height: 120, - child: Image.file(File( - "${widget.podcastLocal.imagePath}")), - ), - ), - Container( - alignment: Alignment.center, - child: podcastInfo(context), - ), - ], - ), - title: top < 70 - ? Text(widget.podcastLocal.title, - style: TextStyle(color: Colors.white)) - : Center(), - ); - }), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return Container( - padding: EdgeInsets.only( - left: 10.0, - right: 10.0, - top: 20.0, - bottom: 10.0), - alignment: Alignment.topLeft, - color: - Theme.of(context).scaffoldBackgroundColor, - child: AboutPodcast( - podcastLocal: widget.podcastLocal), - ); - }, - childCount: 1, - ), - ), - SliverPadding( - padding: const EdgeInsets.all(5.0), - sliver: SliverGrid( - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1.0, - crossAxisCount: 3, - mainAxisSpacing: 6.0, - crossAxisSpacing: 6.0, + title: top < 70 + ? Text(widget.podcastLocal.title, + style: TextStyle(color: Colors.white)) + : Center(), + ); + }), ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - EpisodeBrief episodeBrief = - snapshot.data[index]; - Color _c; - var color = json - .decode(widget.podcastLocal.primaryColor); - if (Theme.of(context).brightness == - Brightness.light) { - (color[0] > 200 && - color[1] > 200 && - color[2] > 200) - ? _c = Color.fromRGBO((255 - color[0]), - 255 - color[1], 255 - color[2], 1.0) - : _c = Color.fromRGBO( - color[0], color[1], color[2], 1.0); - } else { - (color[0] < 50 && - color[1] < 50 && - color[2] < 50) - ? _c = Color.fromRGBO((255 - color[0]), - 255 - color[1], 255 - color[2], 1.0) - : _c = Color.fromRGBO( - color[0], color[1], color[2], 1.0); - } - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push( - context, - ScaleRoute( - page: EpisodeDetail( - episodeItem: episodeBrief, - heroTag: 'podcast', - )), - ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(5.0)), - color: Theme.of(context) - .scaffoldBackgroundColor, - border: Border.all( - color: Theme.of(context) - .brightness == - Brightness.light - ? Theme.of(context).primaryColor - : Theme.of(context) - .scaffoldBackgroundColor, - width: 3.0, - ), - boxShadow: [ - BoxShadow( + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Container( + padding: EdgeInsets.only( + left: 10.0, + right: 10.0, + top: 20.0, + bottom: 10.0), + alignment: Alignment.topLeft, + color: Theme.of(context) + .scaffoldBackgroundColor, + child: AboutPodcast( + podcastLocal: widget.podcastLocal), + ); + }, + childCount: 1, + ), + ), + SliverPadding( + padding: const EdgeInsets.all(5.0), + sliver: SliverGrid( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 1.0, + crossAxisCount: 3, + mainAxisSpacing: 6.0, + crossAxisSpacing: 6.0, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + EpisodeBrief episodeBrief = + snapshot.data[index]; + Color _c; + var color = json.decode( + widget.podcastLocal.primaryColor); + if (Theme.of(context).brightness == + Brightness.light) { + (color[0] > 200 && + color[1] > 200 && + color[2] > 200) + ? _c = Color.fromRGBO( + (255 - color[0]), + 255 - color[1], + 255 - color[2], + 1.0) + : _c = Color.fromRGBO(color[0], + color[1], color[2], 1.0); + } else { + (color[0] < 50 && + color[1] < 50 && + color[2] < 50) + ? _c = Color.fromRGBO( + (255 - color[0]), + 255 - color[1], + 255 - color[2], + 1.0) + : _c = Color.fromRGBO(color[0], + color[1], color[2], 1.0); + } + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.push( + context, + ScaleRoute( + page: EpisodeDetail( + episodeItem: episodeBrief, + heroTag: 'podcast', + )), + ); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(5.0)), color: Theme.of(context) - .primaryColor, - blurRadius: 0.5, - spreadRadius: 0.5, - ), - ]), - alignment: Alignment.center, - padding: EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Expanded( - flex: 2, - child: Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - Hero( - tag: episodeBrief - .enclosureUrl + - 'podcast', - child: Container( - child: ClipRRect( - borderRadius: - BorderRadius.all( - Radius.circular( - _width / 32)), - child: Container( - height: _width / 16, - width: _width / 16, - child: Image.file(File( - "${episodeBrief.imagePath}")), - ), - ), - ), - ), - Spacer(), - Container( - alignment: Alignment.topRight, - child: Text( - (snapshot.data.length - - index) - .toString(), - style: GoogleFonts.teko( - textStyle: TextStyle( - fontSize: _width / 24, - color: _c, - ), - ), - ), - ) - ], - ), - ), - Expanded( - flex: 5, - child: Container( - alignment: Alignment.topLeft, - padding: - EdgeInsets.only(top: 2.0), - child: Text( - episodeBrief.title, - style: TextStyle( - fontSize: _width / 32, - ), - maxLines: 4, - overflow: TextOverflow.fade, + .scaffoldBackgroundColor, + border: Border.all( + color: Theme.of(context) + .brightness == + Brightness.light + ? Theme.of(context) + .primaryColor + : Theme.of(context) + .scaffoldBackgroundColor, + width: 3.0, ), - ), - ), - Expanded( - flex: 1, - child: Row( - children: [ - Align( - alignment: - Alignment.bottomLeft, - child: Text( - episodeBrief.dateToString(), - //podcast[index].pubDate.substring(4, 16), - style: TextStyle( - fontSize: _width / 35, - color: _c, - fontStyle: - FontStyle.italic), - ), + boxShadow: [ + BoxShadow( + color: Theme.of(context) + .primaryColor, + blurRadius: 0.5, + spreadRadius: 0.5, ), - Spacer(), - DownloadIcon( - episodeBrief: episodeBrief), - Padding( - padding: EdgeInsets.all(1), - ), - Container( - alignment: - Alignment.bottomRight, - child: (episodeBrief.liked == - 0) - ? Center() - : IconTheme( - data: IconThemeData( - size: 15), - child: Icon( - Icons.favorite, - color: Colors.red, + ]), + alignment: Alignment.center, + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Hero( + tag: episodeBrief + .enclosureUrl + + 'podcast', + child: Container( + child: ClipRRect( + borderRadius: + BorderRadius.all( + Radius.circular( + _width / + 32)), + child: Container( + height: _width / 16, + width: _width / 16, + child: Image.file(File( + "${episodeBrief.imagePath}")), ), ), + ), + ), + Spacer(), + Container( + alignment: + Alignment.topRight, + child: Text( + (snapshot.data.length - + index) + .toString(), + style: GoogleFonts.teko( + textStyle: TextStyle( + fontSize: + _width / 24, + color: _c, + ), + ), + ), + ) + ], ), - ], - ), + ), + Expanded( + flex: 5, + child: Container( + alignment: Alignment.topLeft, + padding: + EdgeInsets.only(top: 2.0), + child: Text( + episodeBrief.title, + style: TextStyle( + fontSize: _width / 32, + ), + maxLines: 4, + overflow: TextOverflow.fade, + ), + ), + ), + Expanded( + flex: 1, + child: Row( + children: [ + Align( + alignment: + Alignment.bottomLeft, + child: Text( + episodeBrief + .dateToString(), + //podcast[index].pubDate.substring(4, 16), + style: TextStyle( + fontSize: + _width / 35, + color: _c, + fontStyle: FontStyle + .italic), + ), + ), + Spacer(), + DownloadIcon( + episodeBrief: + episodeBrief), + Padding( + padding: + EdgeInsets.all(1), + ), + Container( + alignment: + Alignment.bottomRight, + child: (episodeBrief + .liked == + 0) + ? Center() + : IconTheme( + data: + IconThemeData( + size: 15), + child: Icon( + Icons.favorite, + color: + Colors.red, + ), + ), + ), + ], + ), + ), + ], ), - ], + ), ), - ), - ), - ); - }, - childCount: snapshot.data.length, + ); + }, + childCount: snapshot.data.length, + ), + ), ), - ), - ), - ], - ) - : Center(child: CircularProgressIndicator()); - }, + ], + ) + : Center(child: CircularProgressIndicator()); + }, + ), + Container(child: PlayerWidget()), + ], ), )), ), @@ -428,21 +460,23 @@ class _AboutPodcastState extends State { }, child: !_expand ? Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( _description, maxLines: 3, overflow: TextOverflow.ellipsis, ), - Container( - alignment: Alignment.center, - child: Icon(Icons.keyboard_arrow_down,), - ), - ], - ) + Container( + alignment: Alignment.center, + child: Icon( + Icons.keyboard_arrow_down, + ), + ), + ], + ) : Text(_description), ); } else { diff --git a/lib/podcasts/podcastgroup.dart b/lib/podcasts/podcastgroup.dart index 9676a62..d63bfd8 100644 --- a/lib/podcasts/podcastgroup.dart +++ b/lib/podcasts/podcastgroup.dart @@ -165,213 +165,223 @@ class _PodcastCardState extends State { var _groupList = Provider.of(context); _belongGroups = _groupList.getPodcastGroup(widget.podcastLocal.id); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - onTap: () => setState(() => _loadMenu = !_loadMenu), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12), - height: 100, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - child: Icon( - Icons.unfold_more, - color: _c, - ), - ), - Container( - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(30)), - child: Container( - height: 60, - width: 60, - child: Image.file( - File("${widget.podcastLocal.imagePath}")), + return Container( + decoration: BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => setState(() => _loadMenu = !_loadMenu), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12), + height: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: Icon( + Icons.unfold_more, + color: _c, ), ), - ), - Container( - width: _width / 2, - padding: EdgeInsets.symmetric(horizontal: 10), - alignment: Alignment.centerLeft, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - alignment: Alignment.centerLeft, - child: Text( - widget.podcastLocal.title, - maxLines: 2, - overflow: TextOverflow.fade, - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 15), - ), - ), - Row( - children: _belongGroups.map((group) { - return Container( - padding: EdgeInsets.only(right: 5.0), - child: Text(group.name)); - }).toList(), - ), - ], - )), - Spacer(), - Icon(_loadMenu - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down), - Padding( - padding: EdgeInsets.symmetric(horizontal: 5.0), - ), - ]), - ), - ), - !_loadMenu - ? Center() - : Container( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - border: Border( - bottom: BorderSide( - color: Theme.of(context).primaryColorDark), - top: BorderSide( - color: Theme.of(context).primaryColorDark))), - height: 50, - child: _addGroup - ? Row( + Container( + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: Container( + height: 60, + width: 60, + child: Image.file( + File("${widget.podcastLocal.imagePath}")), + ), + ), + ), + Container( + width: _width / 2, + padding: EdgeInsets.symmetric(horizontal: 10), + alignment: Alignment.centerLeft, + child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Expanded( - flex: 4, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: _groupList.groups - .map((PodcastGroup group) { - return Container( - padding: EdgeInsets.only(left: 5.0), - child: FilterChip( - key: ValueKey(group.id), - label: Text(group.name), - selected: _selectedGroups.contains(group), - onSelected: (bool value) { - setState(() { - if (!value) { - _selectedGroups.remove(group); - print(group.name); - } else { - _selectedGroups.add(group); - } - }); - }, - ), - ); - }).toList()), + Container( + alignment: Alignment.centerLeft, + child: Text( + widget.podcastLocal.title, + maxLines: 2, + overflow: TextOverflow.fade, + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 15), ), ), - Expanded( - flex: 1, - child: Row( - children: [ - IconButton( - icon: Icon(Icons.clear), - onPressed: () => setState(() { - _addGroup = false; - }), - ), - IconButton( - onPressed: () async { - print(_selectedGroups); - if (_selectedGroups.length > 0) { - setState(() { - _addGroup = false; - }); - await _groupList.changeGroup( - widget.podcastLocal.id, - _selectedGroups, - ); - Fluttertoast.showToast( - msg: 'Setting Saved', - gravity: ToastGravity.BOTTOM, - ); - } else - Fluttertoast.showToast( - msg: 'At least select one group', - gravity: ToastGravity.BOTTOM, - ); - }, - icon: Icon(Icons.done), - ), - ], + Row( + children: _belongGroups.map((group) { + return Container( + padding: EdgeInsets.only(right: 5.0), + child: Text(group.name)); + }).toList(), + ), + ], + )), + Spacer(), + Icon(_loadMenu + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5.0), + ), + ]), + ), + ), + !_loadMenu + ? Center() + : Container( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + border: Border( + bottom: BorderSide( + color: Theme.of(context).primaryColorDark), + top: BorderSide( + color: Theme.of(context).primaryColorDark))), + height: 50, + child: _addGroup + ? Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + flex: 4, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _groupList.groups + .map((PodcastGroup group) { + return Container( + padding: EdgeInsets.only(left: 5.0), + child: FilterChip( + key: ValueKey(group.id), + label: Text(group.name), + selected: + _selectedGroups.contains(group), + onSelected: (bool value) { + setState(() { + if (!value) { + _selectedGroups.remove(group); + print(group.name); + } else { + _selectedGroups.add(group); + } + }); + }, + ), + ); + }).toList()), + ), ), - ) - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buttonOnMenu( - Icon(Icons.fullscreen), - () => Navigator.push( - context, - ScaleRoute( - page: PodcastDetail( - podcastLocal: widget.podcastLocal, + Expanded( + flex: 1, + child: Row( + children: [ + IconButton( + icon: Icon(Icons.clear), + onPressed: () => setState(() { + _addGroup = false; + }), + ), + IconButton( + onPressed: () async { + print(_selectedGroups); + if (_selectedGroups.length > 0) { + setState(() { + _addGroup = false; + }); + await _groupList.changeGroup( + widget.podcastLocal.id, + _selectedGroups, + ); + Fluttertoast.showToast( + msg: 'Setting Saved', + gravity: ToastGravity.BOTTOM, + ); + } else + Fluttertoast.showToast( + msg: 'At least select one group', + gravity: ToastGravity.BOTTOM, + ); + }, + icon: Icon(Icons.done), + ), + ], + ), + ) + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buttonOnMenu( + Icon(Icons.fullscreen), + () => Navigator.push( + context, + ScaleRoute( + page: PodcastDetail( + podcastLocal: widget.podcastLocal, + )), )), - )), - _buttonOnMenu(Icon(Icons.add), () { - setState(() { - _addGroup = true; - }); - }), - _buttonOnMenu(Icon(Icons.notifications), () {}), - _buttonOnMenu(Icon(Icons.remove_circle), () { - showDialog( - context: context, - child: AnnotatedRegion( - value: SystemUiOverlayStyle( - systemNavigationBarColor: - Colors.black.withOpacity(0.5), - statusBarColor: Colors.red, - ), - child: AlertDialog( - elevation: 2.0, - title: Text('Remove confirm'), - content: Text( - '${widget.podcastLocal.title} will be removed from device.'), - actions: [ - FlatButton( - onPressed: () => - Navigator.of(context).pop(), - child: Text('CANCEL'), - ), - FlatButton( - onPressed: () { - _groupList.removePodcast( - widget.podcastLocal.id); - Navigator.of(context).pop(); - }, - child: Text( - 'CONFIRM', - style: TextStyle(color: Colors.red), + _buttonOnMenu(Icon(Icons.add), () { + setState(() { + _addGroup = true; + }); + }), + _buttonOnMenu(Icon(Icons.notifications), () {}), + _buttonOnMenu(Icon(Icons.remove_circle), () { + showDialog( + context: context, + child: + AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: + Colors.black.withOpacity(0.5), + statusBarColor: Colors.red, + ), + child: AlertDialog( + elevation: 2.0, + title: Text('Remove confirm'), + content: Text( + '${widget.podcastLocal.title} will be removed from device.'), + actions: [ + FlatButton( + onPressed: () => + Navigator.of(context).pop(), + child: Text('CANCEL'), ), - ) - ], - ), - )); - }), - ], - ), + FlatButton( + onPressed: () { + _groupList.removePodcast( + widget.podcastLocal.id); + Navigator.of(context).pop(); + }, + child: Text( + 'CONFIRM', + style: + TextStyle(color: Colors.red), + ), + ) + ], + ), + )); + }), + ], + ), + ), ), - ), - ], + ], + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 1d4be51..8a625e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,13 +92,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.8" - expandable: - dependency: "direct dev" - description: - name: expandable - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.2" file_picker: dependency: "direct dev" description: @@ -385,6 +378,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.11" + tuple: + dependency: "direct dev" + description: + name: tuple + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" typed_data: dependency: transitive description: @@ -449,5 +449,5 @@ packages: source: hosted version: "3.5.0" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.6.0 <3.0.0" flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index bee9e80..090737d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dev_dependencies: image: ^2.1.4 shared_preferences: ^0.5.6+1 uuid: ^2.0.4 - expandable: ^4.1.2 + tuple: ^1.0.3