Add share clip feature
This commit is contained in:
parent
2f9af80c9c
commit
6ebe9a4a3c
11
README.md
11
README.md
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
];
|
];
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue