This commit is contained in:
exttex 2021-02-09 21:14:14 +01:00
parent ff239aaf86
commit 66bfd5eb70
173 changed files with 912 additions and 32459 deletions

View file

@ -5,6 +5,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/error.dart';
@ -62,6 +64,10 @@ class _LyricsScreenState extends State<LyricsScreen> {
void initState() {
_load();
//Enable visualizer
if (settings.lyricsVisualizer)
playerHelper.startVisualizer();
Timer.periodic(Duration(milliseconds: 350), (timer) {
_timer = timer;
if (_loading) return;
@ -82,7 +88,7 @@ class _LyricsScreenState extends State<LyricsScreen> {
});
//Track change = exit lyrics
AudioService.currentMediaItemStream.listen((event) {
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
if (event.id != widget.trackId)
Navigator.of(context).pop();
});
@ -96,6 +102,9 @@ class _LyricsScreenState extends State<LyricsScreen> {
_timer.cancel();
if (_mediaItemSub != null)
_mediaItemSub.cancel();
//Stop visualizer
if (settings.lyricsVisualizer)
playerHelper.stopVisualizer();
super.dispose();
}
@ -104,48 +113,79 @@ class _LyricsScreenState extends State<LyricsScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Lyrics'.i18n),
body: ListView(
controller: _controller,
body: Stack(
children: [
//Shouldn't really happen, empty lyrics have own text
if (_error)
ErrorScreen(),
//Loading
if (_loading)
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator()
],
//Visualizer
if (settings.lyricsVisualizer)
Align(
alignment: Alignment.bottomCenter,
child: StreamBuilder(
stream: playerHelper.visualizerStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
List<double> data = snapshot.data??[];
double width = MediaQuery.of(context).size.width / data.length - 0.25;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(data.length, (i) => AnimatedContainer(
duration: Duration(milliseconds: 130),
color: Theme.of(context).primaryColor,
height: data[i] * 100,
width: width,
)),
);
}
),
),
if (lyrics != null)
...List.generate(lyrics.lyrics.length, (i) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: (_currentIndex == i) ? Colors.grey.withOpacity(0.25) : Colors.transparent,
),
height: height,
child: Center(
child: Text(
lyrics.lyrics[i].text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal
),
//Lyrics
Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, settings.lyricsVisualizer ? 100 : 0),
child: ListView(
controller: _controller,
children: [
//Shouldn't really happen, empty lyrics have own text
if (_error)
ErrorScreen(),
//Loading
if (_loading)
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator()
],
),
)
)
);
}),
),
if (lyrics != null)
...List.generate(lyrics.lyrics.length, (i) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: (_currentIndex == i) ? Colors.grey.withOpacity(0.25) : Colors.transparent,
),
height: height,
child: Center(
child: Text(
lyrics.lyrics[i].text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal
),
),
)
)
);
}),
],
),
)
],
)
);

View file

@ -102,9 +102,7 @@ class PlayerBar extends StatelessWidget {
}
}
class PrevNextButton extends StatelessWidget {
final double size;
final bool prev;
final bool hidePrev;
@ -113,38 +111,43 @@ class PrevNextButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!prev) {
if (playerHelper.queueIndex == (AudioService.queue??[]).length - 1) {
return IconButton(
icon: Icon(Icons.skip_next),
iconSize: size,
onPressed: null,
);
}
return IconButton(
icon: Icon(Icons.skip_next),
iconSize: size,
onPressed: () => AudioService.skipToNext(),
);
}
if (prev) {
if (i == 0) {
if (hidePrev) {
return Container(height: 0, width: 0,);
return StreamBuilder(
stream: AudioService.queueStream,
builder: (context, _snapshot) {
if (!prev) {
if (playerHelper.queueIndex == (AudioService.queue??[]).length - 1) {
return IconButton(
icon: Icon(Icons.skip_next),
iconSize: size,
onPressed: null,
);
}
return IconButton(
icon: Icon(Icons.skip_next),
iconSize: size,
onPressed: () => AudioService.skipToNext(),
);
}
return IconButton(
icon: Icon(Icons.skip_previous),
iconSize: size,
onPressed: null,
);
}
return IconButton(
icon: Icon(Icons.skip_previous),
iconSize: size,
onPressed: () => AudioService.skipToPrevious(),
);
}
return Container();
if (prev) {
if (i == 0) {
if (hidePrev) {
return Container(height: 0, width: 0,);
}
return IconButton(
icon: Icon(Icons.skip_previous),
iconSize: size,
onPressed: null,
);
}
return IconButton(
icon: Icon(Icons.skip_previous),
iconSize: size,
onPressed: () => AudioService.skipToPrevious(),
);
}
return Container();
},
);
}
}

View file

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -28,8 +25,11 @@ import '../api/definitions.dart';
import 'player_bar.dart';
import 'dart:ui';
import 'dart:convert';
import 'dart:async';
//Changing item in queue view and pressing back causes the pageView to skip song
bool pageViewLock = false;
class PlayerScreen extends StatefulWidget {
@override
@ -40,37 +40,53 @@ class _PlayerScreenState extends State<PlayerScreen> {
LinearGradient _bgGradient;
StreamSubscription _mediaItemSub;
ImageProvider _blurImage;
//Calculate background color
Future _updateColor() async {
if (!settings.colorGradientBackground)
if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
return;
//BG Image
if (settings.blurPlayerBackground)
setState(() {
_blurImage = NetworkImage(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri);
});
//Run in isolate
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri));
//Update notification
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: palette.dominantColor.color.withOpacity(0.7)
));
if (settings.blurPlayerBackground)
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: palette.dominantColor.color.withOpacity(0.25),
systemNavigationBarColor: Color.alphaBlend(palette.dominantColor.color.withOpacity(0.25), Theme.of(context).scaffoldBackgroundColor)
));
setState(() => _bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [palette.dominantColor.color.withOpacity(0.7), Color.fromARGB(0, 0, 0, 0)],
stops: [
0.0,
0.6
]
));
//Color gradient
if (!settings.blurPlayerBackground) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: palette.dominantColor.color.withOpacity(0.7),
));
setState(() => _bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [palette.dominantColor.color.withOpacity(0.7), Color.fromARGB(0, 0, 0, 0)],
stops: [
0.0,
0.6
]
));
}
}
@override
void initState() {
Future.delayed(Duration(milliseconds: 1000), _updateColor);
Future.delayed(Duration(milliseconds: 600), _updateColor);
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
_updateColor();
});
super.initState();
}
@ -95,31 +111,51 @@ class _PlayerScreenState extends State<PlayerScreen> {
body: SafeArea(
child: Container(
decoration: BoxDecoration(
gradient: _bgGradient
gradient: settings.blurPlayerBackground ? null : _bgGradient
),
child: StreamBuilder(
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
builder: (BuildContext context, AsyncSnapshot snapshot) {
child: Stack(
children: [
if (settings.blurPlayerBackground && _blurImage != null)
ClipRect(
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: _blurImage,
fit: BoxFit.fill,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.25), BlendMode.dstATop)
)
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(color: Colors.transparent),
),
),
),
StreamBuilder(
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
builder: (BuildContext context, AsyncSnapshot snapshot) {
//When disconnected
if (AudioService.currentMediaItem == null) {
playerHelper.startService();
return Center(child: CircularProgressIndicator(),);
}
return OrientationBuilder(
builder: (context, orientation) {
//Landscape
if (orientation == Orientation.landscape) {
return PlayerScreenHorizontal();
//When disconnected
if (AudioService.currentMediaItem == null) {
playerHelper.startService();
return Center(child: CircularProgressIndicator(),);
}
//Portrait
return PlayerScreenVertical();
},
);
},
),
return OrientationBuilder(
builder: (context, orientation) {
//Landscape
if (orientation == Orientation.landscape) {
return PlayerScreenHorizontal();
}
//Portrait
return PlayerScreenVertical();
},
);
},
),
],
)
)
)
);
@ -172,28 +208,28 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: ScreenUtil().setSp(40),
height: ScreenUtil().setSp(50),
child: AudioService.currentMediaItem.displayTitle.length >= 22 ?
Marquee(
text: AudioService.currentMediaItem.displayTitle,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold
),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
):
Text(
AudioService.currentMediaItem.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold
),
)
Marquee(
text: AudioService.currentMediaItem.displayTitle,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold
),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
):
Text(
AudioService.currentMediaItem.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold
),
)
),
Container(height: 4,),
Text(
@ -279,28 +315,28 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: ScreenUtil().setSp(64),
height: ScreenUtil().setSp(80),
child: AudioService.currentMediaItem.displayTitle.length >= 26 ?
Marquee(
text: AudioService.currentMediaItem.displayTitle,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold
),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
):
Text(
AudioService.currentMediaItem.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold
),
)
Marquee(
text: AudioService.currentMediaItem.displayTitle,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold
),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
):
Text(
AudioService.currentMediaItem.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold
),
)
),
Container(height: 4,),
Text(
@ -577,6 +613,10 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
child: PageView(
controller: _pageController,
onPageChanged: (int index) {
if (pageViewLock) {
pageViewLock = false;
return;
}
if (_animationLock) return;
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
@ -769,10 +809,11 @@ class _QueueScreenState extends State<QueueScreen> {
return TrackTile(
t,
onTap: () async {
await AudioService.playFromMediaId(t.id);
pageViewLock = true;
await AudioService.skipToQueueItem(t.id);
Navigator.of(context).pop();
},
key: Key(t.id),
key: Key(i.toString()),
trailing: IconButton(
icon: Icon(Icons.close),
onPressed: () async {

View file

@ -18,7 +18,6 @@ import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/home_screen.dart';
import 'package:freezer/ui/updater.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:language_pickers/language_pickers.dart';
import 'package:language_pickers/languages.dart';
import 'package:package_info/package_info.dart';
@ -51,6 +50,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
'name': 'Furry',
'isoCode': 'uwu'
});
defaultLanguagesList.add({
'name': 'Asturian',
'isoCode': 'ast'
});
defaultLanguagesList.add({
'name': 'Chinese',
'isoCode': 'zh'
});
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
return {
@ -250,6 +257,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
),
leading: Icon(Icons.android)
),
ListTile(
title: Text('Font'.i18n),
leading: Icon(Icons.font_download),
subtitle: Text(settings.font),
onTap: () {
showDialog(
context: context,
builder: (context) => FontSelector(() => Navigator.of(context).pop())
);
},
),
ListTile(
title: Text('Player gradient background'.i18n),
leading: Icon(Icons.colorize),
@ -261,6 +279,33 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
),
ListTile(
title: Text('Blur player background'.i18n),
subtitle: Text('Might have impact on performance'.i18n),
leading: Icon(Icons.blur_on),
trailing: Switch(
value: settings.blurPlayerBackground,
onChanged: (bool v) async {
setState(() => settings.blurPlayerBackground = v);
await settings.save();
},
),
),
ListTile(
title: Text('Visualizer'.i18n),
subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n),
leading: Icon(Icons.equalizer),
trailing: Switch(
value: settings.lyricsVisualizer,
onChanged: (bool v) async {
if (await Permission.microphone.request().isGranted) {
setState(() => settings.lyricsVisualizer = v);
await settings.save();
return;
}
},
),
),
ListTile(
title: Text('Primary color'.i18n),
leading: Icon(Icons.format_paint),
@ -322,6 +367,77 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}
}
class FontSelector extends StatefulWidget {
final Function callback;
FontSelector(this.callback, {Key key}): super(key: key);
@override
_FontSelectorState createState() => _FontSelectorState();
}
class _FontSelectorState extends State<FontSelector> {
String query = '';
List<String> get fonts {
return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList();
}
//Font selected
void onTap(String font) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Warning'.i18n),
content: Text("This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!".i18n),
actions: [
FlatButton(
onPressed: () async {
setState(() => settings.font = font);
await settings.save();
Navigator.of(context).pop();
widget.callback();
//Global setState
updateTheme();
},
child: Text('Apply'.i18n),
),
FlatButton(
onPressed: () {
Navigator.of(context).pop();
widget.callback();
},
child: Text('Cancel'),
)
],
)
);
}
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text("Select font".i18n),
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: TextField(
decoration: InputDecoration(
hintText: 'Search'.i18n
),
onChanged: (q) => setState(() => query = q),
),
),
...List.generate(fonts.length, (i) => SimpleDialogOption(
child: Text(fonts[i]),
onPressed: () => onTap(fonts[i]),
))
],
);
}
}
class QualitySettings extends StatefulWidget {
@override
@ -766,6 +882,13 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
}
),
FreezerDivider(),
ListTile(
title: Text('Tags'.i18n),
leading: Icon(Icons.label),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => TagSelectionScreen()
)),
),
ListTile(
title: Text('Create folders for artist'.i18n),
trailing: Switch(
@ -917,6 +1040,62 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
}
}
class TagOption {
String title;
String value;
TagOption(this.title, this.value);
}
class TagSelectionScreen extends StatefulWidget {
@override
_TagSelectionScreenState createState() => _TagSelectionScreenState();
}
class _TagSelectionScreenState extends State<TagSelectionScreen> {
List<TagOption> tags = [
TagOption("Title".i18n, 'title'),
TagOption("Album".i18n, 'album'),
TagOption('Artist'.i18n, 'artist'),
TagOption('Track number'.i18n, 'track'),
TagOption('Disc number'.i18n, 'disc'),
TagOption('Album artist'.i18n, 'albumArtist'),
TagOption('Date/Year'.i18n, 'date'),
TagOption('Label'.i18n, 'label'),
TagOption('ISRC'.i18n, 'isrc'),
TagOption('UPC'.i18n, 'upc'),
TagOption('Track total'.i18n, 'trackTotal'),
TagOption('BPM'.i18n, 'bpm'),
TagOption('Unsynchronized lyrics'.i18n, 'lyrics'),
TagOption('Genre'.i18n, 'genre'),
TagOption('Contributors'.i18n, 'contributors'),
TagOption('Album art'.i18n, 'art')
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Tags'.i18n),
body: ListView(
children: List.generate(tags.length, (i) => ListTile(
title: Text(tags[i].title),
leading: Switch(
value: settings.tags.contains(tags[i].value),
onChanged: (v) async {
//Update
if (v) settings.tags.add(tags[i].value);
else settings.tags.remove(tags[i].value);
setState((){});
await settings.save();
},
),
)),
),
);
}
}
class GeneralSettings extends StatefulWidget {
@override
@ -982,6 +1161,18 @@ class _GeneralSettingsState extends State<GeneralSettings> {
);
},
),
ListTile(
title: Text('Enable equalizer'.i18n),
subtitle: Text('Might enable some equalizer apps to work. Requires restart of Freezer'.i18n),
leading: Icon(Icons.equalizer),
trailing: Switch(
value: settings.enableEqualizer,
onChanged: (v) async {
setState(() => settings.enableEqualizer = v);
settings.save();
},
),
),
ListTile(
title: Text('LastFM'.i18n),
subtitle: Text(

View file

@ -308,7 +308,7 @@ class SmartTrackListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 200.0,
height: 210.0,
child: InkWell(
onTap: onTap,
onLongPress: onHold,