diff --git a/.circleci/config.yml b/.circleci/config.yml index 69642c2..d84e8a6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: cirrusci/flutter:v1.14.6 + - image: cirrusci/flutter:v1.15.17 branches: only: master diff --git a/android/app/build.gradle b/android/app/build.gradle index 9914def..c1db363 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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' } diff --git a/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt b/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt index 615f563..a2ab3b0 100644 --- a/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt +++ b/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt @@ -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) + } + } + } } } diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png index 94ea1cd..9c381ed 100644 Binary files a/android/app/src/main/res/drawable-hdpi/ic_notification.png and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png index 1ca8158..50afe4d 100644 Binary files a/android/app/src/main/res/drawable-mdpi/ic_notification.png and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png index 690a999..0e45d26 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png index 6518358..ba5d65a 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png index 9e501f4..bafa892 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/android/build.gradle b/android/build.gradle index 232bc0d..09661db 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 296b146..c79d946 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/lib/class/settingstate.dart b/lib/class/settingstate.dart index 4d606f7..b0c17c4 100644 --- a/lib/class/settingstate.dart +++ b/lib/class/settingstate.dart @@ -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 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 { diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index 24b1261..59562b3 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -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 { 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?

(
)?

\s?'), ''); + _description = (await dbHelper.getDescription(url)) + .replaceAll(RegExp(r'\s?

(
)?

\s?'), '').replaceAll('\r', ''); if (mounted) setState(() { _loaddes = true; @@ -85,12 +88,11 @@ class _EpisodeDetailState extends State { 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 { 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 { 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 { 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(), ), ), diff --git a/lib/home/appbar/addpodcast.dart b/lib/home/appbar/addpodcast.dart index f9acd8f..0f8533c 100644 --- a/lib/home/appbar/addpodcast.dart +++ b/lib/home/appbar/addpodcast.dart @@ -32,6 +32,8 @@ class _MyHomePageState extends State { final _MyHomePageDelegate _delegate = _MyHomePageDelegate(); final GlobalKey _scaffoldKey = GlobalKey(); + var _androidAppRetain = MethodChannel("android_app_retain"); + @override void initState() { super.initState(); @@ -71,7 +73,16 @@ class _MyHomePageState extends State { PopupMenu(), ], ), - body: Home(), + body: WillPopScope( + onWillPop: () async { + if (Platform.isAndroid) { + _androidAppRetain.invokeMethod('sendToBackground'); + return false; + } else { + return true; + } + }, + child: Home()), ), ); } diff --git a/lib/home/home.dart b/lib/home/home.dart index cc2cf43..691d0dc 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -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 { diff --git a/lib/home/homescroll.dart b/lib/home/home_groups.dart similarity index 99% rename from lib/home/homescroll.dart rename to lib/home/home_groups.dart index e58d345..d7a49cc 100644 --- a/lib/home/homescroll.dart +++ b/lib/home/home_groups.dart @@ -40,7 +40,7 @@ class _ScrollPodcastsState extends State { bool isLoading = groupList.isLoading; return isLoading ? Container( - height: (_width - 20) / 3 + 110, + height: (_width - 20) / 3 + 140, ) : groups[_groupIndex].podcastList.length == 0 ? Column( diff --git a/lib/home/playlist.dart b/lib/home/playlist.dart index 6be4361..b643a9f 100644 --- a/lib/home/playlist.dart +++ b/lib/home/playlist.dart @@ -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 { final GlobalKey _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 { ); } + int _sumPlaylistLength(List 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(context, listen: false); @@ -42,8 +79,9 @@ class _PlaylistPageState extends State { 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 { 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: [ - 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: [ + 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: [ - 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: [ - (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( + 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: [ + 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: [ + 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: [ + (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, + ), + ], + ), + ), + ); + }), + ), + ], + ); }, ), ), diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 4ece98b..f7c54c8 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -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; diff --git a/lib/main.dart b/lib/main.dart index 4147eb6..ee2dad5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 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 { diff --git a/lib/podcasts/podcastdetail.dart b/lib/podcasts/podcastdetail.dart index 16b2e9c..5b1a83e 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -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 { 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 { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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 { ), ], ) - : 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), ); } }, diff --git a/lib/podcasts/podcastgroup.dart b/lib/podcasts/podcastgroup.dart index 15fc98d..c3ba3b3 100644 --- a/lib/podcasts/podcastgroup.dart +++ b/lib/podcasts/podcastgroup.dart @@ -301,50 +301,47 @@ class _PodcastCardState extends State { 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: [ - 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: [ + 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), + ), + ) + ], ), ), ); diff --git a/lib/settings/downloads_manage.dart b/lib/settings/downloads_manage.dart index fad7582..543e893 100644 --- a/lib/settings/downloads_manage.dart +++ b/lib/settings/downloads_manage.dart @@ -136,9 +136,9 @@ class _DownloadsManageState extends State { 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 { 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( diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index cdb3c38..2c45ec2 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -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:?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'), diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart index 5db52f4..fbc4ca1 100644 --- a/lib/settings/storage.dart +++ b/lib/settings/storage.dart @@ -42,6 +42,7 @@ class StorageSetting extends StatelessWidget { .copyWith(color: Theme.of(context).accentColor)), ), ListView( + physics: const BouncingScrollPhysics(), shrinkWrap: true, scrollDirection: Axis.vertical, children: [ diff --git a/lib/settings/syncing.dart b/lib/settings/syncing.dart index a0e1246..36441ac 100644 --- a/lib/settings/syncing.dart +++ b/lib/settings/syncing.dart @@ -48,6 +48,7 @@ class SyncingSetting extends StatelessWidget { .copyWith(color: Theme.of(context).accentColor)), ), ListView( + physics: const BouncingScrollPhysics(), shrinkWrap: true, scrollDirection: Axis.vertical, children: [ diff --git a/lib/settings/theme.dart b/lib/settings/theme.dart index 3fc58ba..23decd1 100644 --- a/lib/settings/theme.dart +++ b/lib/settings/theme.dart @@ -43,6 +43,7 @@ class ThemeSetting extends StatelessWidget { .copyWith(color: Theme.of(context).accentColor)), ), ListView( + physics: const BouncingScrollPhysics(), shrinkWrap: true, scrollDirection: Axis.vertical, children: [ diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index 40e677c..5b4f1dd 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -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: >[ 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 { child: CircularProgressIndicator( backgroundColor: Colors.grey[200], strokeWidth: 1, - valueColor: AlwaysStoppedAnimation(Colors.blue), + valueColor: AlwaysStoppedAnimation(Theme.of(context).accentColor), value: task.progress / 100, ), ); @@ -391,7 +398,7 @@ class _DownloadIconState extends State { data: IconThemeData(size: 15), child: Icon( Icons.done_all, - color: Colors.blue, + color: Theme.of(context).accentColor, ), ); } else if (task.status == DownloadTaskStatus.failed) { diff --git a/lib/util/mypopupmenu.dart b/lib/util/mypopupmenu.dart index 25bcc87..dfdc6ec 100644 --- a/lib/util/mypopupmenu.dart +++ b/lib/util/mypopupmenu.dart @@ -347,6 +347,8 @@ Future _showMenu({ 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 extends State> { 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; diff --git a/lib/webfeed/domain/rss_item.dart b/lib/webfeed/domain/rss_item.dart index 75aed0e..e96428c 100644 --- a/lib/webfeed/domain/rss_item.dart +++ b/lib/webfeed/domain/rss_item.dart @@ -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), diff --git a/pubspec.yaml b/pubspec.yaml index 41ffc58..aa681ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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