Search ui changed a lot, add podcast detail panel.

Update audio service to latest version.
This commit is contained in:
stonegate 2020-07-22 17:34:32 +08:00
parent 602cc67342
commit b619be9a9b
49 changed files with 1493 additions and 772 deletions

View File

@ -1,13 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stonegate.tsacdop"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stonegate.tsacdop" xmlns:tools="http://schemas.android.com/tools">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher_icon" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/LaunchTheme" />
@ -22,11 +21,8 @@
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>

View File

@ -14,8 +14,9 @@ import 'package:google_fonts/google_fonts.dart';
import '../state/audio_state.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import 'episode_download.dart';

View File

@ -4,7 +4,7 @@ import 'package:tsacdop/util/custompaint.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:line_icons/line_icons.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
const String version = '0.4.7';

View File

@ -9,17 +9,18 @@ import 'package:audio_service/audio_service.dart';
import 'package:line_icons/line_icons.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../state/audio_state.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../util/pageroute.dart';
import '../util/colorize.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import '../util/custom_slider.dart';
import '../episodes/episode_detail.dart';
import 'playlist.dart';
import 'audiopanel.dart';
import '../util/audiopanel.dart';
final List<BoxShadow> _customShadow = [
BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white),
@ -438,9 +439,9 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Expanded(
flex: 2,
child: Selector<AudioPlayerNotifier,
Tuple3<BasicPlaybackState, double, String>>(
Tuple3<bool, double, String>>(
selector: (_, audio) => Tuple3(
audio.audioState,
audio.buffering,
(audio.backgroundAudioDuration -
audio.backgroundAudioPosition) /
1000,
@ -453,12 +454,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
? Text(data.item3,
style: const TextStyle(
color: const Color(0xFFFF0000)))
: data.item1 == BasicPlaybackState.buffering ||
data.item1 ==
BasicPlaybackState.connecting ||
data.item1 ==
BasicPlaybackState.skippingToNext ||
data.item1 == BasicPlaybackState.stopped
: data.item1
? Text(
s.buffering,
style: TextStyle(
@ -475,35 +471,18 @@ class _PlayerWidgetState extends State<PlayerWidget> {
),
Expanded(
flex: 2,
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.audioState,
builder: (_, audioplay, __) {
child: Selector<AudioPlayerNotifier, Tuple2<bool, bool>>(
selector: (_, audio) =>
Tuple2(audio.buffering, audio.playing),
builder: (_, data, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//Spacer(),
audioplay == BasicPlaybackState.playing
? InkWell(
onTap:
audioplay == BasicPlaybackState.playing
? () {
audio.pauseAduio();
}
: null,
child: ImageRotate(
title: audio.episode?.title,
path: audio.episode?.imagePath),
)
: InkWell(
onTap:
audioplay == BasicPlaybackState.playing
? null
: () {
audio.resumeAudio();
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
data.item1
? Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(
vertical: 10.0),
@ -521,13 +500,48 @@ class _PlayerWidgetState extends State<PlayerWidget> {
shape: BoxShape.circle,
color: Colors.black),
),
Icon(
Icons.play_arrow,
color: Colors.white,
)
],
),
),
])
: data.item2
? InkWell(
onTap: data.item2
? () => audio.pauseAduio()
: null,
child: ImageRotate(
title: audio.episode?.title,
path: audio.episode?.imagePath),
)
: InkWell(
onTap: data.item2
? null
: () => audio.resumeAudio(),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(
vertical: 10.0),
child: Container(
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundImage: FileImage(File(
"${audio.episode.imagePath}")),
)),
),
Container(
height: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black),
),
if (!data.item1)
Icon(
Icons.play_arrow,
color: Colors.white,
)
],
),
),
IconButton(
padding: EdgeInsets.zero,
onPressed: () => audio.playNext(),
@ -1148,7 +1162,7 @@ class _ControlPanelState extends State<ControlPanel>
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
color: Theme.of(context).primaryColor,
color: context.primaryColor,
height: 300,
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Stack(
@ -1159,10 +1173,9 @@ class _ControlPanelState extends State<ControlPanel>
children: <Widget>[
Consumer<AudioPlayerNotifier>(
builder: (_, data, __) {
Color _c =
(Theme.of(context).brightness == Brightness.light)
? data.episode.primaryColor.colorizedark()
: data.episode.primaryColor.colorizeLight();
Color _c = (context.brightness == Brightness.light)
? data.episode.primaryColor.colorizedark()
: data.episode.primaryColor.colorizeLight();
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -1217,15 +1230,16 @@ class _ControlPanelState extends State<ControlPanel>
color: const Color(0xFFFF0000)))
: Text(
data.audioState ==
BasicPlaybackState
AudioProcessingState
.buffering ||
data.audioState ==
BasicPlaybackState
AudioProcessingState
.connecting ||
data.audioState ==
BasicPlaybackState.none ||
AudioProcessingState
.none ||
data.audioState ==
BasicPlaybackState
AudioProcessingState
.skippingToNext
? context.s.buffering
: '',
@ -1250,9 +1264,9 @@ class _ControlPanelState extends State<ControlPanel>
),
Container(
height: 100,
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.audioState,
builder: (_, backplay, __) {
child: Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playing,
builder: (_, playing, __) {
return Material(
color: Colors.transparent,
child: Row(
@ -1261,10 +1275,9 @@ class _ControlPanelState extends State<ControlPanel>
children: [
IconButton(
padding: EdgeInsets.symmetric(horizontal: 25.0),
onPressed:
backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(-10)
: null,
onPressed: playing
? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0,
icon: Icon(Icons.replay_10),
color: Colors.grey[500]),
@ -1285,14 +1298,13 @@ class _ControlPanelState extends State<ControlPanel>
Brightness.dark
? _customShadowNight
: _customShadow),
child: backplay == BasicPlaybackState.playing
child: playing
? Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(30)),
onTap: backplay ==
BasicPlaybackState.playing
onTap: playing
? () {
audio.pauseAduio();
}
@ -1312,8 +1324,7 @@ class _ControlPanelState extends State<ControlPanel>
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(30)),
onTap: backplay ==
BasicPlaybackState.playing
onTap: playing
? null
: () {
audio.resumeAudio();
@ -1333,10 +1344,9 @@ class _ControlPanelState extends State<ControlPanel>
),
IconButton(
padding: EdgeInsets.symmetric(horizontal: 25.0),
onPressed:
backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(30)
: null,
onPressed: playing
? () => audio.forwardAudio(30)
: null,
iconSize: 32.0,
icon: Icon(Icons.forward_30),
color: Colors.grey[500]),

View File

@ -11,12 +11,13 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:feature_discovery/feature_discovery.dart';
import '../state/audio_state.dart';
import '../type/playlist.dart';
import '../type/episodebrief.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../util/episodegrid.dart';
import '../util/mypopupmenu.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import '../state/download_state.dart';
import '../state/podcast_group.dart';

View File

@ -12,6 +12,7 @@ import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../state/podcast_group.dart';
import '../state/download_state.dart';
import '../type/podcastlocal.dart';
@ -19,7 +20,7 @@ import '../state/audio_state.dart';
import '../util/custompaint.dart';
import '../util/pageroute.dart';
import '../util/colorize.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../episodes/episode_detail.dart';

View File

@ -15,7 +15,7 @@ import 'package:intl/intl.dart';
import '../settings/settting.dart';
import '../state/refresh_podcast.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import 'about.dart';
class PopupMenu extends StatefulWidget {

View File

@ -8,7 +8,7 @@ import '../state/podcast_group.dart';
import '../state/download_state.dart';
import '../state/refresh_podcast.dart';
import '../type/episodebrief.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
class Import extends StatelessWidget {
Widget importColumn(String text, BuildContext context) {

View File

@ -10,7 +10,8 @@ import 'package:line_icons/line_icons.dart';
import '../state/audio_state.dart';
import '../type/episodebrief.dart';
import '../util/context_extension.dart';
import '../type/playlist.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import '../util/colorize.dart';

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import '../state/setting_state.dart';
import '../home/home.dart';
import '../util/pageroute.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import 'fourthpage.dart';
import 'secondpage.dart';
import 'thirdpage.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
class FirstPage extends StatefulWidget {
FirstPage({Key key}) : super(key: key);

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
class FourthPage extends StatefulWidget {
FourthPage({Key key}) : super(key: key);

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
class SecondPage extends StatefulWidget {
SecondPage({Key key}) : super(key: key);

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
class ThirdPage extends StatefulWidget {
ThirdPage({Key key}) : super(key: key);

View File

@ -25,13 +25,14 @@ const String downloadLayoutKey = 'downloadLayoutKey';
const String autoDownloadNetworkKey = 'autoDownloadNetwork';
const String episodePopupMenuKey = 'episodePopupMenuKey';
const String autoDeleteKey = 'autoDeleteKey';
//SleepTImer
const String autoSleepTimerKey = 'autoSleepTimerKey';
const String autoSleepTimerStartKey = 'autoSleepTimerStartKey';
const String autoSleepTimerEndKey = 'autoSleepTimerEndKey';
const String defaultSleepTimerKey = 'defaultSleepTimerKey';
const String autoSleepTimerModeKey = 'autoSleepTimerModeKey';
const String tapToOpenPopupMenuKey = 'tapToOpenPopupMenuKey';
const String fastForwardSecondsKey = 'fastForwardSecondsKey';
const String rewindSecondsKey = 'rewindSecondsKey';
class KeyValueStorage {
final String key;

View File

@ -5,7 +5,7 @@ import 'package:intl/intl.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:dio/dio.dart';
import '../type/podcastlocal.dart';
import '../state/audio_state.dart';
import '../type/play_histroy.dart';
import '../type/episodebrief.dart';
import '../webfeed/webfeed.dart';
import '../type/sub_history.dart';

View File

@ -16,13 +16,14 @@ import 'package:cached_network_image/cached_network_image.dart';
import '../type/podcastlocal.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../util/episodegrid.dart';
import '../home/audioplayer.dart';
import '../type/fireside_data.dart';
import '../util/colorize.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import '../util/general_dialog.dart';
import '../state/audio_state.dart';

View File

@ -13,7 +13,7 @@ import '../type/podcastlocal.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../util/colorize.dart';
import '../util/duraiton_picker.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/general_dialog.dart';
class PodcastGroupList extends StatefulWidget {

View File

@ -12,7 +12,7 @@ import '../state/podcast_group.dart';
import 'podcast_group.dart';
import 'podcastlist.dart';
import '../util/pageroute.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/general_dialog.dart';
import 'custom_tabview.dart';

View File

@ -13,7 +13,7 @@ import '../type/podcastlocal.dart';
import '../local_storage/sqflite_localpodcast.dart';
import 'podcast_detail.dart';
import '../util/pageroute.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
class AboutPodcast extends StatefulWidget {
final PodcastLocal podcastLocal;

View File

@ -0,0 +1,39 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import '../type/searchpodcast.dart';
import '../type/searchepisodes.dart';
import '../.env.dart';
class SearchEngine {
searchPodcasts({String searchText, int nextOffset}) async {
String apiKey = environment['apiKey'];
String url = "https://listen-api.listennotes.com/api/v2/search?q=" +
Uri.encodeComponent(searchText) +
"&sort_by_date=0&type=podcast&offset=$nextOffset";
Response response = await Dio().get(url,
options: Options(headers: {
'X-ListenAPI-Key': "$apiKey",
'Accept': "application/json"
}));
Map searchResultMap = jsonDecode(response.toString());
var searchResult = SearchPodcast.fromJson(searchResultMap);
return searchResult;
}
Future<SearchEpisodes<dynamic>> fetchEpisode(
{String id, int nextEpisodeDate}) async {
String apiKey = environment['apiKey'];
String url =
"https://listen-api.listennotes.com/api/v2/podcasts/$id?next_episode_pub_date=$nextEpisodeDate";
Response response = await Dio().get(url,
options: Options(headers: {
'X-ListenAPI-Key': "$apiKey",
'Accept': "application/json"
}));
Map searchResultMap = jsonDecode(response.toString());
var searchResult = SearchEpisodes.fromJson(searchResultMap);
return searchResult;
}
}

View File

@ -16,7 +16,7 @@ import 'package:wc_flutter_share/wc_flutter_share.dart';
import '../state/podcast_group.dart';
import '../state/setting_state.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../service/ompl_build.dart';
class DataBackup extends StatefulWidget {

View File

@ -9,7 +9,7 @@ import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../type/episodebrief.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../state/download_state.dart';
import '../local_storage/sqflite_localpodcast.dart';

View File

@ -11,8 +11,8 @@ import 'package:tsacdop/state/podcast_group.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../webfeed/webfeed.dart';
import '../type/searchpodcast.dart';
import '../util/context_extension.dart';
import '../state/audio_state.dart';
import '../util/extension_helper.dart';
import '../type/play_histroy.dart';
import '../state/podcast_group.dart';
import '../type/sub_history.dart';

View File

@ -4,7 +4,7 @@ import 'package:intl/intl.dart';
import 'package:line_icons/line_icons.dart';
import 'package:url_launcher/url_launcher.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../generated/l10n.dart';
class LanguagesSetting extends StatefulWidget {

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/episodegrid.dart';
import '../util/custompaint.dart';
import '../local_storage/key_value_storage.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import 'licenses.dart';
class Libries extends StatelessWidget {

View File

@ -11,7 +11,7 @@ import 'package:flutter_time_picker_spinner/flutter_time_picker_spinner.dart';
import '../state/setting_state.dart';
import '../home/audioplayer.dart';
import '../util/general_dialog.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custom_dropdown.dart';
String stringForMins(int mins) {

View File

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:line_icons/line_icons.dart';
import 'package:flare_flutter/flare_actor.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import '../local_storage/key_value_storage.dart';

View File

@ -7,7 +7,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:fluttertoast/fluttertoast.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../intro_slider/app_intro.dart';
import '../home/home.dart';
import '../podcasts/podcast_manage.dart';

View File

@ -6,7 +6,7 @@ import 'package:google_fonts/google_fonts.dart';
import '../settings/downloads_manage.dart';
import '../state/setting_state.dart';
import '../local_storage/key_value_storage.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custom_dropdown.dart';
class StorageSetting extends StatefulWidget {

View File

@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import '../state/setting_state.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/custom_dropdown.dart';
class SyncingSetting extends StatelessWidget {

View File

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../state/setting_state.dart';
import '../util/context_extension.dart';
import '../util/extension_helper.dart';
import '../util/general_dialog.dart';
class ThemeSetting extends StatelessWidget {

View File

@ -1,19 +1,15 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:path/path.dart';
import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../type/playlist.dart';
import '../local_storage/key_value_storage.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../.env.dart';
MediaControl playControl = MediaControl(
androidIcon: 'drawable/ic_stat_play_circle_filled',
@ -50,144 +46,113 @@ void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class PlayHistory {
DBHelper dbHelper = DBHelper();
String title;
String url;
double seconds;
double seekValue;
DateTime playdate;
PlayHistory(this.title, this.url, this.seconds, this.seekValue,
{this.playdate});
EpisodeBrief _episode;
EpisodeBrief get episode => _episode;
getEpisode() async {
_episode = await dbHelper.getRssItemWithUrl(url);
}
}
class Playlist {
String name;
DBHelper dbHelper = DBHelper();
List<EpisodeBrief> _playlist;
List<EpisodeBrief> get playlist => _playlist;
KeyValueStorage storage = KeyValueStorage('playlist');
getPlaylist() async {
List<String> urls = await storage.getStringList();
if (urls.length == 0) {
_playlist = [];
} else {
_playlist = [];
for (String url in urls) {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
if (episode != null) _playlist.add(episode);
}
}
}
savePlaylist() async {
List<String> urls = [];
urls.addAll(_playlist.map((e) => e.enclosureUrl));
await storage.saveStringList(urls.toSet().toList());
}
addToPlayList(EpisodeBrief episodeBrief) async {
if (!_playlist.contains(episodeBrief)) {
_playlist.add(episodeBrief);
await savePlaylist();
dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl);
}
}
addToPlayListAt(EpisodeBrief episodeBrief, int index) async {
if (!_playlist.contains(episodeBrief)) {
_playlist.insert(index, episodeBrief);
await savePlaylist();
dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl);
}
}
Future<int> delFromPlaylist(EpisodeBrief episodeBrief) async {
int index = _playlist.indexOf(episodeBrief);
_playlist.removeWhere(
(episode) => episode.enclosureUrl == episodeBrief.enclosureUrl);
print('delete' + episodeBrief.title);
await savePlaylist();
return index;
}
}
/// Sleep timer mode.
enum SleepTimerMode { endOfEpisode, timer, undefined }
enum ShareStatus { generate, download, complete, undefined, error }
//enum ShareStatus { generate, download, complete, undefined, error }
class AudioPlayerNotifier extends ChangeNotifier {
DBHelper dbHelper = DBHelper();
KeyValueStorage positionStorage = KeyValueStorage(audioPositionKey);
KeyValueStorage autoPlayStorage = KeyValueStorage(autoPlayKey);
KeyValueStorage autoSleepTimerStorage = KeyValueStorage(autoSleepTimerKey);
KeyValueStorage defaultSleepTimerStorage =
KeyValueStorage(defaultSleepTimerKey);
KeyValueStorage autoSleepTimerModeStorage =
KeyValueStorage(autoSleepTimerModeKey);
KeyValueStorage autoSleepTimerStartStorage =
KeyValueStorage(autoSleepTimerStartKey);
KeyValueStorage autoSleepTimerEndStorage =
KeyValueStorage(autoSleepTimerEndKey);
var positionStorage = KeyValueStorage(audioPositionKey);
var autoPlayStorage = KeyValueStorage(autoPlayKey);
var autoSleepTimerStorage = KeyValueStorage(autoSleepTimerKey);
var defaultSleepTimerStorage = KeyValueStorage(defaultSleepTimerKey);
var autoSleepTimerModeStorage = KeyValueStorage(autoSleepTimerModeKey);
var autoSleepTimerStartStorage = KeyValueStorage(autoSleepTimerStartKey);
var autoSleepTimerEndStorage = KeyValueStorage(autoSleepTimerEndKey);
var fastForwardSecondsStorage = KeyValueStorage(fastForwardSecondsKey);
var rewindSecondsStorage = KeyValueStorage(rewindSecondsKey);
/// Current playing episdoe.
EpisodeBrief _episode;
/// Current playlist.
Playlist _queue = Playlist();
/// Notifier for playlist change.
bool _queueUpdate = false;
BasicPlaybackState _audioState = BasicPlaybackState.none;
bool _playerRunning = false;
/// Player state.
AudioProcessingState _audioState = AudioProcessingState.none;
/// Player playing.
bool _playing = false;
/// Fastforward second.
int _fastForwardSeconds;
/// Rewind seconds.
int _rewindSeconds;
/// No slide, set true if slide on seekbar.
bool _noSlide = true;
/// Current episode duration.
int _backgroundAudioDuration = 0;
/// Current episode positin.
int _backgroundAudioPosition = 0;
/// Erroe maeesage.
String _remoteErrorMessage;
/// Seekbar value, min 0, max 1.0.
double _seekSliderValue = 0.0;
/// Record plyaer position.
int _lastPostion = 0;
/// Set true if sleep timer mode is end of episode.
bool _stopOnComplete = false;
/// Sleep timer timer.
Timer _stopTimer;
/// Sleep timer time left.
int _timeLeft = 0;
/// Start sleep timer.
bool _startSleepTimer = false;
/// Control sleep timer anamation.
double _switchValue = 0;
/// Sleep timer mode.
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
//Auto stop at the end of episode when you start play at scheduled time.
bool _autoSleepTimer;
//Default sleep timer time.
ShareStatus _shareStatus = ShareStatus.undefined;
String _shareFile = '';
//set autoplay episode in playlist
bool _autoPlay;
/// Datetime now.
DateTime _current;
/// Current position.
int _currentPosition;
/// Current speed.
double _currentSpeed = 1;
BehaviorSubject<List<MediaItem>> queueSubject;
//Update episode card when setting changed
bool _episodeState = false;
BasicPlaybackState get audioState => _audioState;
AudioProcessingState get audioState => _audioState;
int get backgroundAudioDuration => _backgroundAudioDuration;
int get backgroundAudioPosition => _backgroundAudioPosition;
double get seekSliderValue => _seekSliderValue;
String get remoteErrorMessage => _remoteErrorMessage;
bool get playerRunning => _playerRunning;
bool get playerRunning => _audioState != AudioProcessingState.none;
bool get buffering => _audioState != AudioProcessingState.ready;
int get lastPositin => _lastPostion;
Playlist get queue => _queue;
bool get playing => _playing;
bool get queueUpdate => _queueUpdate;
EpisodeBrief get episode => _episode;
bool get stopOnComplete => _stopOnComplete;
bool get startSleepTimer => _startSleepTimer;
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
ShareStatus get shareStatus => _shareStatus;
String get shareFile => _shareFile;
//bool get autoPlay => _autoPlay;
int get timeLeft => _timeLeft;
double get switchValue => _switchValue;
double get currentSpeed => _currentSpeed;
@ -199,11 +164,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
}
set setShareStatue(ShareStatus status) {
_shareStatus = status;
notifyListeners();
}
set setEpisodeState(bool boo) {
_episodeState = !_episodeState;
notifyListeners();
@ -234,11 +194,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (running) {}
}
loadPlaylist() async {
Future<void> loadPlaylist() async {
await _queue.getPlaylist();
await _getAutoPlay();
// await _getAutoAdd();
// await addNewEpisode('all');
_lastPostion = await positionStorage.getInt();
if (_lastPostion > 0 && _queue.playlist.length > 0) {
final EpisodeBrief episode = _queue.playlist.first;
@ -252,12 +210,13 @@ class AudioPlayerNotifier extends ChangeNotifier {
await lastWorkStorage.saveInt(0);
}
episodeLoad(EpisodeBrief episode, {int startPosition = 0}) async {
Future<void> episodeLoad(EpisodeBrief episode,
{int startPosition = 0}) async {
print(episode.enclosureUrl);
final EpisodeBrief episodeNew =
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
//TODO load episode from last position when player running
if (_playerRunning) {
if (playerRunning) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history);
@ -275,8 +234,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = episodeNew;
_playerRunning = true;
_audioState = BasicPlaybackState.connecting;
_audioState = AudioProcessingState.connecting;
notifyListeners();
//await _queue.savePlaylist();
_startAudioService(startPosition, episodeNew.enclosureUrl);
@ -289,18 +247,29 @@ class AudioPlayerNotifier extends ChangeNotifier {
_startAudioService(int position, String url) async {
_stopOnComplete = false;
_sleepTimerMode = SleepTimerMode.undefined;
/// Connect to audio service.
if (!AudioService.connected) {
await AudioService.connect();
}
/// Get fastword and rewind seconds.
_fastForwardSeconds =
await fastForwardSecondsStorage.getInt(defaultValue: 30);
_rewindSeconds = await rewindSecondsStorage.getInt(defaultValue: 10);
/// Start audio service.
await AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF4d91be,
androidNotificationColor: 0xFF4d91be,
androidNotificationIcon: 'drawable/ic_notification',
enableQueue: true,
androidStopOnRemoveTask: true,
androidStopForegroundOnPause: true);
//Check autoplay setting
androidEnableQueue: true,
androidStopForegroundOnPause: true,
fastForwardInterval: Duration(seconds: _fastForwardSeconds),
rewindInterval: Duration(seconds: _rewindSeconds));
//Check autoplay setting, if true only add one episode, else add playlist.
await _getAutoPlay();
if (_autoPlay) {
for (var episode in _queue.playlist)
@ -315,7 +284,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
await autoSleepTimerStartStorage.getInt(defaultValue: 1380);
int endTime = await autoSleepTimerEndStorage.getInt(defaultValue: 360);
int currentTime = DateTime.now().hour * 60 + DateTime.now().minute;
print('CurrentTime' + currentTime.toString());
if ((startTime > endTime &&
(currentTime > startTime || currentTime < endTime)) ||
((startTime < endTime) &&
@ -327,78 +295,106 @@ class AudioPlayerNotifier extends ChangeNotifier {
sleepTimer(defaultTimer);
}
}
_playerRunning = true;
await AudioService.play();
AudioService.currentMediaItemStream
.where((event) => event != null)
.listen((item) async {
EpisodeBrief episode = await dbHelper.getRssItemWithMediaId(item.id);
_backgroundAudioDuration = item.duration?.inMilliseconds ?? 0;
if (episode != null) {
_episode = episode;
_backgroundAudioDuration = item?.duration ?? 0;
_backgroundAudioDuration = item.duration.inMilliseconds ?? 0;
if (position > 0 &&
_backgroundAudioDuration > 0 &&
_episode.enclosureUrl == url) {
AudioService.seekTo(position);
AudioService.seekTo(Duration(milliseconds: position));
position = 0;
}
notifyListeners();
} else {
_queue.playlist.removeAt(0);
// _queue.playlist.removeAt(0);
AudioService.skipToNext();
}
});
queueSubject = BehaviorSubject<List<MediaItem>>();
queueSubject.addStream(
AudioService.queueStream.distinct().where((event) => event != null));
queueSubject.stream.listen((event) {
if (event.length == _queue.playlist.length - 1 &&
_audioState == BasicPlaybackState.skippingToNext) {
if (event.length == 0 || _stopOnComplete) {
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
notifyListeners();
positionStorage.saveInt(_lastPostion);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
} else if (event.first.id != _episode.mediaId) {
_lastPostion = 0;
notifyListeners();
positionStorage.saveInt(_lastPostion);
_queue.delFromPlaylist(_episode);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
}
// queueSubject = BehaviorSubject<List<MediaItem>>();
// queueSubject.addStream(
// AudioService.queueStream.distinct().where((event) => event != null));
//queueSubject.stream.
AudioService.customEventStream.distinct().listen((event) async {
if (event is String && _episode.title == event) {
print(event);
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
notifyListeners();
await positionStorage.saveInt(_lastPostion);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
}
});
AudioService.playbackStateStream.listen((event) async {
// AudioService.queueStream
// .distinct()
// .where((event) => event != null)
// .listen((event) {
// if (event.length == _queue.playlist.length - 1 &&
// _audioState == AudioProcessingState.skippingToNext) {
// if (event.length == 0 || _stopOnComplete) {
// _queue.delFromPlaylist(_episode);
// _lastPostion = 0;
// notifyListeners();
// positionStorage.saveInt(_lastPostion);
// final PlayHistory history = PlayHistory(
// _episode.title,
// _episode.enclosureUrl,
// backgroundAudioPosition / 1000,
// seekSliderValue);
// dbHelper.saveHistory(history);
// } else if (event.first.id != _episode.mediaId) {
// _lastPostion = 0;
// notifyListeners();
// positionStorage.saveInt(_lastPostion);
// _queue.delFromPlaylist(_episode);
// final PlayHistory history = PlayHistory(
// _episode.title,
// _episode.enclosureUrl,
// backgroundAudioPosition / 1000,
// seekSliderValue);
// dbHelper.saveHistory(history);
// }
// }
// });
AudioService.playbackStateStream
.distinct()
.where((event) => event != null)
.listen((event) async {
_current = DateTime.now();
_audioState = event?.basicState;
if (_audioState == BasicPlaybackState.stopped) {
_playerRunning = false;
_audioState = event?.processingState;
_playing = event?.playing;
_currentSpeed = event.speed;
_currentPosition = event.currentPosition.inMilliseconds ?? 0;
if (_audioState == AudioProcessingState.stopped) {
if (_switchValue > 0) _switchValue = 0;
}
if (_audioState == BasicPlaybackState.error) {
/// Get error state.
if (_audioState == AudioProcessingState.error) {
_remoteErrorMessage = 'Network Error';
}
if (_audioState != BasicPlaybackState.error &&
_audioState != BasicPlaybackState.paused) {
/// Reset error state.
if (_audioState != AudioProcessingState.error && _playing) {
_remoteErrorMessage = null;
}
_currentPosition = event?.currentPosition ?? 0;
notifyListeners();
});
@ -407,7 +403,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
Timer.periodic(Duration(milliseconds: 500), (timer) {
double s = _currentSpeed ?? 1.0;
if (_noSlide) {
if (_audioState == BasicPlaybackState.playing) {
if (_playing) {
getPosition = _currentPosition +
((DateTime.now().difference(_current).inMilliseconds) * s)
.toInt();
@ -432,7 +428,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
notifyListeners();
}
if (_audioState == BasicPlaybackState.stopped) {
if (_audioState == AudioProcessingState.stopped) {
timer.cancel();
}
});
@ -444,9 +440,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = _queue.playlist.first;
_playerRunning = true;
_audioState = BasicPlaybackState.connecting;
_queueUpdate = !_queueUpdate;
_audioState = AudioProcessingState.connecting;
notifyListeners();
_startAudioService(_lastPostion ?? 0, _queue.playlist.first.enclosureUrl);
}
@ -457,7 +452,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
addToPlaylist(EpisodeBrief episode) async {
if (!_queue.playlist.contains(episode)) {
if (_playerRunning) {
if (playerRunning) {
await AudioService.addQueueItem(episode.toMediaItem());
}
await _queue.addToPlayList(episode);
@ -466,7 +461,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
addToPlaylistAt(EpisodeBrief episode, int index) async {
if (_playerRunning) {
if (playerRunning) {
await AudioService.addQueueItemAt(episode.toMediaItem(), index);
}
await _queue.addToPlayListAt(episode, index);
@ -500,7 +495,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
Future<int> delFromPlaylist(EpisodeBrief episode) async {
if (_playerRunning) {
if (playerRunning) {
await AudioService.removeQueueItem(episode.toMediaItem());
}
int index = await _queue.delFromPlaylist(episode);
@ -511,7 +506,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
moveToTop(EpisodeBrief episode) async {
await delFromPlaylist(episode);
if (_playerRunning) {
if (playerRunning) {
await addToPlaylistAt(episode, 1);
} else {
await addToPlaylistAt(episode, 0);
@ -526,29 +521,29 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
resumeAudio() async {
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none) AudioService.play();
if (_audioState != AudioProcessingState.connecting &&
_audioState != AudioProcessingState.none) AudioService.play();
}
forwardAudio(int s) {
int pos = _backgroundAudioPosition + s * 1000;
AudioService.seekTo(pos);
AudioService.seekTo(Duration(milliseconds: pos));
}
seekTo(int position) async {
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none)
await AudioService.seekTo(position);
if (_audioState != AudioProcessingState.connecting &&
_audioState != AudioProcessingState.none)
await AudioService.seekTo(Duration(milliseconds: position));
}
sliderSeek(double val) async {
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none) {
if (_audioState != AudioProcessingState.connecting &&
_audioState != AudioProcessingState.none) {
_noSlide = false;
_seekSliderValue = val;
notifyListeners();
_currentPosition = (val * _backgroundAudioDuration).toInt();
await AudioService.seekTo(_currentPosition);
await AudioService.seekTo(Duration(milliseconds: _currentPosition));
_noSlide = true;
}
}
@ -579,9 +574,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
_stopOnComplete = false;
_startSleepTimer = false;
_switchValue = 0;
_playerRunning = false;
notifyListeners();
//_playerRunning = false;
AudioService.stop();
notifyListeners();
AudioService.disconnect();
});
} else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) {
@ -610,51 +605,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
}
shareClip(int start, int duration) async {
_shareStatus = ShareStatus.generate;
notifyListeners();
int length = math.min(duration, (_backgroundAudioDuration ~/ 1000 - start));
final BaseOptions options = BaseOptions(
connectTimeout: 60000,
receiveTimeout: 120000,
);
String imageUrl = await dbHelper.getImageUrl(_episode.enclosureUrl);
String url = "https://podcastapi.stonegate.me/clip?" +
"audio_link=${_episode.enclosureUrl}&image_link=$imageUrl&title=${_episode.feedTitle}" +
"&text=${_episode.title}&start=$start&length=$length";
String shareKey = environment['shareKey'];
try {
Response response = await Dio(options).get(url,
options: Options(headers: {
'X-Share-Key': "$shareKey",
}));
String shareLink = response.data;
print(shareLink);
String fileName = _episode.title + start.toString() + '.mp4';
_shareStatus = ShareStatus.download;
notifyListeners();
Directory dir = await getTemporaryDirectory();
String shareDir = join(dir.path, 'share', fileName);
try {
await Dio().download(shareLink, shareDir);
_shareFile = shareDir;
_shareStatus = ShareStatus.complete;
notifyListeners();
} on DioError catch (e) {
print(e);
_shareStatus = ShareStatus.error;
notifyListeners();
}
} catch (e) {
print(e);
_shareStatus = ShareStatus.error;
notifyListeners();
}
}
@override
void dispose() async {
await AudioService.stop();
// await AudioService.stop();
await AudioService.disconnect();
//_playerRunning = false;
super.dispose();
@ -666,66 +619,57 @@ class AudioPlayerTask extends BackgroundAudioTask {
List<MediaItem> _queue = [];
AudioPlayer _audioPlayer = AudioPlayer();
Completer _completer = Completer();
BasicPlaybackState _skipState;
bool _lostFocus;
AudioProcessingState _skipState;
bool _playing;
bool _interrupted = false;
bool _stopAtEnd;
int _cacheMax;
bool get hasNext => _queue.length > 0;
MediaItem get mediaItem => _queue.length > 0 ? _queue.first : null;
BasicPlaybackState _stateToBasicState(AudioPlaybackState state) {
switch (state) {
case AudioPlaybackState.none:
return BasicPlaybackState.none;
case AudioPlaybackState.stopped:
return _skipState ?? BasicPlaybackState.stopped;
case AudioPlaybackState.paused:
return BasicPlaybackState.paused;
case AudioPlaybackState.playing:
return BasicPlaybackState.playing;
case AudioPlaybackState.connecting:
return _skipState ?? BasicPlaybackState.connecting;
case AudioPlaybackState.completed:
return BasicPlaybackState.stopped;
default:
throw Exception("Illegal state");
}
}
StreamSubscription<AudioPlaybackState> _playerStateSubscription;
StreamSubscription<AudioPlaybackEvent> _eventSubscription;
@override
Future<void> onStart() async {
Future<void> onStart(Map<String, dynamic> params) async {
_stopAtEnd = false;
_lostFocus = false;
var playerStateSubscription = _audioPlayer.playbackStateStream
_playerStateSubscription = _audioPlayer.playbackStateStream
.where((state) => state == AudioPlaybackState.completed)
.listen((state) {
_handlePlaybackCompleted();
});
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
_eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
if (event.playbackError != null) {
_setState(state: _skipState ?? BasicPlaybackState.error);
_playing = false;
_setState(processingState: _skipState ?? AudioProcessingState.error);
}
BasicPlaybackState state;
if (event.buffering) {
state = _skipState ?? BasicPlaybackState.buffering;
} else {
state = _stateToBasicState(event.state);
}
if (state != BasicPlaybackState.stopped) {
_setState(
state: state,
position: event.position.inMilliseconds,
speed: event.speed,
);
final bufferingState =
event.buffering ? AudioProcessingState.buffering : null;
switch (event.state) {
case AudioPlaybackState.paused:
_setState(
processingState: bufferingState ?? AudioProcessingState.ready,
position: event.position,
);
break;
case AudioPlaybackState.playing:
_setState(
processingState: bufferingState ?? AudioProcessingState.ready,
position: event.position,
);
break;
case AudioPlaybackState.connecting:
_setState(
processingState: _skipState ?? AudioProcessingState.connecting,
position: event.position,
);
break;
default:
break;
}
});
await _completer.future;
playerStateSubscription.cancel();
eventSubscription.cancel();
}
void _handlePlaybackCompleted() async {
@ -740,7 +684,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
void playPause() {
if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing)
if (AudioServiceBackground.state.playing)
onPause();
else
onPlay();
@ -748,24 +692,27 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onSkipToNext() async {
_skipState = BasicPlaybackState.skippingToNext;
_skipState = AudioProcessingState.skippingToNext;
_playing = false;
await _audioPlayer.stop();
if (_queue.length > 0) _queue.removeAt(0);
if (_queue.length > 0) {
AudioServiceBackground.sendCustomEvent(_queue.first.title);
_queue.removeAt(0);
}
await AudioServiceBackground.setQueue(_queue);
// }
if (_queue.length == 0 || _stopAtEnd) {
// await Future.delayed(Duration(milliseconds: 300));
_skipState = null;
onStop();
} else {
await AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id, _cacheMax);
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
print(mediaItem.title);
Duration duration = await _audioPlayer.durationFuture;
if (duration != null)
await AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
mediaItem.copyWith(duration: duration));
_skipState = null;
// Resume playback if we were playing
// if (_playing) {
@ -784,35 +731,22 @@ class AudioPlayerTask extends BackgroundAudioTask {
_playing = true;
_cacheMax = await cacheStorage.getInt(
defaultValue: (200 * 1024 * 1024).toInt());
// await AudioServiceBackground.setQueue(_queue);
if (_cacheMax == 0) {
await cacheStorage.saveInt((200 * 1024 * 1024).toInt());
_cacheMax = 200 * 1024 * 1024;
}
await _audioPlayer.setUrl(mediaItem.id, _cacheMax);
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
var duration = await _audioPlayer.durationFuture;
if (duration != null)
await AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
mediaItem.copyWith(duration: duration));
playFromStart();
}
// if (mediaItem.extras['skip'] > 0) {
// await _audioPlayer.setClip(
// start: Duration(seconds: 60));
// print(mediaItem.extras['skip']);
// print('set clip success');
// }
else {
} else {
_playing = true;
if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting ||
_audioPlayer.playbackEvent.state != AudioPlaybackState.none)
_audioPlayer.play();
}
// if (mediaItem.extras['skip'] >
// _audioPlayer.playbackEvent.position.inSeconds ??
// 0) {
// _audioPlayer.seek(Duration(seconds: mediaItem.extras['skip']));
// }
}
}
@ -823,7 +757,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
try {
_audioPlayer.play();
} catch (e) {
_setState(state: BasicPlaybackState.error);
_setState(processingState: AudioProcessingState.error);
}
if (mediaItem.extras['skip'] > 0) {
_audioPlayer.seek(Duration(seconds: mediaItem.extras['skip']));
@ -834,8 +768,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
void onPause() {
if (_skipState == null) {
if (_playing == null) {
} else if (_audioPlayer.playbackEvent.state ==
AudioPlaybackState.playing) {
} else if (_playing) {
_playing = false;
_audioPlayer.pause();
}
@ -843,10 +776,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
@override
void onSeekTo(int position) {
void onSeekTo(Duration position) {
if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting ||
_audioPlayer.playbackEvent.state != AudioPlaybackState.none)
_audioPlayer.seek(Duration(milliseconds: position));
_audioPlayer.seek(position);
}
@override
@ -854,20 +787,26 @@ class AudioPlayerTask extends BackgroundAudioTask {
if (button == MediaButton.media)
playPause();
else if (button == MediaButton.next)
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position + 30 * 1000));
else if (button == MediaButton.previous)
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position - 10 * 1000));
_seekRelative(fastForwardInterval);
else if (button == MediaButton.previous) _seekRelative(-rewindInterval);
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _audioPlayer.playbackEvent.position + offset;
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
await _audioPlayer.seek(newPosition);
}
@override
void onStop() async {
Future<void> onStop() async {
await _audioPlayer.stop();
await _audioPlayer.dispose();
_setState(state: BasicPlaybackState.stopped);
await Future.delayed(Duration(milliseconds: 300));
_completer?.complete();
_playing = false;
_playerStateSubscription.cancel();
_eventSubscription.cancel();
await _setState(processingState: AudioProcessingState.none);
await super.onStop();
}
@override
@ -890,10 +829,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
_queue.insert(0, mediaItem);
await AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id, _cacheMax);
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
mediaItem.copyWith(duration: duration));
playFromStart();
//onPlay();
} else {
@ -904,26 +843,26 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
void onFastForward() {
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position + 30 * 1000));
_seekRelative(fastForwardInterval);
}
@override
void onRewind() {
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position - 10 * 1000));
_seekRelative(rewindInterval);
}
@override
void onAudioFocusLost() {
if (_skipState == null) {
if (_playing == null) {
} else if (_audioPlayer.playbackEvent.state ==
AudioPlaybackState.playing) {
_playing = false;
_lostFocus = true;
_audioPlayer.pause();
}
void onAudioFocusLost(AudioInterruption interruption) {
if (_playing) _interrupted = true;
switch (interruption) {
case AudioInterruption.pause:
case AudioInterruption.temporaryPause:
case AudioInterruption.unknownPause:
onPause();
break;
case AudioInterruption.temporaryDuck:
_audioPlayer.setVolume(0.5);
break;
}
}
@ -940,14 +879,18 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
@override
void onAudioFocusGained() {
if (_skipState == null) {
if (_lostFocus) {
_lostFocus = false;
_playing = true;
_audioPlayer.play();
}
void onAudioFocusGained(AudioInterruption interruption) {
switch (interruption) {
case AudioInterruption.temporaryPause:
if (!_playing && _interrupted) onPlay();
break;
case AudioInterruption.temporaryDuck:
_audioPlayer.setVolume(1.0);
break;
default:
break;
}
_interrupted = false;
}
@override
@ -965,24 +908,27 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
}
void _setState(
{@required BasicPlaybackState state, int position, double speed}) {
Future<void> _setState({
AudioProcessingState processingState,
Duration position,
Duration bufferedPosition,
}) async {
if (position == null) {
position = _audioPlayer.playbackEvent.position.inMilliseconds;
position = _audioPlayer.playbackEvent.position;
}
if (speed == null) {
speed = _audioPlayer.playbackEvent.speed;
}
AudioServiceBackground.setState(
controls: getControls(state),
await AudioServiceBackground.setState(
controls: getControls(),
systemActions: [MediaAction.seekTo],
basicState: state,
processingState:
processingState ?? AudioServiceBackground.state.processingState,
playing: _playing,
position: position,
speed: speed,
bufferedPosition: bufferedPosition ?? position,
speed: _audioPlayer.speed,
);
}
List<MediaControl> getControls(BasicPlaybackState state) {
List<MediaControl> getControls() {
if (_playing) {
return [pauseControl, forward30, skipToNextControl, stopControl];
} else {

View File

@ -62,7 +62,7 @@ class EpisodeBrief {
title: title,
artist: feedTitle,
album: feedTitle,
// duration: 0,
duration: Duration.zero,
artUri: 'file://$imagePath',
extras: {'skip': skipSeconds});
}

View File

@ -0,0 +1,19 @@
import '../local_storage/sqflite_localpodcast.dart';
import 'episodebrief.dart';
class PlayHistory {
DBHelper dbHelper = DBHelper();
String title;
String url;
double seconds;
double seekValue;
DateTime playdate;
PlayHistory(this.title, this.url, this.seconds, this.seekValue,
{this.playdate});
EpisodeBrief _episode;
EpisodeBrief get episode => _episode;
getEpisode() async {
_episode = await dbHelper.getRssItemWithUrl(url);
}
}

58
lib/type/playlist.dart Normal file
View File

@ -0,0 +1,58 @@
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import 'episodebrief.dart';
class Playlist {
String name;
DBHelper dbHelper = DBHelper();
List<EpisodeBrief> _playlist;
List<EpisodeBrief> get playlist => _playlist;
KeyValueStorage storage = KeyValueStorage('playlist');
getPlaylist() async {
List<String> urls = await storage.getStringList();
if (urls.length == 0) {
_playlist = [];
} else {
_playlist = [];
for (String url in urls) {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
if (episode != null) _playlist.add(episode);
}
}
}
savePlaylist() async {
List<String> urls = [];
urls.addAll(_playlist.map((e) => e.enclosureUrl));
await storage.saveStringList(urls.toSet().toList());
}
addToPlayList(EpisodeBrief episodeBrief) async {
if (!_playlist.contains(episodeBrief)) {
_playlist.add(episodeBrief);
await savePlaylist();
dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl);
}
}
addToPlayListAt(EpisodeBrief episodeBrief, int index) async {
if (!_playlist.contains(episodeBrief)) {
_playlist.insert(index, episodeBrief);
await savePlaylist();
dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl);
}
}
Future<int> delFromPlaylist(EpisodeBrief episodeBrief) async {
int index = _playlist.indexOf(episodeBrief);
_playlist.removeWhere(
(episode) => episode.enclosureUrl == episodeBrief.enclosureUrl);
print('delete' + episodeBrief.title);
await savePlaylist();
return index;
}
}

View File

@ -0,0 +1,41 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:tsacdop/type/searchpodcast.dart';
part 'searchepisodes.g.dart';
@JsonSerializable()
class SearchEpisodes<E> {
@_ConvertE()
final List<E> episodes;
@JsonKey(name: 'next_episode_pub_date')
final int nextEpisodeDate;
SearchEpisodes({this.episodes, this.nextEpisodeDate});
factory SearchEpisodes.fromJson(Map<String, dynamic> json) =>
_$SearchEpisodesFromJson<E>(json);
Map<String, dynamic> toJson() => _$SearchEpisodesToJson(this);
}
class _ConvertE<E> implements JsonConverter<E, Object> {
const _ConvertE();
@override
E fromJson(Object json) {
return OnlineEpisode.fromJson(json) as E;
}
@override
Object toJson(E object) {
return object;
}
}
@JsonSerializable()
class OnlineEpisode {
final String title;
@JsonKey(name: 'pub_date_ms')
final int pubDate;
@JsonKey(name: 'audio_length_sec')
final int length;
OnlineEpisode({this.title, this.pubDate, this.length});
factory OnlineEpisode.fromJson(Map<String, dynamic> json) =>
_$OnlineEpisodeFromJson(json);
Map<String, dynamic> toJson() => _$OnlineEpisodeToJson(this);
}

View File

@ -0,0 +1,36 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'searchepisodes.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SearchEpisodes<E> _$SearchEpisodesFromJson<E>(Map<String, dynamic> json) {
return SearchEpisodes<E>(
episodes:
(json['episodes'] as List)?.map(_ConvertE<E>().fromJson)?.toList(),
nextEpisodeDate: json['next_episode_pub_date'] as int,
);
}
Map<String, dynamic> _$SearchEpisodesToJson<E>(SearchEpisodes<E> instance) =>
<String, dynamic>{
'episodes': instance.episodes?.map(_ConvertE<E>().toJson)?.toList(),
'next_episode_pub_date': instance.nextEpisodeDate,
};
OnlineEpisode _$OnlineEpisodeFromJson(Map<String, dynamic> json) {
return OnlineEpisode(
title: json['title'] as String,
pubDate: json['pub_date_ms'] as int,
length: json['audio_length_sec'] as int,
);
}
Map<String, dynamic> _$OnlineEpisodeToJson(OnlineEpisode instance) =>
<String, dynamic>{
'title': instance.title,
'pub_date_ms': instance.pubDate,
'audio_length_sec': instance.length,
};

View File

@ -1,43 +1,44 @@
import 'dart:ui';
import 'package:json_annotation/json_annotation.dart';
part 'searchpodcast.g.dart';
@JsonSerializable()
class SearchPodcast<P>{
class SearchPodcast<P> {
@_ConvertP()
final List<P> results;
@JsonKey(name: 'next_offset')
final int nextOffset;
final int total;
final int count;
SearchPodcast(
{this.results, this.nextOffset, this.total, this.count}
);
factory SearchPodcast.fromJson(Map<String, dynamic> json) =>
_$SearchPodcastFromJson<P>(json);
SearchPodcast({this.results, this.nextOffset, this.total, this.count});
factory SearchPodcast.fromJson(Map<String, dynamic> json) =>
_$SearchPodcastFromJson<P>(json);
Map<String, dynamic> toJson() => _$SearchPodcastToJson(this);
}
class _ConvertP<P> implements JsonConverter<P, Object>{
class _ConvertP<P> implements JsonConverter<P, Object> {
const _ConvertP();
@override
P fromJson(Object json){
P fromJson(Object json) {
return OnlinePodcast.fromJson(json) as P;
}
@override
Object toJson(P object){
Object toJson(P object) {
return object;
}
}
@JsonSerializable()
class OnlinePodcast{
class OnlinePodcast {
@JsonKey(name: 'earliest_pub_date_ms')
final int earliestPubDate;
@JsonKey(name: 'title_original')
final String title;
final String rss;
@JsonKey(name: 'lastest_pub_date_ms')
final int lastestPubDate;
@JsonKey(name: 'latest_pub_date_ms')
final int latestPubDate;
@JsonKey(name: 'description_original')
final String description;
@JsonKey(name: 'total_episodes')
@ -45,10 +46,30 @@ class OnlinePodcast{
final String image;
@JsonKey(name: 'publisher_original')
final String publisher;
final String id;
OnlinePodcast(
{this.earliestPubDate, this.title, this.count, this.description, this.image, this.lastestPubDate, this.rss, this.publisher}
);
{this.earliestPubDate,
this.title,
this.count,
this.description,
this.image,
this.latestPubDate,
this.rss,
this.publisher,
this.id});
factory OnlinePodcast.fromJson(Map<String, dynamic> json) =>
_$OnlinePodcastFromJson(json);
_$OnlinePodcastFromJson(json);
Map<String, dynamic> toJson() => _$OnlinePodcastToJson(this);
}
@override
bool operator ==(Object onlinePodcast) =>
onlinePodcast is OnlinePodcast && onlinePodcast.id == id;
@override
int get hashCode => hashValues(id, title);
int get interval {
if (count < 1) return null;
return (latestPubDate - earliestPubDate) ~/ count;
}
}

View File

@ -8,34 +8,33 @@ part of 'searchpodcast.dart';
SearchPodcast<P> _$SearchPodcastFromJson<P>(Map<String, dynamic> json) {
return SearchPodcast<P>(
results: (json['results'] as List)
?.map((e) => e == null ? null : _ConvertP<P>().fromJson(e))
?.toList(),
nextOffset: json['next_offset'] as int,
total: json['total'] as int,
count: json['count'] as int);
results: (json['results'] as List)?.map(_ConvertP<P>().fromJson)?.toList(),
nextOffset: json['next_offset'] as int,
total: json['total'] as int,
count: json['count'] as int,
);
}
Map<String, dynamic> _$SearchPodcastToJson<P>(SearchPodcast<P> instance) =>
<String, dynamic>{
'results': instance.results
?.map((e) => e == null ? null : _ConvertP<P>().toJson(e))
?.toList(),
'results': instance.results?.map(_ConvertP<P>().toJson)?.toList(),
'next_offset': instance.nextOffset,
'total': instance.total,
'count': instance.count
'count': instance.count,
};
OnlinePodcast _$OnlinePodcastFromJson(Map<String, dynamic> json) {
return OnlinePodcast(
earliestPubDate: json['earliest_pub_date_ms'] as int,
title: json['title_original'] as String,
count: json['total_episodes'] as int,
description: json['description_original'] as String,
image: json['image'] as String,
lastestPubDate: json['lastest_pub_date_ms'] as int,
rss: json['rss'] as String,
publisher: json['publisher_original'] as String);
earliestPubDate: json['earliest_pub_date_ms'] as int,
title: json['title_original'] as String,
count: json['total_episodes'] as int,
description: json['description_original'] as String,
image: json['image'] as String,
latestPubDate: json['latest_pub_date_ms'] as int,
rss: json['rss'] as String,
publisher: json['publisher_original'] as String,
id: json['id'] as String,
);
}
Map<String, dynamic> _$OnlinePodcastToJson(OnlinePodcast instance) =>
@ -43,9 +42,10 @@ Map<String, dynamic> _$OnlinePodcastToJson(OnlinePodcast instance) =>
'earliest_pub_date_ms': instance.earliestPubDate,
'title_original': instance.title,
'rss': instance.rss,
'lastest_pub_date_ms': instance.lastestPubDate,
'latest_pub_date_ms': instance.latestPubDate,
'description_original': instance.description,
'total_episodes': instance.count,
'image': instance.image,
'publisher_original': instance.publisher
'publisher_original': instance.publisher,
'id': instance.id,
};

View File

@ -34,12 +34,12 @@ class _AudioPanelState extends State<AudioPanel> with TickerProviderStateMixin {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 50))
..addListener(() {
setState(() {});
if (mounted) setState(() {});
});
_slowController =
AnimationController(vsync: this, duration: Duration(milliseconds: 200))
..addListener(() {
setState(() {});
if (mounted) setState(() {});
});
_animation =
Tween<double>(begin: initSize, end: initSize).animate(_controller);

View File

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
import '../generated/l10n.dart';
extension ContextExtension on BuildContext {
Color get primaryColor => Theme.of(this).primaryColor;
Color get accentColor => Theme.of(this).accentColor;
Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor;
Color get primaryColorDark => Theme.of(this).primaryColorDark;
Color get textColor => Theme.of(this).textTheme.bodyText1.color;
Brightness get brightness => Theme.of(this).brightness;
double get width => MediaQuery.of(this).size.width;
double get height => MediaQuery.of(this).size.width;
TextTheme get textTheme => Theme.of(this).textTheme;
S get s => S.of(this);
}

View File

@ -16,11 +16,12 @@ import 'open_container.dart';
import '../state/audio_state.dart';
import '../state/download_state.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../episodes/episode_detail.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import 'colorize.dart';
import 'context_extension.dart';
import 'extension_helper.dart';
import 'custompaint.dart';
enum Layout { three, two, one }

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../generated/l10n.dart';
extension ContextExtension on BuildContext {
Color get primaryColor => Theme.of(this).primaryColor;
Color get accentColor => Theme.of(this).accentColor;
Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor;
Color get primaryColorDark => Theme.of(this).primaryColorDark;
Color get textColor => Theme.of(this).textTheme.bodyText1.color;
Brightness get brightness => Theme.of(this).brightness;
double get width => MediaQuery.of(this).size.width;
double get height => MediaQuery.of(this).size.height;
TextTheme get textTheme => Theme.of(this).textTheme;
S get s => S.of(this);
}
extension IntExtension on int {
String toDate(BuildContext context) {
if (this == null) return '';
final s = context.s;
DateTime date = DateTime.fromMillisecondsSinceEpoch(this, isUtc: true);
var difference = DateTime.now().toUtc().difference(date);
if (difference.inHours < 24) {
return s.hoursAgo(difference.inHours);
} else if (difference.inDays < 7) {
return s.daysAgo(difference.inDays);
} else {
return DateFormat.yMMMd().format(
DateTime.fromMillisecondsSinceEpoch(this, isUtc: true).toLocal());
}
}
String get toTime =>
'${(this ~/ 60)}:${(this.truncate() % 60).toString().padLeft(2, '0')}';
String toInterval(BuildContext context) {
if (this == null || this.isNegative) return '';
final s = context.s;
var interval = Duration(milliseconds: this);
if (interval.inHours <= 48)
return 'Published daily';
else if (interval.inDays > 2 && interval.inDays <= 14)
return 'Published weekly';
else if (interval.inDays > 14 && interval.inDays < 60)
return 'Published monthly';
else
return 'Published yearly';
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'context_extension.dart';
import 'extension_helper.dart';
generalDialog(BuildContext context,
{Widget title, Widget content, List<Widget> actions}) =>
@ -26,7 +26,7 @@ generalDialog(BuildContext context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0))),
titlePadding: EdgeInsets.all(20),
title: SizedBox(width: context.width-160, child: title),
title: SizedBox(width: context.width - 160, child: title),
content: content,
actionsPadding: EdgeInsets.all(10),
actions: actions),

View File

@ -4,7 +4,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'context_extension.dart';
import 'extension_helper.dart';
/// Signature for a function that creates a [Widget] to be used within an
/// [OpenContainer].

View File

@ -11,41 +11,43 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^0.1.3
json_annotation: ^3.0.1
sqflite: ^1.3.0
flutter_html: ^0.11.1
path_provider: ^1.6.8
color_thief_flutter: ^1.0.2
provider: ^4.1.2
google_fonts: ^1.1.0
dio: ^3.0.9
file_picker: ^1.9.0+1
marquee: ^1.3.1
flutter_downloader: ^1.4.4
permission_handler: ^5.0.0+hotfix.3
fluttertoast: ^4.0.1
intl: ^0.16.1
url_launcher: ^5.4.10
image: ^2.1.12
shared_preferences: ^0.5.7
uuid: ^2.2.0
tuple: ^1.0.3
cached_network_image: ^2.2.0+1
workmanager: ^0.2.3
fl_chart: ^0.10.1
audio_service: ^0.8.0
flutter_file_dialog: ^0.0.5
flutter_linkify: ^3.1.3
extended_nested_scroll_view: ^0.4.0
connectivity: ^0.4.8+2
flare_flutter: ^2.0.5
rxdart: ^0.24.0
wc_flutter_share: ^0.2.1
auto_animated: ^2.1.0
audio_service: ^0.11.2
cached_network_image: ^2.2.0+1
color_thief_flutter: ^1.0.2
cupertino_icons: ^0.1.3
connectivity: ^0.4.9
dio: ^3.0.9
extended_nested_scroll_view: ^1.0.1
feature_discovery: ^0.10.0
file_picker: ^1.12.0
flutter_html: ^0.11.1
flutter_downloader: ^1.4.4
fluttertoast: ^4.0.0
flutter_isolate: ^1.0.0+14
flutter_time_picker_spinner: ^1.0.6+1
flutter_linkify: ^3.1.3
flutter_file_dialog: ^0.0.5
flare_flutter: ^2.0.5
fl_chart: ^0.10.1
marquee: ^1.3.1
google_fonts: ^1.1.0
image: ^2.1.14
intl: ^0.16.1
json_serializable: ^3.3.0
json_annotation: ^3.0.1
path_provider: ^1.6.11
permission_handler: ^5.0.1
provider: ^4.3.1
rxdart: ^0.24.1
sqflite: ^1.3.1
shared_preferences: ^0.5.8
tuple: ^1.0.3
url_launcher: ^5.5.0
uuid: ^2.2.0
xml: ^4.2.0
workmanager: ^0.2.3
wc_flutter_share: ^0.2.2
just_audio:
git:
url: https://github.com/stonega/just_audio.git
@ -59,9 +61,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
dependency_overrides:
xml: "4.2.0"
build_runner: ^1.10.0
flutter:
assets: