Back to saty in background
Playlist UI change
@ -2,7 +2,7 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cirrusci/flutter:v1.14.6
|
||||
- image: cirrusci/flutter:v1.15.17
|
||||
|
||||
branches:
|
||||
only: master
|
||||
|
@ -79,7 +79,7 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
@ -3,10 +3,24 @@ package com.stonegate.tsacdop
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.view.FlutterNativeView
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import io.flutter.embedding.engine.dart.DartExecutor.DartCallback
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor, "android_app_retain").apply {
|
||||
setMethodCallHandler { method, result ->
|
||||
if (method.method == "sendToBackground") {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 633 B |
Before Width: | Height: | Size: 427 B After Width: | Height: | Size: 390 B |
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 758 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.70'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.android.tools.build:gradle:3.6.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
#Fri Mar 20 23:46:20 CST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
@ -9,7 +9,6 @@ import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||
void callbackDispatcher() {
|
||||
Workmanager.executeTask((task, inputData) async {
|
||||
var dbHelper = DBHelper();
|
||||
print('Start task');
|
||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||
int i = 0;
|
||||
await Future.forEach(podcastList, (podcastLocal) async {
|
||||
@ -135,7 +134,7 @@ class SettingState extends ChangeNotifier {
|
||||
|
||||
Future _getUpdateInterval() async {
|
||||
_initUpdateTag = await intervalstorage.getInt();
|
||||
_updateInterval = _initUpdateTag == 0 ? 24 : _initUpdateTag;
|
||||
_updateInterval = _initUpdateTag;
|
||||
}
|
||||
|
||||
Future _saveUpdateInterval() async {
|
||||
|
@ -11,6 +11,8 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/episodebrief.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
@ -31,10 +33,11 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
||||
bool _loaddes;
|
||||
bool _showMenu;
|
||||
String path;
|
||||
String _description;
|
||||
Future getSDescription(String url) async {
|
||||
var dbHelper = DBHelper();
|
||||
widget.episodeItem.description = (await dbHelper.getDescription(url))
|
||||
.replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), '');
|
||||
_description = (await dbHelper.getDescription(url))
|
||||
.replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), '').replaceAll('\r', '');
|
||||
if (mounted)
|
||||
setState(() {
|
||||
_loaddes = true;
|
||||
@ -85,12 +88,11 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
// statusBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
title: Text(widget.episodeItem.feedTitle),
|
||||
// title: Text(widget.episodeItem.feedTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Stack(
|
||||
@ -162,7 +164,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
||||
style: textstyle),
|
||||
)
|
||||
: Center(),
|
||||
widget.episodeItem.enclosureLength != null
|
||||
widget.episodeItem.enclosureLength != null &&
|
||||
widget.episodeItem.enclosureLength != 0
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.lightBlue[300],
|
||||
@ -193,15 +196,15 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
||||
padding: EdgeInsets.only(top: 5.0),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
//physics: const AlwaysScrollableScrollPhysics(),
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
controller: _controller,
|
||||
child: _loaddes
|
||||
? (widget.episodeItem.description.contains('<'))
|
||||
? (_description.contains('<'))
|
||||
? Html(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 20.0),
|
||||
defaultTextStyle: TextStyle(height: 1.8),
|
||||
data: widget.episodeItem.description,
|
||||
data: _description,
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
@ -215,12 +218,20 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 20.0),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
widget.episodeItem.description,
|
||||
child: SelectableLinkify(
|
||||
onOpen: (link) {
|
||||
_launchUrl(link.url);
|
||||
},
|
||||
text: _description,
|
||||
style: TextStyle(
|
||||
height: 1.8,
|
||||
),
|
||||
))
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
),
|
||||
),
|
||||
|
@ -32,6 +32,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
var _androidAppRetain = MethodChannel("android_app_retain");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -71,7 +73,16 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
PopupMenu(),
|
||||
],
|
||||
),
|
||||
body: Home(),
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (Platform.isAndroid) {
|
||||
_androidAppRetain.invokeMethod('sendToBackground');
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
child: Home()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'hometab.dart';
|
||||
import 'package:tsacdop/home/appbar/importompl.dart';
|
||||
import 'package:tsacdop/home/audioplayer.dart';
|
||||
import 'homescroll.dart';
|
||||
import 'home_groups.dart';
|
||||
|
||||
class Home extends StatelessWidget {
|
||||
|
||||
|
@ -40,7 +40,7 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
||||
bool isLoading = groupList.isLoading;
|
||||
return isLoading
|
||||
? Container(
|
||||
height: (_width - 20) / 3 + 110,
|
||||
height: (_width - 20) / 3 + 140,
|
||||
)
|
||||
: groups[_groupIndex].podcastList.length == 0
|
||||
? Column(
|
@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -6,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:tsacdop/episodes/episodedetail.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/episodebrief.dart';
|
||||
@ -19,7 +21,8 @@ class PlaylistPage extends StatefulWidget {
|
||||
class _PlaylistPageState extends State<PlaylistPage> {
|
||||
final GlobalKey<AnimatedListState> _playlistKey = GlobalKey();
|
||||
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
|
||||
Widget episodeTag(String text, Color color) {
|
||||
|
||||
Widget _episodeTag(String text, Color color) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
||||
@ -31,6 +34,40 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
);
|
||||
}
|
||||
|
||||
int _sumPlaylistLength(List<EpisodeBrief> episodes) {
|
||||
int sum = 0;
|
||||
if (episodes.length == 0) {
|
||||
return sum;
|
||||
} else {
|
||||
episodes.forEach((episode) {
|
||||
sum += episode.duration;
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollController _controller;
|
||||
_scrollListener() {
|
||||
double value = _controller.offset;
|
||||
setState(() => _topHeight = (100 - value) > 0 ? 100 - value : 0);
|
||||
}
|
||||
|
||||
double _topHeight;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_topHeight = 100;
|
||||
_controller = ScrollController();
|
||||
_controller.addListener(_scrollListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
@ -42,8 +79,9 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Playlist'),
|
||||
title: _topHeight == 0 ? Text('Playlist') : Center(),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
@ -53,137 +91,225 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
selector: (_, audio) =>
|
||||
Tuple2(audio.queue.playlist, audio.playerRunning),
|
||||
builder: (_, data, __) {
|
||||
return AnimatedList(
|
||||
key: _playlistKey,
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
initialItemCount: data.item1.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
Color _c =
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? data.item1[index].primaryColor.colorizedark()
|
||||
: data.item1[index].primaryColor.colorizeLight();
|
||||
return ScaleTransition(
|
||||
alignment: Alignment.centerLeft,
|
||||
scale: animation,
|
||||
child: Dismissible(
|
||||
background: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
],
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Transform.scale(
|
||||
alignment: Alignment.topLeft,
|
||||
scale: _topHeight / 100,
|
||||
child: Container(
|
||||
height: _topHeight,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: (_topHeight - 60) > 0 ? _topHeight - 60 : 0,
|
||||
left: 60),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Total ',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 20,
|
||||
),
|
||||
height: 50,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
key: Key(data.item1[index].enclosureUrl),
|
||||
onDismissed: (direction) async {
|
||||
await audio.delFromPlaylist(data.item1[index]);
|
||||
_playlistKey.currentState.removeItem(
|
||||
index, (context, animation) => Center());
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Removed From Playlist',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
data.item1[index].title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _c.withOpacity(0.5),
|
||||
backgroundImage: FileImage(
|
||||
File("${data.item1[index].imagePath}")),
|
||||
),
|
||||
trailing: index == 0
|
||||
? data.item2
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 12.0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 15,
|
||||
child: WaveLoader()),
|
||||
)
|
||||
: IconButton(
|
||||
icon: Icon(Icons.play_arrow),
|
||||
onPressed: () => audio.playlistLoad())
|
||||
: IconButton(
|
||||
tooltip: 'Move to Top',
|
||||
icon:
|
||||
Icon(LineIcons.arrow_circle_up_solid),
|
||||
onPressed: () async {
|
||||
await audio
|
||||
.moveToTop(data.item1[index]);
|
||||
_playlistKey.currentState.removeItem(
|
||||
index,
|
||||
(context, animation) =>
|
||||
Container());
|
||||
data.item2
|
||||
? _playlistKey.currentState
|
||||
.insertItem(1)
|
||||
: _playlistKey.currentState
|
||||
.insertItem(0);
|
||||
}),
|
||||
subtitle: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
(data.item1[index].explicit == 1)
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[800],
|
||||
shape: BoxShape.circle),
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
margin:
|
||||
EdgeInsets.only(right: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text('E',
|
||||
style: TextStyle(
|
||||
color: Colors.white)))
|
||||
: Center(),
|
||||
data.item1[index].duration != 0
|
||||
? episodeTag(
|
||||
(data.item1[index].duration)
|
||||
.toString() +
|
||||
'mins',
|
||||
Colors.cyan[300])
|
||||
: Center(),
|
||||
data.item1[index].enclosureLength != null
|
||||
? episodeTag(
|
||||
((data.item1[index]
|
||||
.enclosureLength) ~/
|
||||
1000000)
|
||||
.toString() +
|
||||
'MB',
|
||||
Colors.lightBlue[300])
|
||||
: Center(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 2,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: data.item1.length.toString(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 40,
|
||||
)),
|
||||
TextSpan(
|
||||
text: data.item1.length < 2
|
||||
? ' episode'
|
||||
: ' episodes ',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 20,
|
||||
)),
|
||||
TextSpan(
|
||||
text: _sumPlaylistLength(data.item1).toString(),
|
||||
style: GoogleFonts.teko(
|
||||
textStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 60,
|
||||
)),
|
||||
),
|
||||
TextSpan(
|
||||
text: ' mins',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 20,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedList(
|
||||
controller: _controller,
|
||||
key: _playlistKey,
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
initialItemCount: data.item1.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
Color _c = (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? data.item1[index].primaryColor.colorizedark()
|
||||
: data.item1[index].primaryColor.colorizeLight();
|
||||
return ScaleTransition(
|
||||
alignment: Alignment.centerLeft,
|
||||
scale: animation,
|
||||
child: Dismissible(
|
||||
background: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.red),
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Icon(
|
||||
LineIcons.trash_alt_solid,
|
||||
color: Colors.white,
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.red),
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Icon(
|
||||
LineIcons.trash_alt_solid,
|
||||
color: Colors.white,
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
height: 50,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
key: Key(data.item1[index].enclosureUrl),
|
||||
onDismissed: (direction) async {
|
||||
await audio.delFromPlaylist(data.item1[index]);
|
||||
_playlistKey.currentState.removeItem(
|
||||
index, (context, animation) => Center());
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Removed From Playlist',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10.0, bottom: 5.0),
|
||||
child: Text(
|
||||
data.item1[index].title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _c.withOpacity(0.5),
|
||||
backgroundImage: FileImage(File(
|
||||
"${data.item1[index].imagePath}")),
|
||||
),
|
||||
trailing: index == 0
|
||||
? data.item2
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 12.0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 15,
|
||||
child: WaveLoader()),
|
||||
)
|
||||
: IconButton(
|
||||
icon: Icon(Icons.play_arrow),
|
||||
onPressed: () =>
|
||||
audio.playlistLoad())
|
||||
: Transform.rotate(
|
||||
angle: math.pi,
|
||||
child: IconButton(
|
||||
tooltip: 'Move to Top',
|
||||
icon: Icon(
|
||||
LineIcons.download_solid),
|
||||
onPressed: () async {
|
||||
await audio.moveToTop(
|
||||
data.item1[index]);
|
||||
_playlistKey.currentState
|
||||
.removeItem(
|
||||
index,
|
||||
(context,
|
||||
animation) =>
|
||||
Container());
|
||||
data.item2
|
||||
? _playlistKey
|
||||
.currentState
|
||||
.insertItem(1)
|
||||
: _playlistKey
|
||||
.currentState
|
||||
.insertItem(0);
|
||||
}),
|
||||
),
|
||||
subtitle: Container(
|
||||
padding:
|
||||
EdgeInsets.only(top: 5, bottom: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
(data.item1[index].explicit == 1)
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[800],
|
||||
shape: BoxShape.circle),
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
margin: EdgeInsets.only(
|
||||
right: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text('E',
|
||||
style: TextStyle(
|
||||
color: Colors.white)))
|
||||
: Center(),
|
||||
data.item1[index].duration != 0
|
||||
? _episodeTag(
|
||||
(data.item1[index].duration)
|
||||
.toString() +
|
||||
'mins',
|
||||
Colors.cyan[300])
|
||||
: Center(),
|
||||
data.item1[index].enclosureLength !=
|
||||
null
|
||||
? _episodeTag(
|
||||
((data.item1[index]
|
||||
.enclosureLength) ~/
|
||||
1000000)
|
||||
.toString() +
|
||||
'MB',
|
||||
Colors.lightBlue[300])
|
||||
: Center(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -417,7 +417,7 @@ class DBHelper {
|
||||
}
|
||||
|
||||
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||
final length = feed.items[i]?.enclosure?.length;
|
||||
final length = feed.items[i]?.enclosure?.length ?? 0;
|
||||
final pubDate = feed.items[i].pubDate;
|
||||
final date = _parsePubDate(pubDate);
|
||||
final milliseconds = date.millisecondsSinceEpoch;
|
||||
|
@ -2,28 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
import 'package:tsacdop/class/podcastlocal.dart';
|
||||
import 'package:tsacdop/class/podcast_group.dart';
|
||||
import 'package:tsacdop/home/appbar/addpodcast.dart';
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/importompl.dart';
|
||||
import 'package:tsacdop/class/settingstate.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
|
||||
void callbackDispatcher() {
|
||||
Workmanager.executeTask((task, inputData) async {
|
||||
var dbHelper = DBHelper();
|
||||
print('Start task');
|
||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||
await Future.forEach(podcastList, (podcastLocal) async {
|
||||
await dbHelper.updatePodcastRss(podcastLocal);
|
||||
print('Refresh ' + podcastLocal.title);
|
||||
});
|
||||
return Future.value(true);
|
||||
});
|
||||
}
|
||||
|
||||
final SettingState themeSetting = SettingState();
|
||||
Future main() async {
|
||||
|
@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:tsacdop/class/podcastlocal.dart';
|
||||
@ -389,6 +389,14 @@ class _AboutPodcastState extends State<AboutPodcast> {
|
||||
setState(() => _load = true);
|
||||
}
|
||||
|
||||
_launchUrl(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -419,8 +427,15 @@ class _AboutPodcastState extends State<AboutPodcast> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
_description,
|
||||
Linkify(
|
||||
onOpen: (link) {
|
||||
_launchUrl(link.url);
|
||||
},
|
||||
text: _description,
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
textBaseline: TextBaseline.ideographic),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -432,12 +447,27 @@ class _AboutPodcastState extends State<AboutPodcast> {
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(_description),
|
||||
: Linkify(
|
||||
onOpen: (link) {
|
||||
_launchUrl(link.url);
|
||||
},
|
||||
text: _description,
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
textBaseline: TextBaseline.ideographic),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SelectableText(
|
||||
_description,
|
||||
toolbarOptions: ToolbarOptions(copy: true),
|
||||
return Linkify(
|
||||
text: _description,
|
||||
onOpen: (link) {
|
||||
_launchUrl(link.url);
|
||||
},
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
decoration: TextDecoration.underline,
|
||||
textBaseline: TextBaseline.ideographic),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -301,50 +301,47 @@ class _PodcastCardState extends State<PodcastCard> {
|
||||
Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(15, 15, 15, 1),
|
||||
statusBarColor:
|
||||
Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(5, 5, 5, 1),
|
||||
// statusBarColor:
|
||||
// Theme.of(context).brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||
// : Color.fromRGBO(5, 5, 5, 1),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: AlertDialog(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
titlePadding: EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
title: Text('Remove confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to unsubscribe?'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600]),
|
||||
),
|
||||
child: AlertDialog(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
titlePadding: EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
title: Text('Remove confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to unsubscribe?'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600]),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
_groupList.removePodcast(
|
||||
widget.podcastLocal.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'CONFIRM',
|
||||
style:
|
||||
TextStyle(color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
_groupList.removePodcast(
|
||||
widget.podcastLocal.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'CONFIRM',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -136,9 +136,9 @@ class _DownloadsManageState extends State<DownloadsManage> {
|
||||
TextSpan(
|
||||
text: _fileNum.toString(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold)),
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 40,
|
||||
)),
|
||||
TextSpan(
|
||||
text: _fileNum < 2 ? ' episode' : ' episodes ',
|
||||
style: TextStyle(
|
||||
@ -148,9 +148,9 @@ class _DownloadsManageState extends State<DownloadsManage> {
|
||||
TextSpan(
|
||||
text: (_size ~/ 1000000).toString(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 60,
|
||||
fontWeight: FontWeight.bold)),
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 60,
|
||||
)),
|
||||
TextSpan(
|
||||
text: ' Mb',
|
||||
style: TextStyle(
|
||||
|
@ -59,8 +59,8 @@ class Settings extends StatelessWidget {
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SingleChildScrollView(
|
||||
//physics: const AlwaysScrollableScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@ -93,14 +93,16 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ThemeSetting())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.adjust_solid),
|
||||
title: Text('Appearance'),
|
||||
subtitle: Text('Colors and themes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.play_circle),
|
||||
title: Text('AutoPlay'),
|
||||
subtitle: Text('Autoplay next episode in playlist'),
|
||||
@ -117,7 +119,8 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SyncingSetting())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.cloud_download_alt_solid),
|
||||
title: Text('Syncing'),
|
||||
subtitle: Text('Refresh podcasts in the background'),
|
||||
@ -128,7 +131,8 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StorageSetting())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.save),
|
||||
title: Text('Storage'),
|
||||
subtitle: Text('Manage cache and download storage'),
|
||||
@ -139,7 +143,8 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PlayedHistory())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.update),
|
||||
title: Text('History'),
|
||||
subtitle: Text('Listen data'),
|
||||
@ -149,7 +154,8 @@ class Settings extends StatelessWidget {
|
||||
onTap: () {
|
||||
_exportOmpl();
|
||||
},
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.file_code_solid),
|
||||
title: Text('Export'),
|
||||
subtitle: Text('Export ompl file'),
|
||||
@ -184,25 +190,31 @@ class Settings extends StatelessWidget {
|
||||
ListTile(
|
||||
onTap: () => _launchUrl(
|
||||
'https://github.com/stonega/tsacdop/releases'),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.map_signs_solid),
|
||||
title: Text('Changelog'),
|
||||
subtitle: Text('List of chagnes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => Libries())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Libries())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.book_open_solid),
|
||||
title: Text('Libraries'),
|
||||
subtitle: Text('Open source libraries in application'),
|
||||
subtitle:
|
||||
Text('Open source libraries in application'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
onTap: () => _launchUrl(
|
||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.bug_solid),
|
||||
title: Text('Feedback'),
|
||||
subtitle: Text('Bugs and feature requests'),
|
||||
|
@ -42,6 +42,7 @@ class StorageSetting extends StatelessWidget {
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
|
@ -48,6 +48,7 @@ class SyncingSetting extends StatelessWidget {
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
|
@ -43,6 +43,7 @@ class ThemeSetting extends StatelessWidget {
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
|
@ -31,7 +31,7 @@ class EpisodeGrid extends StatelessWidget {
|
||||
this.heroTag,
|
||||
this.updateCount = 0})
|
||||
: super(key: key);
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
@ -46,7 +46,6 @@ class EpisodeGrid extends StatelessWidget {
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
||||
|
||||
items: <PopupMenuEntry<int>>[
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
@ -85,7 +84,7 @@ class EpisodeGrid extends StatelessWidget {
|
||||
if (value == 0) {
|
||||
if (!isPlaying) audio.episodeLoad(episode);
|
||||
} else if (value == 1) {
|
||||
if (!isInPlaylist) {
|
||||
if (!isInPlaylist) {
|
||||
audio.addToPlaylist(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to playlist',
|
||||
@ -97,7 +96,7 @@ class EpisodeGrid extends StatelessWidget {
|
||||
msg: 'Removed from playlist',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -187,7 +186,15 @@ class EpisodeGrid extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
index < updateCount ? Text('New', style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic)) : Center(),
|
||||
index < updateCount
|
||||
? Text('New',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontStyle: FontStyle.italic))
|
||||
: Center(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2),
|
||||
),
|
||||
showNumber
|
||||
? Container(
|
||||
alignment: Alignment.topRight,
|
||||
@ -371,7 +378,7 @@ class _DownloadIconState extends State<DownloadIcon> {
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: Colors.grey[200],
|
||||
strokeWidth: 1,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).accentColor),
|
||||
value: task.progress / 100,
|
||||
),
|
||||
);
|
||||
@ -391,7 +398,7 @@ class _DownloadIconState extends State<DownloadIcon> {
|
||||
data: IconThemeData(size: 15),
|
||||
child: Icon(
|
||||
Icons.done_all,
|
||||
color: Colors.blue,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
);
|
||||
} else if (task.status == DownloadTaskStatus.failed) {
|
||||
|
@ -347,6 +347,8 @@ Future<T> _showMenu<T>({
|
||||
break;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
|
||||
}
|
||||
|
||||
@ -469,6 +471,8 @@ class MyPopupMenuButtonState<T> extends State<MyPopupMenuButton<T>> {
|
||||
return const Icon(Icons.more_vert);
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return const Icon(Icons.more_horiz);
|
||||
}
|
||||
return null;
|
||||
|
@ -49,8 +49,7 @@ class RssItem {
|
||||
}
|
||||
return RssItem(
|
||||
title: findElementOrNull(element, "title")?.text,
|
||||
description: findElementOrNull(element, "description")?.text?.trim()
|
||||
,
|
||||
description: findElementOrNull(element, "description")?.text?.trim(),
|
||||
link: findElementOrNull(element, "link")?.text?.trim(),
|
||||
categories: element.findElements("category").map((element) {
|
||||
return RssCategory.parse(element);
|
||||
@ -60,7 +59,7 @@ class RssItem {
|
||||
author: findElementOrNull(element, "author")?.text?.trim(),
|
||||
// comments: findElementOrNull(element, "comments")?.text,
|
||||
// source: RssSource.parse(findElementOrNull(element, "source")),
|
||||
content: RssContent.parse(findElementOrNull(element, "content:encoded")),
|
||||
content: RssContent.parse(findElementOrNull(element, "content:encoded")),
|
||||
// media: Media.parse(element),
|
||||
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
|
||||
//dc: DublinCore.parse(element),
|
||||
|
@ -28,7 +28,7 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
json_annotation: ^3.0.1
|
||||
sqflite: ^1.2.2+1
|
||||
sqflite: ^1.3.0
|
||||
flutter_html: ^0.11.1
|
||||
path_provider: ^1.6.1
|
||||
color_thief_flutter: ^1.0.1
|
||||
@ -59,6 +59,7 @@ dev_dependencies:
|
||||
git:
|
||||
url: https://github.com/galonsos/line_icons.git
|
||||
flutter_file_dialog: ^0.0.5
|
||||
flutter_linkify: ^3.1.0
|
||||
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
|