1513 lines
70 KiB
Dart
1513 lines
70 KiB
Dart
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:line_icons/line_icons.dart';
|
|
import 'package:marquee/marquee.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
import '../episodes/episode_detail.dart';
|
|
import '../local_storage/key_value_storage.dart';
|
|
import '../local_storage/sqflite_localpodcast.dart';
|
|
import '../state/audio_state.dart';
|
|
import '../type/episodebrief.dart';
|
|
import '../type/play_histroy.dart';
|
|
import '../util/audiopanel.dart';
|
|
import '../util/custom_slider.dart';
|
|
import '../util/custom_widget.dart';
|
|
import '../util/extension_helper.dart';
|
|
import '../util/pageroute.dart';
|
|
import 'playlist.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)
|
|
];
|
|
|
|
const List kMinsToSelect = [10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99];
|
|
const List kMinPlayerHeight = <double>[70.0, 75.0, 80.0];
|
|
const List kMaxPlayerHeight = <double>[300.0, 325.0, 350.0];
|
|
|
|
class PlayerWidget extends StatelessWidget {
|
|
PlayerWidget({this.playerKey, this.isPlayingPage = false});
|
|
final GlobalKey<AudioPanelState> playerKey;
|
|
final bool isPlayingPage;
|
|
Widget _miniPanel(BuildContext context) {
|
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
final s = context.s;
|
|
return Container(
|
|
color: context.primaryColor,
|
|
height: 60,
|
|
child:
|
|
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
|
Selector<AudioPlayerNotifier, Tuple2<String, double>>(
|
|
selector: (_, audio) =>
|
|
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
|
|
builder: (_, data, __) {
|
|
final c = context.brightness == Brightness.light
|
|
? data.item1.colorizedark()
|
|
: data.item1.colorizeLight();
|
|
return SizedBox(
|
|
height: 2,
|
|
child: LinearProgressIndicator(
|
|
value: data.item2,
|
|
backgroundColor: context.primaryColor,
|
|
valueColor: AlwaysStoppedAnimation<Color>(c),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: <Widget>[
|
|
Expanded(
|
|
flex: 4,
|
|
child: Selector<AudioPlayerNotifier, String>(
|
|
selector: (_, audio) => audio.episode?.title,
|
|
builder: (_, title, __) {
|
|
return Text(
|
|
title,
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.clip,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Selector<AudioPlayerNotifier,
|
|
Tuple3<bool, double, String>>(
|
|
selector: (_, audio) => Tuple3(
|
|
audio.buffering,
|
|
(audio.backgroundAudioDuration -
|
|
audio.backgroundAudioPosition) /
|
|
1000,
|
|
audio.remoteErrorMessage),
|
|
builder: (_, data, __) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
child: data.item3 != null
|
|
? Text(data.item3,
|
|
style:
|
|
const TextStyle(color: Color(0xFFFF0000)))
|
|
: data.item1
|
|
? Text(
|
|
s.buffering,
|
|
style:
|
|
TextStyle(color: context.accentColor),
|
|
)
|
|
: Text(
|
|
s.timeLeft(
|
|
(data.item2).toInt().toTime ?? ''),
|
|
maxLines: 2,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Selector<AudioPlayerNotifier,
|
|
Tuple3<bool, bool, EpisodeBrief>>(
|
|
selector: (_, audio) =>
|
|
Tuple3(audio.buffering, audio.playing, audio.episode),
|
|
builder: (_, data, __) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
data.item1
|
|
? Stack(
|
|
alignment: Alignment.center,
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 10.0),
|
|
child: SizedBox(
|
|
height: 30.0,
|
|
width: 30.0,
|
|
child: CircleAvatar(
|
|
backgroundColor: data.item3
|
|
.backgroudColor(context),
|
|
backgroundImage:
|
|
data.item3.avatarImage,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: 40.0,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.black),
|
|
),
|
|
])
|
|
: data.item2
|
|
? InkWell(
|
|
onTap: data.item2
|
|
? () => audio.pauseAduio()
|
|
: null,
|
|
child:
|
|
ImageRotate(episodeItem: data.item3),
|
|
)
|
|
: InkWell(
|
|
onTap: data.item2
|
|
? null
|
|
: () => audio.resumeAudio(),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 10.0),
|
|
child: SizedBox(
|
|
height: 30.0,
|
|
width: 30.0,
|
|
child: CircleAvatar(
|
|
backgroundColor: data.item3
|
|
.backgroudColor(context),
|
|
backgroundImage:
|
|
data.item3.avatarImage,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: 40.0,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.black),
|
|
),
|
|
if (!data.item1)
|
|
Icon(
|
|
Icons.play_arrow,
|
|
color: Colors.white,
|
|
)
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => audio.playNext(),
|
|
iconSize: 20.0,
|
|
icon: Icon(Icons.skip_next),
|
|
color: context.textColor)
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Selector<AudioPlayerNotifier, Tuple2<bool, PlayerHeight>>(
|
|
selector: (_, audio) => Tuple2(audio.playerRunning, audio?.playerHeight),
|
|
builder: (_, data, __) {
|
|
if (!data.item1) {
|
|
return Center();
|
|
} else {
|
|
var minHeight = kMinPlayerHeight[data.item2.index];
|
|
var maxHeight = kMaxPlayerHeight[data.item2.index];
|
|
return AudioPanel(
|
|
minHeight: minHeight,
|
|
maxHeight: maxHeight,
|
|
key: playerKey,
|
|
miniPanel: _miniPanel(context),
|
|
expandedPanel: ControlPanel(
|
|
maxHeight: maxHeight,
|
|
isPlayingPage: isPlayingPage,
|
|
onExpand: () {
|
|
playerKey.currentState.scrollToTop();
|
|
},
|
|
onClose: () {
|
|
playerKey.currentState.backToMini();
|
|
},
|
|
));
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class LastPosition extends StatelessWidget {
|
|
LastPosition({Key key}) : super(key: key);
|
|
|
|
Future<PlayHistory> getPosition(EpisodeBrief episode) async {
|
|
var dbHelper = DBHelper();
|
|
return await dbHelper.getPosition(episode);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final s = context.s;
|
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
return Selector<AudioPlayerNotifier, EpisodeBrief>(
|
|
selector: (_, audio) => audio.episode,
|
|
builder: (context, episode, child) {
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Selector<AudioPlayerNotifier, bool>(
|
|
selector: (_, audio) => audio.skipSilence,
|
|
builder: (_, data, __) => FlatButton(
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.flash_on, size: 18),
|
|
SizedBox(width: 5),
|
|
Text(s.skipSilence),
|
|
],
|
|
),
|
|
color: data ? context.accentColor : Colors.transparent,
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(100.0),
|
|
side: BorderSide(
|
|
color: data
|
|
? context.accentColor
|
|
: Theme.of(context)
|
|
.colorScheme
|
|
.onSurface
|
|
.withOpacity(0.12))),
|
|
textColor: data ? Colors.white : null,
|
|
onPressed: () =>
|
|
audio.setSkipSilence(skipSilence: !data))),
|
|
SizedBox(width: 10),
|
|
Selector<AudioPlayerNotifier, bool>(
|
|
selector: (_, audio) => audio.boostVolume,
|
|
builder: (_, data, __) => FlatButton(
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.volume_up, size: 18),
|
|
SizedBox(width: 5),
|
|
Text(s.boostVolume),
|
|
],
|
|
),
|
|
color: data ? context.accentColor : Colors.transparent,
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(100.0),
|
|
side: BorderSide(
|
|
color: data
|
|
? context.accentColor
|
|
: Theme.of(context)
|
|
.colorScheme
|
|
.onSurface
|
|
.withOpacity(0.12))),
|
|
textColor: data ? Colors.white : null,
|
|
onPressed: () =>
|
|
audio.setBoostVolume(boostVolume: !data))),
|
|
SizedBox(width: 10),
|
|
FutureBuilder<PlayHistory>(
|
|
future: getPosition(episode),
|
|
builder: (context, snapshot) {
|
|
return snapshot.hasData
|
|
? snapshot.data.seekValue > 0.90
|
|
? Container(
|
|
height: 20,
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CustomPaint(
|
|
painter: ListenedAllPainter(
|
|
context.accentColor,
|
|
stroke: 2.0),
|
|
),
|
|
),
|
|
)
|
|
: snapshot.data.seconds < 10
|
|
? Center()
|
|
: OutlineButton(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius:
|
|
BorderRadius.circular(100.0),
|
|
side: BorderSide(
|
|
color: context.accentColor)),
|
|
highlightedBorderColor: Colors.green[700],
|
|
onPressed: () => audio.seekTo(
|
|
(snapshot.data.seconds * 1000).toInt()),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CustomPaint(
|
|
painter: ListenedPainter(
|
|
context.textColor,
|
|
stroke: 2.0),
|
|
),
|
|
),
|
|
SizedBox(width: 5),
|
|
Text(snapshot.data.seconds.toTime),
|
|
],
|
|
),
|
|
)
|
|
: Center();
|
|
}),
|
|
Selector<AudioPlayerNotifier, double>(
|
|
selector: (_, audio) => audio.switchValue,
|
|
builder: (_, data, __) => data == 1
|
|
? Container(
|
|
height: 20,
|
|
width: 40,
|
|
child: Transform.rotate(
|
|
angle: math.pi * 0.7,
|
|
child: Icon(Icons.brightness_2,
|
|
size: 18, color: context.accentColor)))
|
|
: Center(),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlaylistWidget extends StatefulWidget {
|
|
const PlaylistWidget({Key key}) : super(key: key);
|
|
|
|
@override
|
|
_PlaylistWidgetState createState() => _PlaylistWidgetState();
|
|
}
|
|
|
|
class _PlaylistWidgetState extends State<PlaylistWidget> {
|
|
final GlobalKey<AnimatedListState> miniPlaylistKey = GlobalKey();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
return Container(
|
|
alignment: Alignment.topLeft,
|
|
height: 300,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: context.accentColor.withAlpha(70),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Column(
|
|
children: <Widget>[
|
|
Expanded(
|
|
child:
|
|
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>(
|
|
selector: (_, audio) =>
|
|
Tuple2(audio.queue.playlist, audio.queueUpdate),
|
|
builder: (_, data, __) {
|
|
var episodesToPlay = data.item1.sublist(1);
|
|
return AnimatedList(
|
|
key: miniPlaylistKey,
|
|
shrinkWrap: true,
|
|
scrollDirection: Axis.vertical,
|
|
initialItemCount: episodesToPlay.length,
|
|
itemBuilder: (context, index, animation) => ScaleTransition(
|
|
alignment: Alignment.center,
|
|
scale: animation,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Row(
|
|
children: <Widget>[
|
|
Expanded(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () {
|
|
audio.episodeLoad(data.item1[index]);
|
|
miniPlaylistKey.currentState.removeItem(
|
|
index,
|
|
(context, animation) => Center());
|
|
},
|
|
child: Container(
|
|
height: 60,
|
|
padding:
|
|
EdgeInsets.symmetric(horizontal: 20),
|
|
alignment: Alignment.centerLeft,
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.center,
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Container(
|
|
padding: EdgeInsets.all(10.0),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(15.0)),
|
|
child: Container(
|
|
height: 30.0,
|
|
width: 30.0,
|
|
child: Image.file(File(
|
|
"${episodesToPlay[index].imagePath}"))),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
episodesToPlay[index].title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Material(
|
|
borderRadius: BorderRadius.circular(100),
|
|
clipBehavior: Clip.hardEdge,
|
|
color: context.primaryColor,
|
|
child: InkWell(
|
|
borderRadius:
|
|
BorderRadius.all(Radius.circular(15.0)),
|
|
onTap: () async {
|
|
var episdoe =
|
|
episodesToPlay.removeAt(index);
|
|
episodesToPlay.insert(0, episdoe);
|
|
miniPlaylistKey.currentState.removeItem(
|
|
index,
|
|
(context, animation) {
|
|
return Center();
|
|
},
|
|
duration: Duration.zero,
|
|
);
|
|
miniPlaylistKey.currentState.insertItem(0,
|
|
duration: Duration(milliseconds: 100));
|
|
await Future.delayed(
|
|
Duration(milliseconds: 100));
|
|
await audio
|
|
.moveToTop(data.item1[index + 1]);
|
|
},
|
|
child: SizedBox(
|
|
height: 30.0,
|
|
width: 30.0,
|
|
child: Transform.rotate(
|
|
angle: math.pi,
|
|
child: Icon(
|
|
LineIcons.download_solid,
|
|
size: 20.0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Divider(height: 1),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 60.0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Row(
|
|
children: <Widget>[
|
|
Text(
|
|
context.s.homeMenuPlaylist,
|
|
style: TextStyle(
|
|
color: context.accentColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16),
|
|
),
|
|
Spacer(),
|
|
Material(
|
|
borderRadius: BorderRadius.circular(100),
|
|
color: context.primaryColor,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.all(Radius.circular(15)),
|
|
onTap: () {
|
|
audio.playNext();
|
|
miniPlaylistKey.currentState
|
|
.removeItem(0, (context, animation) => Container());
|
|
miniPlaylistKey.currentState.insertItem(0);
|
|
},
|
|
child: SizedBox(
|
|
height: 30,
|
|
width: 60,
|
|
child: Icon(
|
|
Icons.skip_next,
|
|
size: 30,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 20),
|
|
Material(
|
|
borderRadius: BorderRadius.circular(100),
|
|
color: context.primaryColor,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
SlideLeftRoute(
|
|
page: PlaylistPage(
|
|
initPage: InitPage.playlist,
|
|
)),
|
|
);
|
|
},
|
|
child: SizedBox(
|
|
height: 30.0,
|
|
width: 30.0,
|
|
child: Transform.rotate(
|
|
angle: math.pi,
|
|
child: Icon(
|
|
LineIcons.database_solid,
|
|
size: 20.0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SleepMode extends StatefulWidget {
|
|
SleepMode({Key key}) : super(key: key);
|
|
|
|
@override
|
|
SleepModeState createState() => SleepModeState();
|
|
}
|
|
|
|
class SleepModeState extends State<SleepMode>
|
|
with SingleTickerProviderStateMixin {
|
|
int _minSelected;
|
|
AnimationController _controller;
|
|
Animation<double> _animation;
|
|
Future _getDefaultTime() async {
|
|
var defaultSleepTimerStorage = KeyValueStorage(defaultSleepTimerKey);
|
|
var defaultTime = await defaultSleepTimerStorage.getInt(defaultValue: 30);
|
|
setState(() => _minSelected = defaultTime);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_minSelected = 30;
|
|
_getDefaultTime();
|
|
|
|
_controller =
|
|
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
|
|
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
|
|
..addListener(() {
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
|
|
_controller.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
Provider.of<AudioPlayerNotifier>(context, listen: false)
|
|
..sleepTimer(_minSelected)
|
|
..setSwitchValue = 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
List<BoxShadow> customShadow(double scale) => [
|
|
BoxShadow(
|
|
blurRadius: 26 * (1 - scale),
|
|
offset: Offset(-6, -6) * (1 - scale),
|
|
color: Colors.white),
|
|
BoxShadow(
|
|
blurRadius: 8 * (1 - scale),
|
|
offset: Offset(2, 2) * (1 - scale),
|
|
color: Colors.grey[600].withOpacity(0.4))
|
|
];
|
|
List<BoxShadow> customShadowNight(double scale) => [
|
|
BoxShadow(
|
|
blurRadius: 6 * (1 - scale),
|
|
offset: Offset(-1, -1) * (1 - scale),
|
|
color: Colors.grey[100].withOpacity(0.3)),
|
|
BoxShadow(
|
|
blurRadius: 8 * (1 - scale),
|
|
offset: Offset(2, 2) * (1 - scale),
|
|
color: Colors.black)
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final s = context.s;
|
|
final _colorTween =
|
|
ColorTween(begin: context.accentColor.withAlpha(60), end: Colors.black);
|
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
return Selector<AudioPlayerNotifier, Tuple3<int, double, SleepTimerMode>>(
|
|
selector: (_, audio) =>
|
|
Tuple3(audio?.timeLeft, audio?.switchValue, audio.sleepTimerMode),
|
|
builder: (_, data, __) {
|
|
var fraction =
|
|
data.item2 == 1 ? 1.0 : math.min(_animation.value * 2, 1.0);
|
|
var move =
|
|
data.item2 == 1 ? 1.0 : math.max(_animation.value * 2 - 1, 0.0);
|
|
return LayoutBuilder(builder: (context, constraints) {
|
|
var width = constraints.maxWidth;
|
|
return Container(
|
|
height: 300,
|
|
decoration: BoxDecoration(
|
|
color: _colorTween.transform(move),
|
|
borderRadius: BorderRadius.circular(10)),
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
SizedBox(
|
|
height: 10,
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 20),
|
|
child: move == 1
|
|
? Center()
|
|
: Wrap(
|
|
direction: Axis.horizontal,
|
|
children: kMinsToSelect
|
|
.map((e) => InkWell(
|
|
onTap: () =>
|
|
setState(() => _minSelected = e),
|
|
child: Container(
|
|
margin: EdgeInsets.all(10.0),
|
|
decoration: BoxDecoration(
|
|
color: (e == _minSelected)
|
|
? context.accentColor
|
|
: context.primaryColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
alignment: Alignment.center,
|
|
height: 30,
|
|
width: 30,
|
|
child: Text(e.toString(),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: (e == _minSelected)
|
|
? Colors.white
|
|
: null)),
|
|
),
|
|
))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
Stack(
|
|
children: <Widget>[
|
|
SizedBox(
|
|
height: 100,
|
|
width: width,
|
|
),
|
|
Positioned(
|
|
left: data.item3 == SleepTimerMode.timer
|
|
? -width * (move) / 4
|
|
: width * (move) / 4,
|
|
child: SizedBox(
|
|
height: 100,
|
|
width: width,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: <Widget>[
|
|
Container(
|
|
alignment: Alignment.center,
|
|
height: 40,
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
border:
|
|
Border.all(color: context.primaryColor),
|
|
color: _colorTween.transform(move),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () {
|
|
audio.setSleepTimerMode =
|
|
SleepTimerMode.endOfEpisode;
|
|
if (fraction == 0) {
|
|
_controller.forward();
|
|
} else if (fraction == 1) {
|
|
_controller.reverse();
|
|
audio.cancelTimer();
|
|
}
|
|
},
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: SizedBox(
|
|
height: 40,
|
|
width: 120,
|
|
child: Center(
|
|
child: Text(
|
|
s.endOfEpisode,
|
|
style: TextStyle(
|
|
color: (move > 0
|
|
? Colors.white
|
|
: null)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: 100 * (1 - fraction),
|
|
width: 1,
|
|
color: context.primaryColorDark,
|
|
),
|
|
Container(
|
|
height: 40,
|
|
width: 120,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
border:
|
|
Border.all(color: context.primaryColor),
|
|
color: _colorTween.transform(move),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () {
|
|
audio.setSleepTimerMode =
|
|
SleepTimerMode.timer;
|
|
if (fraction == 0) {
|
|
_controller.forward();
|
|
} else if (fraction == 1) {
|
|
_controller.reverse();
|
|
audio.cancelTimer();
|
|
}
|
|
},
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: SizedBox(
|
|
height: 40,
|
|
width: 120,
|
|
child: Center(
|
|
child: Text(
|
|
data.item2 == 1
|
|
? data.item1.toTime
|
|
: (_minSelected * 60).toTime,
|
|
style: TextStyle(
|
|
color: (move > 0
|
|
? Colors.white
|
|
: null)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(
|
|
height: 60.0,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
context.s.sleepTimer,
|
|
style: TextStyle(
|
|
color: Theme.of(context).accentColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
if (move > 0)
|
|
Positioned(
|
|
bottom: 120,
|
|
left: width / 2 - 100,
|
|
width: 200,
|
|
child: Center(
|
|
child: Transform.translate(
|
|
offset: Offset(0, -50 * move),
|
|
child: Text(s.goodNight,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20,
|
|
color: Colors.white.withOpacity(move))),
|
|
),
|
|
),
|
|
),
|
|
if (data.item2 == 1) CustomPaint(painter: StarSky()),
|
|
if (data.item2 == 1) MeteorLoader()
|
|
],
|
|
),
|
|
);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class ControlPanel extends StatefulWidget {
|
|
ControlPanel(
|
|
{this.onExpand,
|
|
this.onClose,
|
|
this.maxHeight,
|
|
this.isPlayingPage = false,
|
|
Key key})
|
|
: super(key: key);
|
|
final VoidCallback onExpand;
|
|
final VoidCallback onClose;
|
|
final double maxHeight;
|
|
final bool isPlayingPage;
|
|
@override
|
|
_ControlPanelState createState() => _ControlPanelState();
|
|
}
|
|
|
|
class _ControlPanelState extends State<ControlPanel>
|
|
with TickerProviderStateMixin {
|
|
double _setSpeed;
|
|
AnimationController _controller;
|
|
Animation<double> _animation;
|
|
TabController _tabController;
|
|
int _tabIndex = 0;
|
|
List<BoxShadow> customShadow(double scale) => [
|
|
BoxShadow(
|
|
blurRadius: 26 * (1 - scale),
|
|
offset: Offset(-6, -6) * (1 - scale),
|
|
color: Colors.white),
|
|
BoxShadow(
|
|
blurRadius: 8 * (1 - scale),
|
|
offset: Offset(2, 2) * (1 - scale),
|
|
color: Colors.grey[600].withOpacity(0.4))
|
|
];
|
|
List<BoxShadow> customShadowNight(double scale) => [
|
|
BoxShadow(
|
|
blurRadius: 6 * (1 - scale),
|
|
offset: Offset(-1, -1) * (1 - scale),
|
|
color: Colors.grey[100].withOpacity(0.3)),
|
|
BoxShadow(
|
|
blurRadius: 8 * (1 - scale),
|
|
offset: Offset(2, 2) * (1 - scale),
|
|
color: Colors.black)
|
|
];
|
|
|
|
Future<List<double>> _getSpeedList() async {
|
|
var storage = KeyValueStorage('speedListKey');
|
|
return await storage.getSpeedList();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_setSpeed = 0;
|
|
_tabController = TabController(vsync: this, length: 2)
|
|
..addListener(() {
|
|
setState(() => _tabIndex = _tabController.index);
|
|
});
|
|
_controller =
|
|
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
|
|
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
|
|
..addListener(() {
|
|
setState(() => _setSpeed = _animation.value);
|
|
});
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_tabController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
var height = constraints.maxHeight;
|
|
return Container(
|
|
color: context.primaryColor,
|
|
height: 300,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Consumer<AudioPlayerNotifier>(
|
|
builder: (_, data, __) {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Container(
|
|
padding:
|
|
EdgeInsets.only(top: 20, left: 30, right: 30),
|
|
child: SliderTheme(
|
|
data: SliderTheme.of(context).copyWith(
|
|
activeTrackColor: height <= widget.maxHeight
|
|
? context.accentColor.withAlpha(70)
|
|
: Colors.transparent,
|
|
inactiveTrackColor: height > widget.maxHeight
|
|
? Colors.transparent
|
|
: context.primaryColorDark,
|
|
trackHeight: 8.0,
|
|
trackShape: MyRectangularTrackShape(),
|
|
thumbColor: context.accentColor,
|
|
thumbShape: RoundSliderThumbShape(
|
|
enabledThumbRadius: 6.0,
|
|
disabledThumbRadius: 6.0,
|
|
),
|
|
overlayColor: context.accentColor.withAlpha(32),
|
|
overlayShape:
|
|
RoundSliderOverlayShape(overlayRadius: 4.0),
|
|
),
|
|
child: Slider(
|
|
value: data.seekSliderValue,
|
|
onChanged: (val) {
|
|
audio.sliderSeek(val);
|
|
}),
|
|
),
|
|
),
|
|
Container(
|
|
height: 20.0,
|
|
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
|
child: height > widget.maxHeight
|
|
? Center()
|
|
: Row(
|
|
children: <Widget>[
|
|
Text(
|
|
(data.backgroundAudioPosition ~/ 1000)
|
|
.toTime ??
|
|
'',
|
|
style: TextStyle(fontSize: 10),
|
|
),
|
|
Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
child: data.remoteErrorMessage != null
|
|
? Text(data.remoteErrorMessage,
|
|
style: const TextStyle(
|
|
color: Color(0xFFFF0000)))
|
|
: Text(
|
|
data.audioState ==
|
|
AudioProcessingState
|
|
.buffering ||
|
|
data.audioState ==
|
|
AudioProcessingState
|
|
.connecting ||
|
|
data.audioState ==
|
|
AudioProcessingState
|
|
.none ||
|
|
data.audioState ==
|
|
AudioProcessingState
|
|
.skippingToNext
|
|
? context.s.buffering
|
|
: '',
|
|
style: TextStyle(
|
|
color: context.accentColor),
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
(data.backgroundAudioDuration ~/ 1000)
|
|
.toTime ??
|
|
'',
|
|
style: TextStyle(fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
SizedBox(
|
|
height: 100,
|
|
child: Selector<AudioPlayerNotifier, bool>(
|
|
selector: (_, audio) => audio.playing,
|
|
builder: (_, playing, __) {
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
FlatButton(
|
|
color: Colors.transparent,
|
|
padding: EdgeInsets.only(right: 10, left: 10),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(100.0),
|
|
side: BorderSide(color: Colors.transparent)),
|
|
onPressed: playing ? () => audio.rewind() : null,
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.fast_rewind,
|
|
size: 32, color: Colors.grey[500]),
|
|
SizedBox(width: 5),
|
|
Selector<AudioPlayerNotifier, int>(
|
|
selector: (_, audio) =>
|
|
audio.rewindSeconds,
|
|
builder: (_, seconds, __) => Padding(
|
|
padding:
|
|
const EdgeInsets.only(top: 5.0),
|
|
child: Text('$seconds s',
|
|
style: GoogleFonts.teko(
|
|
textBaseline:
|
|
TextBaseline.ideographic,
|
|
textStyle: TextStyle(
|
|
color: Colors.grey[500],
|
|
fontSize: 25),
|
|
)),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 30),
|
|
height: 60,
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: context.primaryColor,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color:
|
|
context.brightness == Brightness.dark
|
|
? Colors.black12
|
|
: Colors.white10,
|
|
width: 1),
|
|
boxShadow:
|
|
context.brightness == Brightness.dark
|
|
? _customShadowNight
|
|
: _customShadow),
|
|
child: playing
|
|
? Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(30)),
|
|
onTap: playing
|
|
? () {
|
|
audio.pauseAduio();
|
|
}
|
|
: null,
|
|
child: SizedBox(
|
|
height: 60,
|
|
width: 60,
|
|
child: Icon(
|
|
Icons.pause,
|
|
size: 40,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(30)),
|
|
onTap: playing
|
|
? null
|
|
: () {
|
|
audio.resumeAudio();
|
|
},
|
|
child: SizedBox(
|
|
height: 60,
|
|
width: 60,
|
|
child: Icon(
|
|
Icons.play_arrow,
|
|
size: 40,
|
|
color: context.accentColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
FlatButton(
|
|
padding: EdgeInsets.only(left: 10.0, right: 10),
|
|
onPressed:
|
|
playing ? () => audio.fastForward() : null,
|
|
color: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(100.0),
|
|
side: BorderSide(color: Colors.transparent)),
|
|
child: Row(
|
|
children: [
|
|
Selector<AudioPlayerNotifier, int>(
|
|
selector: (_, audio) =>
|
|
audio.fastForwardSeconds,
|
|
builder: (_, seconds, __) => Padding(
|
|
padding:
|
|
const EdgeInsets.only(top: 5.0),
|
|
child: Text('$seconds s',
|
|
style: GoogleFonts.teko(
|
|
textStyle: TextStyle(
|
|
color: Colors.grey[500],
|
|
fontSize: 25),
|
|
)),
|
|
)),
|
|
SizedBox(width: 10),
|
|
Icon(Icons.fast_forward,
|
|
size: 32.0, color: Colors.grey[500]),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 80.0,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Expanded(
|
|
child: Selector<AudioPlayerNotifier, String>(
|
|
selector: (_, audio) => audio.episode.title,
|
|
builder: (_, title, __) {
|
|
return Container(
|
|
padding: EdgeInsets.only(left: 60, right: 60),
|
|
child: LayoutBuilder(
|
|
builder: (context, size) {
|
|
var span = TextSpan(
|
|
text: title,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20));
|
|
var tp = TextPainter(
|
|
text: span,
|
|
maxLines: 1,
|
|
textDirection: TextDirection.ltr);
|
|
tp.layout(maxWidth: size.maxWidth);
|
|
if (tp.didExceedMaxLines) {
|
|
return Marquee(
|
|
text: title,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18),
|
|
scrollAxis: Axis.horizontal,
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
blankSpace: 30.0,
|
|
velocity: 50.0,
|
|
pauseAfterRound: Duration.zero,
|
|
startPadding: 30.0,
|
|
accelerationDuration:
|
|
Duration(milliseconds: 100),
|
|
accelerationCurve: Curves.linear,
|
|
decelerationDuration:
|
|
Duration(milliseconds: 100),
|
|
decelerationCurve: Curves.linear,
|
|
);
|
|
} else {
|
|
return Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
if (height <= widget.maxHeight) LastPosition()
|
|
],
|
|
),
|
|
),
|
|
if (height > widget.maxHeight)
|
|
SizedBox(
|
|
height: height - widget.maxHeight,
|
|
child: SingleChildScrollView(
|
|
physics: NeverScrollableScrollPhysics(),
|
|
child: SizedBox(
|
|
height: 300,
|
|
child: ScrollConfiguration(
|
|
behavior: NoGrowBehavior(),
|
|
child: TabBarView(
|
|
controller: _tabController,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20.0),
|
|
child: PlaylistWidget(),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20.0),
|
|
child: SleepMode(),
|
|
)
|
|
]),
|
|
))),
|
|
),
|
|
Expanded(
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
if (height <= widget.maxHeight)
|
|
Selector<AudioPlayerNotifier,
|
|
Tuple4<EpisodeBrief, bool, bool, double>>(
|
|
selector: (_, audio) => Tuple4(
|
|
audio.episode,
|
|
audio.stopOnComplete,
|
|
audio.startSleepTimer,
|
|
audio.currentSpeed),
|
|
builder: (_, data, __) {
|
|
final currentSpeed = data.item4 ?? 1.0;
|
|
return Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 20.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
if (_setSpeed == 0)
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () async {
|
|
widget.onClose();
|
|
if (!widget.isPlayingPage) {
|
|
Navigator.push(
|
|
context,
|
|
FadeRoute(
|
|
page: EpisodeDetail(
|
|
episodeItem: data.item1,
|
|
heroTag: 'playpanel')));
|
|
}
|
|
},
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
height: 30.0,
|
|
width: 30.0,
|
|
child: CircleAvatar(
|
|
backgroundImage:
|
|
data.item1.avatarImage,
|
|
),
|
|
),
|
|
SizedBox(width: 5),
|
|
SizedBox(
|
|
width: 100,
|
|
child: Text(
|
|
data.item1.feedTitle,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.fade,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (_setSpeed > 0)
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.all(10.0),
|
|
scrollDirection: Axis.horizontal,
|
|
child: FutureBuilder<List<double>>(
|
|
future: _getSpeedList(),
|
|
initialData: [],
|
|
builder: (context, snapshot) => Row(
|
|
children: snapshot.data
|
|
.map<Widget>((e) => InkWell(
|
|
onTap: () {
|
|
if (_setSpeed == 1) {
|
|
audio.setSpeed(e);
|
|
}
|
|
},
|
|
child: Container(
|
|
height: 30,
|
|
width: 30,
|
|
margin: EdgeInsets
|
|
.symmetric(
|
|
horizontal: 5),
|
|
decoration: e ==
|
|
currentSpeed &&
|
|
_setSpeed > 0
|
|
? BoxDecoration(
|
|
color: context
|
|
.accentColor,
|
|
shape: BoxShape
|
|
.circle,
|
|
boxShadow: context
|
|
.brightness ==
|
|
Brightness
|
|
.light
|
|
? customShadow(
|
|
1.0)
|
|
: customShadowNight(
|
|
1.0),
|
|
)
|
|
: BoxDecoration(
|
|
color: context
|
|
.primaryColor,
|
|
shape: BoxShape
|
|
.circle,
|
|
boxShadow: context
|
|
.brightness ==
|
|
Brightness
|
|
.light
|
|
? customShadow(1 -
|
|
_setSpeed)
|
|
: customShadowNight(1 -
|
|
_setSpeed)),
|
|
alignment:
|
|
Alignment.center,
|
|
child: _setSpeed > 0
|
|
? Text(e.toString(),
|
|
style: TextStyle(
|
|
fontWeight:
|
|
FontWeight
|
|
.bold,
|
|
color: e ==
|
|
currentSpeed
|
|
? Colors
|
|
.white
|
|
: null))
|
|
: Center(),
|
|
),
|
|
))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
padding: EdgeInsets.zero,
|
|
onPressed: () {
|
|
if (_setSpeed == 0) {
|
|
_controller.forward();
|
|
} else {
|
|
_controller.reverse();
|
|
}
|
|
},
|
|
icon: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceEvenly,
|
|
children: <Widget>[
|
|
Transform.rotate(
|
|
angle: math.pi * _setSpeed,
|
|
child: Text('X')),
|
|
Text(currentSpeed.toStringAsFixed(1)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (_setSpeed == 0)
|
|
Positioned(
|
|
bottom: widget.maxHeight == kMaxPlayerHeight[2]
|
|
? 35.0
|
|
: widget.maxHeight == kMaxPlayerHeight[1]
|
|
? 25.0
|
|
: 15.0,
|
|
child: InkWell(
|
|
child: SizedBox(
|
|
height: 50,
|
|
width: 100,
|
|
child: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: CustomPaint(
|
|
size: Size(100, 5),
|
|
painter: TabIndicator(
|
|
index: _tabIndex,
|
|
indicatorSize: 20,
|
|
fraction:
|
|
(height - widget.maxHeight) / 300,
|
|
accentColor: context.accentColor,
|
|
color: context.textColor)),
|
|
),
|
|
),
|
|
onTap: widget.onExpand),
|
|
),
|
|
if (_setSpeed == 0 && height > widget.maxHeight)
|
|
Transform.translate(
|
|
offset:
|
|
Offset(0, 5) * (height - widget.maxHeight) / 300,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: context.width / 2 - 80),
|
|
child: TabBar(
|
|
controller: _tabController,
|
|
indicatorSize: TabBarIndicatorSize.label,
|
|
labelColor: context.accentColor,
|
|
unselectedLabelColor: context.textColor,
|
|
indicator: BoxDecoration(),
|
|
tabs: [
|
|
Container(
|
|
height: 20,
|
|
width: 20,
|
|
child: Icon(Icons.playlist_play)),
|
|
Container(
|
|
height: 20,
|
|
width: 20,
|
|
child: Transform.rotate(
|
|
angle: math.pi * 0.7,
|
|
child: Icon(Icons.brightness_2,
|
|
size: 18))),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|