Back to saty in background

Playlist UI change
This commit is contained in:
stonegate 2020-03-22 00:14:10 +08:00
parent c5fcfd239b
commit 64dee98523
29 changed files with 447 additions and 249 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs: jobs:
build: build:
docker: docker:
- image: cirrusci/flutter:v1.14.6 - image: cirrusci/flutter:v1.15.17
branches: branches:
only: master only: master

View File

@ -79,7 +79,7 @@ flutter {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
} }

View File

@ -3,10 +3,24 @@ package com.stonegate.tsacdop
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.view.FlutterNativeView
import io.flutter.plugins.GeneratedPluginRegistrant 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() { class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine); GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor, "android_app_retain").apply {
setMethodCallHandler { method, result ->
if (method.method == "sendToBackground") {
moveTaskToBack(true)
}
}
}
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 B

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.3.70'
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -1,6 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017 #Fri Mar 20 23:46:20 CST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View File

@ -9,7 +9,6 @@ import 'package:tsacdop/local_storage/key_value_storage.dart';
void callbackDispatcher() { void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async { Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
print('Start task');
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll(); List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0; int i = 0;
await Future.forEach(podcastList, (podcastLocal) async { await Future.forEach(podcastList, (podcastLocal) async {
@ -135,7 +134,7 @@ class SettingState extends ChangeNotifier {
Future _getUpdateInterval() async { Future _getUpdateInterval() async {
_initUpdateTag = await intervalstorage.getInt(); _initUpdateTag = await intervalstorage.getInt();
_updateInterval = _initUpdateTag == 0 ? 24 : _initUpdateTag; _updateInterval = _initUpdateTag;
} }
Future _saveUpdateInterval() async { Future _saveUpdateInterval() async {

View File

@ -11,6 +11,8 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.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/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
@ -31,10 +33,11 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
bool _loaddes; bool _loaddes;
bool _showMenu; bool _showMenu;
String path; String path;
String _description;
Future getSDescription(String url) async { Future getSDescription(String url) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
widget.episodeItem.description = (await dbHelper.getDescription(url)) _description = (await dbHelper.getDescription(url))
.replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), ''); .replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), '').replaceAll('\r', '');
if (mounted) if (mounted)
setState(() { setState(() {
_loaddes = true; _loaddes = true;
@ -85,12 +88,11 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness, Theme.of(context).accentColorBrightness,
// statusBarColor: Theme.of(context).primaryColor,
), ),
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar( appBar: AppBar(
title: Text(widget.episodeItem.feedTitle), // title: Text(widget.episodeItem.feedTitle),
centerTitle: true, centerTitle: true,
), ),
body: Stack( body: Stack(
@ -162,7 +164,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
style: textstyle), style: textstyle),
) )
: Center(), : Center(),
widget.episodeItem.enclosureLength != null widget.episodeItem.enclosureLength != null &&
widget.episodeItem.enclosureLength != 0
? Container( ? Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.lightBlue[300], color: Colors.lightBlue[300],
@ -193,15 +196,15 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
padding: EdgeInsets.only(top: 5.0), padding: EdgeInsets.only(top: 5.0),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
//physics: const AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
controller: _controller, controller: _controller,
child: _loaddes child: _loaddes
? (widget.episodeItem.description.contains('<')) ? (_description.contains('<'))
? Html( ? Html(
padding: padding:
EdgeInsets.symmetric(horizontal: 20.0), EdgeInsets.symmetric(horizontal: 20.0),
defaultTextStyle: TextStyle(height: 1.8), defaultTextStyle: TextStyle(height: 1.8),
data: widget.episodeItem.description, data: _description,
linkStyle: TextStyle( linkStyle: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
@ -215,12 +218,20 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
padding: padding:
EdgeInsets.symmetric(horizontal: 20.0), EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Text( child: SelectableLinkify(
widget.episodeItem.description, onOpen: (link) {
_launchUrl(link.url);
},
text: _description,
style: TextStyle( style: TextStyle(
height: 1.8, height: 1.8,
), ),
)) linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
),
),
)
: Center(), : Center(),
), ),
), ),

View File

@ -32,6 +32,8 @@ class _MyHomePageState extends State<MyHomePage> {
final _MyHomePageDelegate _delegate = _MyHomePageDelegate(); final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
var _androidAppRetain = MethodChannel("android_app_retain");
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -71,7 +73,16 @@ class _MyHomePageState extends State<MyHomePage> {
PopupMenu(), PopupMenu(),
], ],
), ),
body: Home(), body: WillPopScope(
onWillPop: () async {
if (Platform.isAndroid) {
_androidAppRetain.invokeMethod('sendToBackground');
return false;
} else {
return true;
}
},
child: Home()),
), ),
); );
} }

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'hometab.dart'; import 'hometab.dart';
import 'package:tsacdop/home/appbar/importompl.dart'; import 'package:tsacdop/home/appbar/importompl.dart';
import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/home/audioplayer.dart';
import 'homescroll.dart'; import 'home_groups.dart';
class Home extends StatelessWidget { class Home extends StatelessWidget {

View File

@ -40,7 +40,7 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
bool isLoading = groupList.isLoading; bool isLoading = groupList.isLoading;
return isLoading return isLoading
? Container( ? Container(
height: (_width - 20) / 3 + 110, height: (_width - 20) / 3 + 140,
) )
: groups[_groupIndex].podcastList.length == 0 : groups[_groupIndex].podcastList.length == 0
? Column( ? Column(

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -6,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
@ -19,7 +21,8 @@ class PlaylistPage extends StatefulWidget {
class _PlaylistPageState extends State<PlaylistPage> { class _PlaylistPageState extends State<PlaylistPage> {
final GlobalKey<AnimatedListState> _playlistKey = GlobalKey(); final GlobalKey<AnimatedListState> _playlistKey = GlobalKey();
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black); final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
Widget episodeTag(String text, Color color) {
Widget _episodeTag(String text, Color color) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
@ -42,8 +79,9 @@ class _PlaylistPageState extends State<PlaylistPage> {
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
), ),
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar( appBar: AppBar(
title: Text('Playlist'), title: _topHeight == 0 ? Text('Playlist') : Center(),
elevation: 0, elevation: 0,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
), ),
@ -53,137 +91,225 @@ class _PlaylistPageState extends State<PlaylistPage> {
selector: (_, audio) => selector: (_, audio) =>
Tuple2(audio.queue.playlist, audio.playerRunning), Tuple2(audio.queue.playlist, audio.playerRunning),
builder: (_, data, __) { builder: (_, data, __) {
return AnimatedList( return Column(
key: _playlistKey, mainAxisSize: MainAxisSize.min,
shrinkWrap: true, crossAxisAlignment: CrossAxisAlignment.start,
scrollDirection: Axis.vertical, children: <Widget>[
initialItemCount: data.item1.length, Transform.scale(
itemBuilder: (context, index, animation) { alignment: Alignment.topLeft,
Color _c = scale: _topHeight / 100,
(Theme.of(context).brightness == Brightness.light) child: Container(
? data.item1[index].primaryColor.colorizedark() height: _topHeight,
: data.item1[index].primaryColor.colorizeLight(); padding: EdgeInsets.only(
return ScaleTransition( bottom: (_topHeight - 60) > 0 ? _topHeight - 60 : 0,
alignment: Alignment.centerLeft, left: 60),
scale: animation, alignment: Alignment.bottomLeft,
child: Dismissible( child: RichText(
background: Container( text: TextSpan(
padding: EdgeInsets.symmetric(horizontal: 20.0), text: 'Total ',
child: Row( style: TextStyle(
mainAxisAlignment: MainAxisAlignment.spaceBetween, color: Theme.of(context).accentColor,
children: <Widget>[ fontSize: 20,
Icon(
Icons.delete,
color: Theme.of(context).accentColor,
),
Icon(
Icons.delete,
color: Theme.of(context).accentColor,
),
],
), ),
height: 50, children: <TextSpan>[
color: Colors.grey[500], TextSpan(
), text: data.item1.length.toString(),
key: Key(data.item1[index].enclosureUrl), style: TextStyle(
onDismissed: (direction) async { color: Theme.of(context).accentColor,
await audio.delFromPlaylist(data.item1[index]); fontSize: 40,
_playlistKey.currentState.removeItem( )),
index, (context, animation) => Center()); TextSpan(
Fluttertoast.showToast( text: data.item1.length < 2
msg: 'Removed From Playlist', ? ' episode'
gravity: ToastGravity.BOTTOM, : ' episodes ',
); style: TextStyle(
}, color: Theme.of(context).accentColor,
child: Column( fontSize: 20,
children: <Widget>[ )),
ListTile( TextSpan(
title: Text( text: _sumPlaylistLength(data.item1).toString(),
data.item1[index].title, style: GoogleFonts.teko(
maxLines: 1, textStyle: TextStyle(
overflow: TextOverflow.ellipsis, color: Theme.of(context).accentColor,
), fontSize: 60,
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,
), ),
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,
),
],
),
),
);
}),
),
],
);
}, },
), ),
), ),

View File

@ -417,7 +417,7 @@ class DBHelper {
} }
final title = feed.items[i].itunes.title ?? feed.items[i].title; 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 pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate); final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch; final milliseconds = date.millisecondsSinceEpoch;

View File

@ -2,28 +2,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.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/class/podcast_group.dart';
import 'package:tsacdop/home/appbar/addpodcast.dart'; import 'package:tsacdop/home/appbar/addpodcast.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/importompl.dart'; import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/settingstate.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(); final SettingState themeSetting = SettingState();
Future main() async { Future main() async {

View File

@ -6,8 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/class/podcastlocal.dart';
@ -389,6 +389,14 @@ class _AboutPodcastState extends State<AboutPodcast> {
setState(() => _load = true); setState(() => _load = true);
} }
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -419,8 +427,15 @@ class _AboutPodcastState extends State<AboutPodcast> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Linkify(
_description, onOpen: (link) {
_launchUrl(link.url);
},
text: _description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, 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 { } else {
return SelectableText( return Linkify(
_description, text: _description,
toolbarOptions: ToolbarOptions(copy: true), onOpen: (link) {
_launchUrl(link.url);
},
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
); );
} }
}, },

View File

@ -301,50 +301,47 @@ class _PodcastCardState extends State<PodcastCard> {
Brightness.light Brightness.light
? Color.fromRGBO(113, 113, 113, 1) ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1), : Color.fromRGBO(15, 15, 15, 1),
statusBarColor: // statusBarColor:
Theme.of(context).brightness == // Theme.of(context).brightness ==
Brightness.light // Brightness.light
? Color.fromRGBO(113, 113, 113, 1) // ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1), // : Color.fromRGBO(5, 5, 5, 1),
), ),
child: SafeArea( child: AlertDialog(
child: AlertDialog( elevation: 1,
elevation: 1, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(
borderRadius: BorderRadius.all( Radius.circular(10.0))),
Radius.circular(10.0))), titlePadding: EdgeInsets.only(
titlePadding: EdgeInsets.only( top: 20,
top: 20, left: 20,
left: 20, right: 200,
right: 200, bottom: 20),
bottom: 20), title: Text('Remove confirm'),
title: Text('Remove confirm'), content: Text(
content: Text( 'Are you sure you want to unsubscribe?'),
'Are you sure you want to unsubscribe?'), actions: <Widget>[
actions: <Widget>[ FlatButton(
FlatButton( onPressed: () =>
onPressed: () => Navigator.of(context).pop(),
Navigator.of(context).pop(), child: Text(
child: Text( 'CANCEL',
'CANCEL', style: TextStyle(
style: TextStyle( color: Colors.grey[600]),
color: Colors.grey[600]),
),
), ),
FlatButton( ),
onPressed: () { FlatButton(
_groupList.removePodcast( onPressed: () {
widget.podcastLocal.id); _groupList.removePodcast(
Navigator.of(context).pop(); widget.podcastLocal.id);
}, Navigator.of(context).pop();
child: Text( },
'CONFIRM', child: Text(
style: 'CONFIRM',
TextStyle(color: Colors.red), style: TextStyle(color: Colors.red),
), ),
) )
], ],
),
), ),
), ),
); );

View File

@ -136,9 +136,9 @@ class _DownloadsManageState extends State<DownloadsManage> {
TextSpan( TextSpan(
text: _fileNum.toString(), text: _fileNum.toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
fontSize: 40, fontSize: 40,
fontWeight: FontWeight.bold)), )),
TextSpan( TextSpan(
text: _fileNum < 2 ? ' episode' : ' episodes ', text: _fileNum < 2 ? ' episode' : ' episodes ',
style: TextStyle( style: TextStyle(
@ -148,9 +148,9 @@ class _DownloadsManageState extends State<DownloadsManage> {
TextSpan( TextSpan(
text: (_size ~/ 1000000).toString(), text: (_size ~/ 1000000).toString(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
fontSize: 60, fontSize: 60,
fontWeight: FontWeight.bold)), )),
TextSpan( TextSpan(
text: ' Mb', text: ' Mb',
style: TextStyle( style: TextStyle(

View File

@ -59,8 +59,8 @@ class Settings extends StatelessWidget {
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
), ),
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), //physics: const AlwaysScrollableScrollPhysics(),
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -93,14 +93,16 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ThemeSetting())), builder: (context) => ThemeSetting())),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.adjust_solid), leading: Icon(LineIcons.adjust_solid),
title: Text('Appearance'), title: Text('Appearance'),
subtitle: Text('Colors and themes'), subtitle: Text('Colors and themes'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.play_circle), leading: Icon(LineIcons.play_circle),
title: Text('AutoPlay'), title: Text('AutoPlay'),
subtitle: Text('Autoplay next episode in playlist'), subtitle: Text('Autoplay next episode in playlist'),
@ -117,7 +119,8 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SyncingSetting())), builder: (context) => SyncingSetting())),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.cloud_download_alt_solid), leading: Icon(LineIcons.cloud_download_alt_solid),
title: Text('Syncing'), title: Text('Syncing'),
subtitle: Text('Refresh podcasts in the background'), subtitle: Text('Refresh podcasts in the background'),
@ -128,7 +131,8 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StorageSetting())), builder: (context) => StorageSetting())),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.save), leading: Icon(LineIcons.save),
title: Text('Storage'), title: Text('Storage'),
subtitle: Text('Manage cache and download storage'), subtitle: Text('Manage cache and download storage'),
@ -139,7 +143,8 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PlayedHistory())), builder: (context) => PlayedHistory())),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.update), leading: Icon(Icons.update),
title: Text('History'), title: Text('History'),
subtitle: Text('Listen data'), subtitle: Text('Listen data'),
@ -149,7 +154,8 @@ class Settings extends StatelessWidget {
onTap: () { onTap: () {
_exportOmpl(); _exportOmpl();
}, },
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.file_code_solid), leading: Icon(LineIcons.file_code_solid),
title: Text('Export'), title: Text('Export'),
subtitle: Text('Export ompl file'), subtitle: Text('Export ompl file'),
@ -184,25 +190,31 @@ class Settings extends StatelessWidget {
ListTile( ListTile(
onTap: () => _launchUrl( onTap: () => _launchUrl(
'https://github.com/stonega/tsacdop/releases'), 'https://github.com/stonega/tsacdop/releases'),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.map_signs_solid), leading: Icon(LineIcons.map_signs_solid),
title: Text('Changelog'), title: Text('Changelog'),
subtitle: Text('List of chagnes'), subtitle: Text('List of chagnes'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
onTap: () => Navigator.push(context, onTap: () => Navigator.push(
MaterialPageRoute(builder: (context) => Libries())), context,
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), MaterialPageRoute(
builder: (context) => Libries())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.book_open_solid), leading: Icon(LineIcons.book_open_solid),
title: Text('Libraries'), title: Text('Libraries'),
subtitle: Text('Open source libraries in application'), subtitle:
Text('Open source libraries in application'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
onTap: () => _launchUrl( onTap: () => _launchUrl(
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'), 'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.bug_solid), leading: Icon(LineIcons.bug_solid),
title: Text('Feedback'), title: Text('Feedback'),
subtitle: Text('Bugs and feature requests'), subtitle: Text('Bugs and feature requests'),

View File

@ -42,6 +42,7 @@ class StorageSetting extends StatelessWidget {
.copyWith(color: Theme.of(context).accentColor)), .copyWith(color: Theme.of(context).accentColor)),
), ),
ListView( ListView(
physics: const BouncingScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
children: <Widget>[ children: <Widget>[

View File

@ -48,6 +48,7 @@ class SyncingSetting extends StatelessWidget {
.copyWith(color: Theme.of(context).accentColor)), .copyWith(color: Theme.of(context).accentColor)),
), ),
ListView( ListView(
physics: const BouncingScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
children: <Widget>[ children: <Widget>[

View File

@ -43,6 +43,7 @@ class ThemeSetting extends StatelessWidget {
.copyWith(color: Theme.of(context).accentColor)), .copyWith(color: Theme.of(context).accentColor)),
), ),
ListView( ListView(
physics: const BouncingScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
children: <Widget>[ children: <Widget>[

View File

@ -31,7 +31,7 @@ class EpisodeGrid extends StatelessWidget {
this.heroTag, this.heroTag,
this.updateCount = 0}) this.updateCount = 0})
: super(key: key); : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = MediaQuery.of(context).size.width;
@ -46,7 +46,6 @@ class EpisodeGrid extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
context: context, context: context,
position: RelativeRect.fromLTRB(left, top, _width - left, 0), position: RelativeRect.fromLTRB(left, top, _width - left, 0),
items: <PopupMenuEntry<int>>[ items: <PopupMenuEntry<int>>[
PopupMenuItem( PopupMenuItem(
value: 0, value: 0,
@ -85,7 +84,7 @@ class EpisodeGrid extends StatelessWidget {
if (value == 0) { if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode); if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) { } else if (value == 1) {
if (!isInPlaylist) { if (!isInPlaylist) {
audio.addToPlaylist(episode); audio.addToPlaylist(episode);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Added to playlist', msg: 'Added to playlist',
@ -97,7 +96,7 @@ class EpisodeGrid extends StatelessWidget {
msg: 'Removed from playlist', msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
} }
}); });
} }
@ -187,7 +186,15 @@ class EpisodeGrid extends StatelessWidget {
), ),
), ),
Spacer(), 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 showNumber
? Container( ? Container(
alignment: Alignment.topRight, alignment: Alignment.topRight,
@ -371,7 +378,7 @@ class _DownloadIconState extends State<DownloadIcon> {
child: CircularProgressIndicator( child: CircularProgressIndicator(
backgroundColor: Colors.grey[200], backgroundColor: Colors.grey[200],
strokeWidth: 1, strokeWidth: 1,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).accentColor),
value: task.progress / 100, value: task.progress / 100,
), ),
); );
@ -391,7 +398,7 @@ class _DownloadIconState extends State<DownloadIcon> {
data: IconThemeData(size: 15), data: IconThemeData(size: 15),
child: Icon( child: Icon(
Icons.done_all, Icons.done_all,
color: Colors.blue, color: Theme.of(context).accentColor,
), ),
); );
} else if (task.status == DownloadTaskStatus.failed) { } else if (task.status == DownloadTaskStatus.failed) {

View File

@ -347,6 +347,8 @@ Future<T> _showMenu<T>({
break; break;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel; label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
} }
@ -469,6 +471,8 @@ class MyPopupMenuButtonState<T> extends State<MyPopupMenuButton<T>> {
return const Icon(Icons.more_vert); return const Icon(Icons.more_vert);
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return const Icon(Icons.more_horiz); return const Icon(Icons.more_horiz);
} }
return null; return null;

View File

@ -49,8 +49,7 @@ class RssItem {
} }
return RssItem( return RssItem(
title: findElementOrNull(element, "title")?.text, title: findElementOrNull(element, "title")?.text,
description: findElementOrNull(element, "description")?.text?.trim() description: findElementOrNull(element, "description")?.text?.trim(),
,
link: findElementOrNull(element, "link")?.text?.trim(), link: findElementOrNull(element, "link")?.text?.trim(),
categories: element.findElements("category").map((element) { categories: element.findElements("category").map((element) {
return RssCategory.parse(element); return RssCategory.parse(element);
@ -60,7 +59,7 @@ class RssItem {
author: findElementOrNull(element, "author")?.text?.trim(), author: findElementOrNull(element, "author")?.text?.trim(),
// comments: findElementOrNull(element, "comments")?.text, // comments: findElementOrNull(element, "comments")?.text,
// source: RssSource.parse(findElementOrNull(element, "source")), // source: RssSource.parse(findElementOrNull(element, "source")),
content: RssContent.parse(findElementOrNull(element, "content:encoded")), content: RssContent.parse(findElementOrNull(element, "content:encoded")),
// media: Media.parse(element), // media: Media.parse(element),
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
//dc: DublinCore.parse(element), //dc: DublinCore.parse(element),

View File

@ -28,7 +28,7 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
json_annotation: ^3.0.1 json_annotation: ^3.0.1
sqflite: ^1.2.2+1 sqflite: ^1.3.0
flutter_html: ^0.11.1 flutter_html: ^0.11.1
path_provider: ^1.6.1 path_provider: ^1.6.1
color_thief_flutter: ^1.0.1 color_thief_flutter: ^1.0.1
@ -59,6 +59,7 @@ dev_dependencies:
git: git:
url: https://github.com/galonsos/line_icons.git url: https://github.com/galonsos/line_icons.git
flutter_file_dialog: ^0.0.5 flutter_file_dialog: ^0.0.5
flutter_linkify: ^3.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the