Add share clip feature

This commit is contained in:
stonegate 2020-05-19 01:03:45 +08:00
parent 2f9af80c9c
commit 6ebe9a4a3c
12 changed files with 641 additions and 13 deletions

View File

@ -22,12 +22,13 @@ The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
## Features ## Features
* Subscriptoin group management * Subscriptoin group management
* Playlist support * Playlist support
* Sleep timer * Sleep timer / Speed setting
* OMPL file export and import * OMPL file export and import
* Auto syncing in background * Auto syncing in background
* Listen and subscribe history record * Listen and subscribe history record
* Dark mode / Accent color personalization * Dark mode / Accent color
* Download for offline playing * Download for offline playing
* Share clip on twitter
More to come... More to come...
@ -46,12 +47,14 @@ Tsacdop is using ListenNotes api 1.0 pro to search podcast, which is not free. S
If you want to build the app, you need to create a new file named .env.dart in lib folder. Add below code in .env.dart. If you want to build the app, you need to create a new file named .env.dart in lib folder. Add below code in .env.dart.
``` ```
final environment = {"apiKey":"APIKEY"}; final environment = {"apiKey":"APIKEY", "shareKey":"SHAREKEY"};
``` ```
You can get own api key on [RapidApi](https://rapidapi.com/listennotes/api/listennotes), basic plan is free to all, and replace "APIKEY" with it. You can get own api key on [RapidApi](https://rapidapi.com/listennotes/api/listennotes), basic plan is free to all, and replace "APIKEY" with it.
If no api key added, the search function in the app won't work. But you can still add podcasts by serach rss link or import ompl file. If no api key added, the search function in the app won't work. But you can still add podcasts by serach rss link or import ompl file.
Share_key is used for generate clip.
## Getting Started ## Getting Started
This project is a starting point for a Flutter application. This project is a starting point for a Flutter application.

View File

@ -47,7 +47,7 @@ android {
applicationId "com.stonegate.tsacdop" applicationId "com.stonegate.tsacdop"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 28 targetSdkVersion 28
versionCode 11 versionCode 12
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -4,7 +4,7 @@ import 'package:tsacdop/util/custompaint.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
const String version = '0.2.3'; const String version = '0.2.4';
class AboutApp extends StatelessWidget { class AboutApp extends StatelessWidget {
_launchUrl(String url) async { _launchUrl(String url) async {

View File

@ -19,6 +19,7 @@ import '../util/customslider.dart';
import '../episodes/episodedetail.dart'; import '../episodes/episodedetail.dart';
import 'playlist.dart'; import 'playlist.dart';
import 'audiopanel.dart'; import 'audiopanel.dart';
import 'share.dart';
final List<BoxShadow> _customShadow = [ final List<BoxShadow> _customShadow = [
BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white), BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white),
@ -54,7 +55,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
return Container( return Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
height: 300, height: 300,
width: MediaQuery.of(context).size.width, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
@ -305,7 +306,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Widget _expandedPanel(BuildContext context) { Widget _expandedPanel(BuildContext context) {
return DefaultTabController( return DefaultTabController(
initialIndex: 1, initialIndex: 1,
length: 3, length: 4,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
TabBarView( TabBarView(
@ -313,6 +314,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
SleepMode(), SleepMode(),
ControlPanel(), ControlPanel(),
_playlist(context), _playlist(context),
ShareClip(),
], ],
), ),
Positioned( Positioned(
@ -364,6 +366,16 @@ class _PlayerWidgetState extends State<PlayerWidget> {
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
width: 2.0)), width: 2.0)),
), ),
Container(
height: 8.0,
width: 8.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
border: Border.all(
color: Theme.of(context).accentColor,
width: 2.0)),
),
]), ]),
), ),
), ),

69
lib/home/preview.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import '../util/context_extension.dart';
class ClipPreview extends StatefulWidget {
final String filePath;
ClipPreview({this.filePath, Key key}) : super(key: key);
@override
_ClipPreviewState createState() => _ClipPreviewState();
}
class _ClipPreviewState extends State<ClipPreview> {
VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.file(File(widget.filePath))
..initialize().then((_) {
setState(() {});
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: Colors.black,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
appBar: AppBar(backgroundColor: Colors.black),
backgroundColor: Colors.black,
body: Center(
child: _controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Container(),
),
floatingActionButton: FloatingActionButton(
backgroundColor: context.accentColor,
onPressed: () {
setState(() {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
});
},
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
),
);
}
}

469
lib/home/share.dart Normal file
View File

@ -0,0 +1,469 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/home/preview.dart';
import 'package:wc_flutter_share/wc_flutter_share.dart';
import 'package:tuple/tuple.dart';
import '../state/audiostate.dart';
import '../util/context_extension.dart';
import '../util/customslider.dart';
import '../util/pageroute.dart';
final List<BoxShadow> _customShadow = [
BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white),
BoxShadow(
blurRadius: 8,
offset: Offset(2, 2),
color: Colors.grey[600].withOpacity(0.4))
];
final List<BoxShadow> _customShadowNight = [
BoxShadow(
blurRadius: 6,
offset: Offset(-1, -1),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(blurRadius: 8, offset: Offset(2, 2), color: Colors.black)
];
String _stringForSeconds(int seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
class ShareClip extends StatefulWidget {
ShareClip({Key key}) : super(key: key);
@override
_ShareClipState createState() => _ShareClipState();
}
class _ShareClipState extends State<ShareClip> {
int _durationSelected;
int _startPosition;
bool _startConfirm;
List<int> _durationToSelect = [30, 60, 90, 120];
Widget _animatedWidget;
Widget _toastWidget;
@override
void initState() {
super.initState();
_durationSelected = 60;
_startPosition = 0;
_startConfirm = false;
_animatedWidget = Center();
_toastWidget = Center();
}
_formatSeconds(int s) {
switch (s) {
case 30:
return "30sec";
break;
case 60:
return "1min";
break;
case 90:
return "90sec";
break;
case 120:
return "2min";
break;
default:
return '';
break;
}
}
_setShareButton(ShareStatus status, String filePath) {
switch (status) {
case ShareStatus.generate:
_animatedWidget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
)),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
Text('Clipping'),
],
);
_toastWidget = Text('May take one minute',
style: TextStyle(color: const Color(0xff67727d)));
break;
case ShareStatus.download:
_animatedWidget = Text('Loading');
break;
case ShareStatus.complete:
_animatedWidget = Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.share),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
Text('Share'),
],
);
_toastWidget = Row(
children: [
Text('Preview'),
IconButton(
icon: Icon(LineIcons.play_solid),
onPressed: () {
if (filePath != '')
Navigator.push(
context,
SlideLeftRoute(
page: ClipPreview(
filePath: filePath,
)),
);
}),
],
);
break;
case ShareStatus.undefined:
_animatedWidget = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LineIcons.cut_solid),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
Text('Clip')
],
);
_toastWidget = Center();
break;
case ShareStatus.error:
_animatedWidget = Text('Retry');
_toastWidget = Text('Something wrong happened');
break;
}
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
height: 300,
width: double.infinity,
color: context.primaryColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 60.0,
// color: context.primaryColorDark,
alignment: Alignment.centerLeft,
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 20.0,
// color: context.primaryColorDark,
alignment: Alignment.centerLeft,
child: Text(
'Share Clip',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
fontSize: 16),
),
),
Spacer(),
Selector<AudioPlayerNotifier, Tuple2<ShareStatus, String>>(
selector: (_, audio) =>
Tuple2(audio.shareStatus, audio.shareFile ?? ''),
builder: (_, data, __) {
_setShareButton(data.item1, data.item2);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
transitionBuilder: (child, animation) =>
ScaleTransition(scale: animation, child: child),
duration: Duration(milliseconds: 500),
child: _toastWidget),
Container(
margin: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
height: 40,
width: 120,
decoration: BoxDecoration(
color: context.primaryColor,
borderRadius:
BorderRadius.all(Radius.circular(20)),
border: Border.all(
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.black12
: Colors.white10,
width: 1),
boxShadow: Theme.of(context).brightness ==
Brightness.dark
? _customShadowNight
: _customShadow),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(15)),
onTap: () async {
if (data.item1 == ShareStatus.undefined ||
data.item1 ==
ShareStatus.error) if (_startConfirm)
audio.shareClip(
_startPosition, _durationSelected);
else
Fluttertoast.showToast(
msg: 'Please confirm start position',
gravity: ToastGravity.BOTTOM,
);
else if (data.item1 == ShareStatus.complete) {
File file = File(data.item2);
final Uint8List bytes =
await file.readAsBytes();
await WcFlutterShare.share(
sharePopupTitle: 'share Clip',
fileName: data.item2.split('/').last,
mimeType: 'video/mp4',
bytesOfFile: bytes.buffer.asUint8List());
audio.setShareStatue = ShareStatus.undefined;
}
},
child: SizedBox(
height: 40,
width: 100,
child: Center(
child: AnimatedSwitcher(
transitionBuilder: (child, animation) =>
ScaleTransition(
scale: animation, child: child),
duration: Duration(milliseconds: 700),
child: _animatedWidget)),
),
),
),
),
],
);
},
),
],
),
),
Consumer<AudioPlayerNotifier>(builder: (_, data, __) {
return Container(
padding: EdgeInsets.only(top: 5, left: 10, right: 10),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor:
Theme.of(context).brightness == Brightness.dark
? Colors.black38
: Colors.grey[400],
inactiveTrackColor: Theme.of(context).primaryColorDark,
trackHeight: 20.0,
trackShape: MyRectangularTrackShape(),
thumbColor: Theme.of(context).accentColor,
thumbShape: MyRoundSliderThumpShape(
enabledThumbRadius: 10.0,
disabledThumbRadius: 10.0,
thumbCenterColor: context.accentColor),
overlayColor: Theme.of(context).accentColor.withAlpha(32),
overlayShape: RoundSliderOverlayShape(overlayRadius: 4.0),
),
child: Slider(
value: data.seekSliderValue,
onChanged: (double val) {
audio.sliderSeek(val);
}),
),
);
}),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 150,
width: 200,
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Center(
child: Selector<AudioPlayerNotifier, int>(
selector: (_, audio) => audio.backgroundAudioPosition,
builder: (_, position, __) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Text.rich(
TextSpan(
text: 'Start at \n',
style:
TextStyle(color: context.accentColor),
children: [
TextSpan(
text: !_startConfirm
? _stringForSeconds(
position ~/ 1000)
: _stringForSeconds(
_startPosition),
style: context.textTheme.headline5
.copyWith(
color: _startConfirm
? context.accentColor
: null)),
]),
textAlign: TextAlign.center,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.symmetric(
horizontal: 10.0),
onPressed: () => audio.forwardAudio(-30),
iconSize: 25.0,
icon: Icon(Icons.replay_30),
color: Colors.grey[500]),
InkWell(
onTap: () => setState(() {
if (!_startConfirm)
_startPosition = position ~/ 1000;
_startConfirm = !_startConfirm;
}),
child: Container(
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
boxShadow: !_startConfirm
? (context.brightness ==
Brightness.dark)
? _customShadowNight
: _customShadow
: null,
color: _startConfirm
? Theme.of(context).accentColor
: Theme.of(context).primaryColor,
shape: BoxShape.circle),
alignment: Alignment.center,
width: 40,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
LineIcons.thumbtack_solid,
color: _startConfirm
? Colors.white
: null,
)),
),
),
IconButton(
padding: EdgeInsets.symmetric(
horizontal: 10.0),
onPressed: () => audio.forwardAudio(10),
iconSize: 25.0,
icon: Icon(Icons.forward_10),
color: Colors.grey[500]),
],
),
],
);
},
),
),
),
),
Container(
height: 100,
width: 2,
color: context.primaryColorDark,
),
SizedBox(
height: 150,
width: 200,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Center(
child: Wrap(
direction: Axis.horizontal,
children: _durationToSelect
.map<Widget>((e) => InkWell(
onTap: () =>
setState(() => _durationSelected = e),
child: Container(
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(15)),
boxShadow: e != _durationSelected
? (context.brightness ==
Brightness.dark)
? _customShadowNight
: _customShadow
: null,
color: (e == _durationSelected)
? Theme.of(context).accentColor
: Theme.of(context).primaryColor,
),
alignment: Alignment.center,
width: 70,
height: 30,
child: Text(_formatSeconds(e),
style: TextStyle(
fontWeight: FontWeight.bold,
color: (e == _durationSelected)
? Colors.white
: null)),
),
))
.toList(),
),
)),
),
],
),
),
Container(
height: 20,
padding: EdgeInsets.symmetric(horizontal: 20),
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
onTap: () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('experimental'),
Icon(
LineIcons.info_circle_solid,
size: 20.0,
color: context.accentColor,
),
],
),
),
),
],
),
);
}
}

