mirror of
https://github.com/stonega/tsacdop
synced 2025-03-06 12:17:56 +01:00
Search ui changed a lot, add podcast detail panel.
Update audio service to latest version.
This commit is contained in:
parent
602cc67342
commit
b619be9a9b
@ -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>
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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]),
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
39
lib/service/api_search.dart
Normal file
39
lib/service/api_search.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -62,7 +62,7 @@ class EpisodeBrief {
|
||||
title: title,
|
||||
artist: feedTitle,
|
||||
album: feedTitle,
|
||||
// duration: 0,
|
||||
duration: Duration.zero,
|
||||
artUri: 'file://$imagePath',
|
||||
extras: {'skip': skipSeconds});
|
||||
}
|
||||
|
19
lib/type/play_histroy.dart
Normal file
19
lib/type/play_histroy.dart
Normal 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
58
lib/type/playlist.dart
Normal 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;
|
||||
}
|
||||
}
|
41
lib/type/searchepisodes.dart
Normal file
41
lib/type/searchepisodes.dart
Normal 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);
|
||||
}
|
36
lib/type/searchepisodes.g.dart
Normal file
36
lib/type/searchepisodes.g.dart
Normal 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,
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
@ -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);
|
||||
}
|
@ -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 }
|
||||
|
50
lib/util/extension_helper.dart
Normal file
50
lib/util/extension_helper.dart
Normal 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';
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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].
|
||||
|
68
pubspec.yaml
68
pubspec.yaml
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user