View File

@ -1039,4 +1039,14 @@ class DBHelper {
return episode; return episode;
} }
} }
Future<String> getImageUrl(String url) async{
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"""SELECT P.imageUrl FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]);
if(list.length ==0)
return null;
return list.first["imageUrl"];
}
} }

View File

@ -68,4 +68,5 @@ List<Libries> plugins = [
Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart'), Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart'),
Libries('flutter_isolate', mit, 'https://pub.dev/packages/flutter_isolate'), Libries('flutter_isolate', mit, 'https://pub.dev/packages/flutter_isolate'),
Libries('auto_animated', mit, 'https://pub.dev/packages/auto_animated'), Libries('auto_animated', mit, 'https://pub.dev/packages/auto_animated'),
Libries('wc_flutter_share', apacheLicense, 'https://pub.dev/packages/wc_flutter_share')
]; ];

View File

@ -1,12 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:path/path.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
import '../.env.dart';
MediaControl playControl = MediaControl( MediaControl playControl = MediaControl(
androidIcon: 'drawable/ic_stat_play_circle_filled', androidIcon: 'drawable/ic_stat_play_circle_filled',
@ -110,6 +117,7 @@ class Playlist {
} }
enum SleepTimerMode { endOfEpisode, timer, undefined } enum SleepTimerMode { endOfEpisode, timer, undefined }
enum ShareStatus { generate, download, complete, undefined, error }
class AudioPlayerNotifier extends ChangeNotifier { class AudioPlayerNotifier extends ChangeNotifier {
DBHelper dbHelper = DBHelper(); DBHelper dbHelper = DBHelper();
@ -135,6 +143,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool _startSleepTimer = false; bool _startSleepTimer = false;
double _switchValue = 0; double _switchValue = 0;
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined; SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
ShareStatus _shareStatus = ShareStatus.undefined;
String _shareFile = '';
//set autoplay episode in playlist //set autoplay episode in playlist
bool _autoPlay = true; bool _autoPlay = true;
//TODO Set auto add episodes to playlist //TODO Set auto add episodes to playlist
@ -158,6 +168,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool get stopOnComplete => _stopOnComplete; bool get stopOnComplete => _stopOnComplete;
bool get startSleepTimer => _startSleepTimer; bool get startSleepTimer => _startSleepTimer;
SleepTimerMode get sleepTimerMode => _sleepTimerMode; SleepTimerMode get sleepTimerMode => _sleepTimerMode;
ShareStatus get shareStatus => _shareStatus;
String get shareFile => _shareFile;
bool get autoPlay => _autoPlay; bool get autoPlay => _autoPlay;
int get timeLeft => _timeLeft; int get timeLeft => _timeLeft;
double get switchValue => _switchValue; double get switchValue => _switchValue;
@ -174,6 +186,11 @@ class AudioPlayerNotifier extends ChangeNotifier {
_setAutoPlay(); _setAutoPlay();
} }
set setShareStatue(ShareStatus status) {
_shareStatus = status;
notifyListeners();
}
Future _getAutoPlay() async { Future _getAutoPlay() async {
int i = await autoPlayStorage.getInt(); int i = await autoPlayStorage.getInt();
_autoPlay = i == 0 ? true : false; _autoPlay = i == 0 ? true : false;
@ -556,6 +573,48 @@ class AudioPlayerNotifier extends ChangeNotifier {
} }
} }
shareClip(int start, int duration) async {
_shareStatus = ShareStatus.generate;
notifyListeners();
int length = math.min(duration, (_backgroundAudioDuration ~/ 1000 - start));
final BaseOptions options = BaseOptions(
connectTimeout: 60000,
receiveTimeout: 120000,
);
String imageUrl = await dbHelper.getImageUrl(_episode.enclosureUrl);
String url = "https://podcastapi.stonegate.me/clip?" +
"audio_link=${_episode.enclosureUrl}&image_link=$imageUrl&title=${_episode.feedTitle}" +
"&text=${_episode.title}&start=$start&length=$length";
String shareKey = environment['shareKey'];
try {
Response response = await Dio(options).get(url,
options: Options(headers: {
'X-Share-Key': "$shareKey",
}));
String shareLink = response.data;
print(shareLink);
String fileName = _episode.title + start.toString() + '.mp4';
_shareStatus = ShareStatus.download;
notifyListeners();
Directory dir = await getTemporaryDirectory();
String shareDir = join(dir.path, 'share', fileName);
try {
await Dio().download(shareLink, shareDir);
_shareFile = shareDir;
_shareStatus = ShareStatus.complete;
notifyListeners();
} on DioError catch (e) {
print(e);
_shareStatus = ShareStatus.error;
notifyListeners();
}
} catch (e) {
print(e);
_shareStatus = ShareStatus.error;
notifyListeners();
}
}
@override @override
void dispose() async { void dispose() async {
await AudioService.stop(); await AudioService.stop();

View File

@ -46,16 +46,18 @@ class MyRoundSliderThumpShape extends SliderComponentShape {
@required SliderThemeData sliderTheme, @required SliderThemeData sliderTheme,
TextDirection textDirection, TextDirection textDirection,
double value, double value,
double textScaleFactor,
Size sizeWithOverflow,
}) { }) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>( final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius, begin: _disabledThumbRadius,
end: enabledThumbRadius, end: enabledThumbRadius,
); );
// final ColorTween colorTween = ColorTween( final ColorTween colorTween = ColorTween(
// begin: sliderTheme.disabledThumbColor, begin: sliderTheme.disabledThumbColor,
// end: sliderTheme.thumbColor, end: sliderTheme.thumbColor,
// ); );
canvas.drawCircle( canvas.drawCircle(
center, center,

View File

@ -1,7 +1,7 @@
name: tsacdop name: tsacdop
description: An easy-use podacasts player. description: An easy-use podacasts player.
version: 0.2.3 version: 0.2.4
environment: environment:
sdk: ">=2.6.0 <3.0.0" sdk: ">=2.6.0 <3.0.0"
@ -47,6 +47,8 @@ dev_dependencies:
flare_flutter: ^2.0.3 flare_flutter: ^2.0.3
rxdart: ^0.24.0 rxdart: ^0.24.0
auto_animated: ^2.1.0 auto_animated: ^2.1.0
wc_flutter_share: ^0.2.1
video_player: ^0.10.11
just_audio: just_audio:
git: git:
url: https://github.com/stonega/just_audio.git url: https://github.com/stonega/just_audio.git

View File

@ -4,6 +4,7 @@ import 'dart:io';
Future<void> main() async { Future<void> main() async {
final config = { final config = {
'apiKey': Platform.environment['API_KEY'], 'apiKey': Platform.environment['API_KEY'],
'shareKey': Platform.environment['SHARE_KEY']
}; };
final filename = 'lib/.env.dart'; final filename = 'lib/.env.dart';