diff --git a/lib/settings.dart b/lib/settings.dart index dd105ac..d90013f 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -20,7 +20,6 @@ Settings settings; @JsonSerializable() class Settings { - //Language @JsonKey(defaultValue: null) String language; @@ -46,7 +45,6 @@ class Settings { @JsonKey(defaultValue: AudioQuality.FLAC) AudioQuality downloadQuality; - //Download options String downloadPath; @@ -78,12 +76,26 @@ class Settings { String singletonFilename; @JsonKey(defaultValue: 1400) int albumArtResolution; - @JsonKey(defaultValue: ["title", "album", "artist", "track", "disc", - "albumArtist", "date", "label", "isrc", "upc", "trackTotal", "bpm", - "lyrics", "genre", "contributors", "art"]) + @JsonKey(defaultValue: [ + "title", + "album", + "artist", + "track", + "disc", + "albumArtist", + "date", + "label", + "isrc", + "upc", + "trackTotal", + "bpm", + "lyrics", + "genre", + "contributors", + "art" + ]) List tags; - //Appearance @JsonKey(defaultValue: Themes.Dark) Themes theme; @@ -105,7 +117,7 @@ class Settings { Color primaryColor = Colors.blue; static _colorToJson(Color c) => c.value; - static _colorFromJson(int v) => Color(v??Colors.blue.value); + static _colorFromJson(int v) => Color(v ?? Colors.blue.value); @JsonKey(defaultValue: false) bool useArtColor = false; @@ -127,13 +139,13 @@ class Settings { @JsonKey(defaultValue: null) String lastFMPassword; - Settings({this.downloadPath, this.arl}); ThemeData get themeData { //System theme if (useSystemTheme) { - if (SchedulerBinding.instance.window.platformBrightness == Brightness.light) { + if (SchedulerBinding.instance.window.platformBrightness == + Brightness.light) { return _themeData[Themes.Light]; } else { if (theme == Themes.Light) return _themeData[Themes.Dark]; @@ -141,7 +153,7 @@ class Settings { } } //Theme - return _themeData[theme]??ThemeData(); + return _themeData[theme] ?? ThemeData(); } //Get all available fonts @@ -158,7 +170,8 @@ class Settings { useArtColor = v; if (v) { //On media item change set color - _useArtColorSub = AudioService.currentMediaItemStream.listen((event) async { + _useArtColorSub = + AudioService.currentMediaItemStream.listen((event) async { if (event == null || event.artUri == null) return; this.primaryColor = await imagesDatabase.getPrimaryColor(event.artUri); updateTheme(); @@ -173,10 +186,9 @@ class Settings { } SliderThemeData get _sliderTheme => SliderThemeData( - thumbColor: primaryColor, - activeTrackColor: primaryColor, - inactiveTrackColor: primaryColor.withOpacity(0.2) - ); + thumbColor: primaryColor, + activeTrackColor: primaryColor, + inactiveTrackColor: primaryColor.withOpacity(0.2)); //Load settings/init Future loadSettings() async { @@ -188,7 +200,8 @@ class Settings { } Settings s = Settings.fromJson({}); //Set default path, because async - s.downloadPath = (await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_MUSIC)); + s.downloadPath = (await ExtStorage.getExternalStoragePublicDirectory( + ExtStorage.DIRECTORY_MUSIC)); s.save(); return s; } @@ -210,17 +223,22 @@ class Settings { //AudioQuality to deezer int int getQualityInt(AudioQuality q) { switch (q) { - case AudioQuality.MP3_128: return 1; - case AudioQuality.MP3_320: return 3; - case AudioQuality.FLAC: return 9; - default: return 8; + case AudioQuality.MP3_128: + return 1; + case AudioQuality.MP3_320: + return 3; + case AudioQuality.FLAC: + return 9; + default: + return 8; } } //Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke bool get isDark { if (useSystemTheme) { - if (SchedulerBinding.instance.window.platformBrightness == Brightness.light) return false; + if (SchedulerBinding.instance.window.platformBrightness == + Brightness.light) return false; return true; } if (theme == Themes.Light) return false; @@ -229,85 +247,94 @@ class Settings { static const deezerBg = Color(0xFF1F1A16); static const deezerBottom = Color(0xFF1b1714); - TextTheme get _textTheme => (font == 'Deezer') ? null : GoogleFonts.getTextTheme(font); + TextTheme get _textTheme => + (font == 'Deezer') ? null : GoogleFonts.getTextTheme(font); String get _fontFamily => (font == 'Deezer') ? 'MabryPro' : null; - Map get _themeData => { - Themes.Light: ThemeData( - textTheme: _textTheme, - fontFamily: _fontFamily, - brightness: Brightness.light, - primaryColor: primaryColor, - accentColor: primaryColor, - sliderTheme: _sliderTheme, - toggleableActiveColor: primaryColor, - bottomAppBarColor: Color(0xfff5f5f5), - appBarTheme: AppBarTheme(brightness: Brightness.light), - ), - Themes.Dark: ThemeData( - textTheme: _textTheme, - fontFamily: _fontFamily, - brightness: Brightness.dark, - primaryColor: primaryColor, - accentColor: primaryColor, - sliderTheme: _sliderTheme, - toggleableActiveColor: primaryColor, - appBarTheme: AppBarTheme(brightness: Brightness.dark), - ), - Themes.Deezer: ThemeData( - textTheme: _textTheme, - fontFamily: _fontFamily, - brightness: Brightness.dark, - primaryColor: primaryColor, - accentColor: primaryColor, - sliderTheme: _sliderTheme, - toggleableActiveColor: primaryColor, - backgroundColor: deezerBg, - scaffoldBackgroundColor: deezerBg, - bottomAppBarColor: deezerBottom, - dialogBackgroundColor: deezerBottom, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: deezerBottom - ), - appBarTheme: AppBarTheme(brightness: Brightness.dark), - cardColor: deezerBg - ), - Themes.Black: ThemeData( - textTheme: _textTheme, - fontFamily: _fontFamily, - brightness: Brightness.dark, - primaryColor: primaryColor, - accentColor: primaryColor, - backgroundColor: Colors.black, - scaffoldBackgroundColor: Colors.black, - bottomAppBarColor: Colors.black, - dialogBackgroundColor: Colors.black, - sliderTheme: _sliderTheme, - toggleableActiveColor: primaryColor, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: Colors.black, - ), - appBarTheme: AppBarTheme(brightness: Brightness.dark), - ) - }; + MaterialColor get _primarySwatch => + MaterialColor(primaryColor.value, { + 50: primaryColor.withOpacity(.1), + 100: primaryColor.withOpacity(.2), + 200: primaryColor.withOpacity(.3), + 300: primaryColor.withOpacity(.4), + 400: primaryColor.withOpacity(.5), + 500: primaryColor.withOpacity(.6), + 600: primaryColor.withOpacity(.7), + 700: primaryColor.withOpacity(.8), + 800: primaryColor.withOpacity(.9), + 900: primaryColor.withOpacity(1), + }); - Future getPath() async => p.join((await getApplicationDocumentsDirectory()).path, 'settings.json'); + Map get _themeData => { + Themes.Light: ThemeData( + textTheme: _textTheme, + fontFamily: _fontFamily, + brightness: Brightness.light, + primaryColor: primaryColor, + primarySwatch: _primarySwatch, + accentColor: primaryColor, + sliderTheme: _sliderTheme, + toggleableActiveColor: primaryColor, + bottomAppBarColor: Color(0xfff5f5f5), + appBarTheme: AppBarTheme(brightness: Brightness.light), + ), + Themes.Dark: ThemeData( + textTheme: _textTheme, + fontFamily: _fontFamily, + brightness: Brightness.dark, + primaryColor: primaryColor, + primarySwatch: _primarySwatch, + accentColor: primaryColor, + sliderTheme: _sliderTheme, + toggleableActiveColor: primaryColor, + appBarTheme: AppBarTheme(brightness: Brightness.dark), + ), + Themes.Deezer: ThemeData( + textTheme: _textTheme, + fontFamily: _fontFamily, + brightness: Brightness.dark, + primaryColor: primaryColor, + primarySwatch: _primarySwatch, + accentColor: primaryColor, + sliderTheme: _sliderTheme, + toggleableActiveColor: primaryColor, + backgroundColor: deezerBg, + scaffoldBackgroundColor: deezerBg, + bottomAppBarColor: deezerBottom, + dialogBackgroundColor: deezerBottom, + bottomSheetTheme: + BottomSheetThemeData(backgroundColor: deezerBottom), + appBarTheme: AppBarTheme(brightness: Brightness.dark), + cardColor: deezerBg), + Themes.Black: ThemeData( + textTheme: _textTheme, + fontFamily: _fontFamily, + brightness: Brightness.dark, + primaryColor: primaryColor, + primarySwatch: _primarySwatch, + accentColor: primaryColor, + backgroundColor: Colors.black, + scaffoldBackgroundColor: Colors.black, + bottomAppBarColor: Colors.black, + dialogBackgroundColor: Colors.black, + sliderTheme: _sliderTheme, + toggleableActiveColor: primaryColor, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: Colors.black, + ), + appBarTheme: AppBarTheme(brightness: Brightness.dark), + ) + }; + + Future getPath() async => + p.join((await getApplicationDocumentsDirectory()).path, 'settings.json'); //JSON - factory Settings.fromJson(Map json) => _$SettingsFromJson(json); + factory Settings.fromJson(Map json) => + _$SettingsFromJson(json); Map toJson() => _$SettingsToJson(this); } -enum AudioQuality { - MP3_128, - MP3_320, - FLAC, - ASK -} +enum AudioQuality { MP3_128, MP3_320, FLAC, ASK } -enum Themes { - Light, - Dark, - Deezer, - Black -} \ No newline at end of file +enum Themes { Light, Dark, Deezer, Black } diff --git a/lib/ui/lyrics.dart b/lib/ui/lyrics.dart index 5cab83d..d63ab3c 100644 --- a/lib/ui/lyrics.dart +++ b/lib/ui/lyrics.dart @@ -11,18 +11,16 @@ import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/error.dart'; class LyricsScreen extends StatefulWidget { - final Lyrics lyrics; final String trackId; - LyricsScreen({this.lyrics, this.trackId, Key key}): super(key: key); + LyricsScreen({this.lyrics, this.trackId, Key key}) : super(key: key); @override _LyricsScreenState createState() => _LyricsScreenState(); } class _LyricsScreenState extends State { - Lyrics lyrics; bool _loading = true; bool _error = false; @@ -33,6 +31,8 @@ class _LyricsScreenState extends State { StreamSubscription _mediaItemSub; final double height = 90; + bool _freeScroll = false; + Future _load() async { //Already available if (this.lyrics != null) return; @@ -59,17 +59,27 @@ class _LyricsScreenState extends State { } } + void _scrollToLyric() { + //Lyric height, screen height, appbar height + double _scrollTo = (height * _currentIndex) - + (MediaQuery.of(context).size.height / 2) + + (height / 2) + + 56; + if (0 > _scrollTo) return; + _controller.animateTo(_scrollTo, + duration: Duration(milliseconds: 250), curve: Curves.ease); + } + @override void initState() { _load(); //Enable visualizer - if (settings.lyricsVisualizer) - playerHelper.startVisualizer(); - + if (settings.lyricsVisualizer) playerHelper.startVisualizer(); Timer.periodic(Duration(milliseconds: 350), (timer) { _timer = timer; - _currentIndex = lyrics?.lyrics?.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition); + _currentIndex = lyrics?.lyrics?.lastIndexWhere( + (l) => l.offset <= AudioService.playbackState.currentPosition); if (_loading) return; //Scroll to current lyric if (_currentIndex <= 0) return; @@ -77,20 +87,13 @@ class _LyricsScreenState extends State { //Update current lyric index setState(() => null); _prevIndex = _currentIndex; - //Lyric height, screen height, appbar height - double _scrollTo = (height * _currentIndex) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56; - if (0 > _scrollTo) return; - _controller.animateTo( - _scrollTo, - duration: Duration(milliseconds: 250), - curve: Curves.ease - ); + if (_freeScroll) return; + _scrollToLyric(); }); //Track change = exit lyrics _mediaItemSub = AudioService.currentMediaItemStream.listen((event) { - if (event.id != widget.trackId) - Navigator.of(context).pop(); + if (event.id != widget.trackId) Navigator.of(context).pop(); }); super.initState(); @@ -98,93 +101,151 @@ class _LyricsScreenState extends State { @override void dispose() { - if (_timer != null) - _timer.cancel(); - if (_mediaItemSub != null) - _mediaItemSub.cancel(); + if (_timer != null) _timer.cancel(); + if (_mediaItemSub != null) _mediaItemSub.cancel(); //Stop visualizer - if (settings.lyricsVisualizer) - playerHelper.stopVisualizer(); + if (settings.lyricsVisualizer) playerHelper.stopVisualizer(); super.dispose(); } - @override Widget build(BuildContext context) { return Scaffold( - appBar: FreezerAppBar('Lyrics'.i18n), - body: Stack( - children: [ - //Visualizer - if (settings.lyricsVisualizer) - Align( - alignment: Alignment.bottomCenter, - child: StreamBuilder( - stream: playerHelper.visualizerStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - List 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, - )), - ); - } + appBar: FreezerAppBar('Lyrics'.i18n, + height: _freeScroll ? 100 : 56, + bottom: _freeScroll + ? PreferredSize( + preferredSize: Size.fromHeight(46), + child: Theme( + data: settings.themeData.copyWith( + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Colors.white)))), + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + setState(() => _freeScroll = false); + _scrollToLyric(); + }, + child: Text( + _currentIndex >= 0 + ? lyrics.lyrics[_currentIndex].text + : '...', + textAlign: TextAlign.center, + ), + style: ButtonStyle( + foregroundColor: + MaterialStateProperty.all(Colors.white))) + ], + )), + )) + : null), + body: Stack( + children: [ + //Visualizer + if (settings.lyricsVisualizer) + Align( + alignment: Alignment.bottomCenter, + child: StreamBuilder( + stream: playerHelper.visualizerStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + List 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, + )), + ); + }), ), - ), - //Lyrics - Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, settings.lyricsVisualizer ? 100 : 0), - child: _error ? - //Shouldn't really happen, empty lyrics have own text - ErrorScreen() : - // Loading - _loading ? Padding( - padding: EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator() - ], - ), - ) : ListView.builder( - controller: _controller, - itemCount: lyrics.lyrics.length, - itemBuilder: (BuildContext context, int 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: InkWell( - borderRadius: BorderRadius.circular(8.0), - onTap: lyrics.id != null ? () => AudioService.seekTo(lyrics.lyrics[i].offset) : null, - 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: _error + ? + //Shouldn't really happen, empty lyrics have own text + ErrorScreen() + : + // Loading + _loading + ? Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + ), ) - ); - }, - ), - ) - ], - ) - ); + : NotificationListener( + onNotification: (Notification notification) { + if (notification is! ScrollEndNotification) + return false; + if (_freeScroll) return false; + double _currentScroll = _controller.position.pixels; + double _expectedScroll = (height * _currentIndex) - + (MediaQuery.of(context).size.height / 2) + + (height / 2) + + 56; + print( + 'current: $_currentScroll, expected: $_expectedScroll'); + if (_currentScroll != _expectedScroll) + setState(() => _freeScroll = true); + return false; + }, + child: ListView.builder( + controller: _controller, + itemCount: lyrics.lyrics.length, + itemBuilder: (BuildContext context, int 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: InkWell( + borderRadius: + BorderRadius.circular(8.0), + onTap: lyrics.id != null + ? () => AudioService.seekTo( + lyrics.lyrics[i].offset) + : null, + child: Center( + child: Text( + lyrics.lyrics[i].text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 26.0, + fontWeight: + (_currentIndex == i) + ? FontWeight.bold + : FontWeight.normal), + ), + )))); + }, + )), + ) + ], + )); } } diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 9a9a06d..c5cdc55 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:audio_service/audio_service.dart'; import 'package:country_pickers/country.dart'; import 'package:country_pickers/country_picker_dialog.dart'; @@ -9,6 +11,12 @@ import 'package:flutter_material_color_picker/flutter_material_color_picker.dart import 'package:fluttericon/font_awesome5_icons.dart'; import 'package:fluttericon/web_symbols_icons.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:package_info/package_info.dart'; +import 'package:path_provider_ex/path_provider_ex.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:scrobblenaut/scrobblenaut.dart'; +import 'package:url_launcher/url_launcher.dart'; + import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; @@ -18,17 +26,9 @@ 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:package_info/package_info.dart'; -import 'package:path_provider_ex/path_provider_ex.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:freezer/translations.i18n.dart'; -import 'package:scrobblenaut/scrobblenaut.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../settings.dart'; -import '../main.dart'; - -import 'dart:io'; +import 'package:freezer/settings.dart'; +import 'package:freezer/main.dart'; class SettingsScreen extends StatefulWidget { @override @@ -36,7 +36,6 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - @override Widget build(BuildContext context) { return Scaffold( @@ -46,39 +45,33 @@ class _SettingsScreenState extends State { ListTile( title: Text('General'.i18n), leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)), - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (context) => GeneralSettings() - )), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => GeneralSettings())), ), ListTile( title: Text('Download Settings'.i18n), - leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)), - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (context) => DownloadsSettings() - )), + leading: + LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => DownloadsSettings())), ), ListTile( title: Text('Appearance'.i18n), leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)), - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => AppearanceSettings()) - ), + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => AppearanceSettings())), ), ListTile( title: Text('Quality'.i18n), leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)), - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => QualitySettings()) - ), + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => QualitySettings())), ), ListTile( title: Text('Deezer'.i18n), leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)), - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => DeezerSettings() - )), + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => DeezerSettings())), ), //Language select ListTile( @@ -86,55 +79,53 @@ class _SettingsScreenState extends State { leading: LeadingIcon(Icons.language, color: Color(0xff009a85)), onTap: () { showDialog( - context: context, - builder: (context) => SimpleDialog( - title: Text('Select language'.i18n), - children: List.generate(languages.length, (int i) { - Language l = languages[i]; - return ListTile( - title: Text(l.name), - subtitle: Text("${l.locale}-${l.country}"), - onTap: () async { - setState(() => settings.language = "${l.locale}_${l.country}"); - await settings.save(); - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Language'.i18n), - content: Text('Language changed, please restart Freezer to apply!'.i18n), - actions: [ - TextButton( - child: Text('OK'), - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - ) - ], - ); - } + context: context, + builder: (context) => SimpleDialog( + title: Text('Select language'.i18n), + children: List.generate(languages.length, (int i) { + Language l = languages[i]; + return ListTile( + title: Text(l.name), + subtitle: Text("${l.locale}-${l.country}"), + onTap: () async { + setState(() => + settings.language = "${l.locale}_${l.country}"); + await settings.save(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Language'.i18n), + content: Text( + 'Language changed, please restart Freezer to apply!' + .i18n), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ) + ], + ); + }); + }, ); - }, - ); - }) - ) - ); + }))); }, ), ListTile( title: Text('Updates'.i18n), leading: LeadingIcon(Icons.update, color: Color(0xff2ba766)), - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => UpdaterScreen() - )), + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => UpdaterScreen())), ), ListTile( title: Text('About'.i18n), leading: LeadingIcon(Icons.info, color: Colors.grey), - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => CreditsScreen() - )), + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => CreditsScreen())), ), ], ), @@ -148,8 +139,6 @@ class AppearanceSettings extends StatefulWidget { } class _AppearanceSettingsState extends State { - - ColorSwatch _swatch(int c) => ColorSwatch(c, {500: Color(c)}); @override @@ -160,60 +149,59 @@ class _AppearanceSettingsState extends State { children: [ ListTile( title: Text('Theme'.i18n), - subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'), + subtitle: Text('Currently'.i18n + + ': ${settings.theme.toString().split('.').last}'), leading: Icon(Icons.color_lens), onTap: () { showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: Text('Select theme'.i18n), - children: [ - SimpleDialogOption( - child: Text('Light'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Light); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - SimpleDialogOption( - child: Text('Dark'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Dark); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - SimpleDialogOption( - child: Text('Black (AMOLED)'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Black); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - SimpleDialogOption( - child: Text('Deezer (Dark)'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Deezer); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - ], - ); - } - ); + context: context, + builder: (context) { + return SimpleDialog( + title: Text('Select theme'.i18n), + children: [ + SimpleDialogOption( + child: Text('Light'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Light); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + SimpleDialogOption( + child: Text('Dark'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Dark); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + SimpleDialogOption( + child: Text('Black (AMOLED)'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Black); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + SimpleDialogOption( + child: Text('Deezer (Dark)'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Deezer); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + ], + ); + }); }, ), - ListTile( - title: Text('Use system theme'.i18n), - trailing: Switch( + SwitchListTile( + title: Text('Use system theme'.i18n), value: settings.useSystemTheme, onChanged: (bool v) async { setState(() { @@ -222,112 +210,101 @@ class _AppearanceSettingsState extends State { updateTheme(); await settings.save(); }, - ), - leading: Icon(Icons.android) - ), + secondary: 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()) - ); + context: context, + builder: (context) => + FontSelector(() => Navigator.of(context).pop())); }, ), - ListTile( + SwitchListTile( title: Text('Player gradient background'.i18n), - leading: Icon(Icons.colorize), - trailing: Switch( - value: settings.colorGradientBackground, - onChanged: (bool v) async { - setState(() => settings.colorGradientBackground = v); - await settings.save(); - }, - ), + secondary: Icon(Icons.colorize), + value: settings.colorGradientBackground, + onChanged: (bool v) async { + setState(() => settings.colorGradientBackground = v); + await settings.save(); + }, ), - ListTile( + SwitchListTile( 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(); - }, - ), + secondary: Icon(Icons.blur_on), + value: settings.blurPlayerBackground, + onChanged: (bool v) async { + setState(() => settings.blurPlayerBackground = v); + await settings.save(); + }, ), - ListTile( + SwitchListTile( 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; - } - }, - ), + subtitle: Text( + 'Show visualizers on lyrics page. WARNING: Requires microphone permission!' + .i18n), + secondary: Icon(Icons.equalizer), + 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), subtitle: Text( 'Selected color'.i18n, - style: TextStyle( - color: settings.primaryColor - ), + style: TextStyle(color: settings.primaryColor), ), onTap: () { showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Primary color'.i18n), - content: Container( - height: 240, - child: MaterialColorPicker( - colors: [ - ...Colors.primaries, - //Logo colors - _swatch(0xffeca704), - _swatch(0xffbe3266), - _swatch(0xff4b2e7e), - _swatch(0xff384697), - _swatch(0xff0880b5), - _swatch(0xff009a85), - _swatch(0xff2ba766) - ], - allowShades: false, - selectedColor: settings.primaryColor, - onMainColorChange: (ColorSwatch color) { - setState(() { - settings.primaryColor = color; - }); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, + context: context, + builder: (context) { + return AlertDialog( + title: Text('Primary color'.i18n), + content: Container( + height: 240, + child: MaterialColorPicker( + colors: [ + ...Colors.primaries, + //Logo colors + _swatch(0xffeca704), + _swatch(0xffbe3266), + _swatch(0xff4b2e7e), + _swatch(0xff384697), + _swatch(0xff0880b5), + _swatch(0xff009a85), + _swatch(0xff2ba766) + ], + allowShades: false, + selectedColor: settings.primaryColor, + onMainColorChange: (ColorSwatch color) { + setState(() { + settings.primaryColor = color; + }); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), ), - ), - ); - } - ); + ); + }); }, ), - ListTile( + SwitchListTile( title: Text('Use album art primary color'.i18n), subtitle: Text('Warning: might be buggy'.i18n), - leading: Icon(Icons.invert_colors), - trailing: Switch( - value: settings.useArtColor, - onChanged: (v) => setState(() => settings.updateUseArtColor(v)), - ), + secondary: Icon(Icons.invert_colors), + value: settings.useArtColor, + onChanged: (v) => setState(() => settings.updateUseArtColor(v)), ), //Display mode ListTile( @@ -337,22 +314,22 @@ class _AppearanceSettingsState extends State { onTap: () async { List modes = await FlutterDisplayMode.supported; showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: Text('Display mode'.i18n), - children: List.generate(modes.length, (i) => SimpleDialogOption( - child: Text(modes[i].toString()), - onPressed: () async { - settings.displayMode = i; - await settings.save(); - await FlutterDisplayMode.setMode(modes[i]); - Navigator.of(context).pop(); - }, - )) - ); - } - ); + context: context, + builder: (context) { + return SimpleDialog( + title: Text('Display mode'.i18n), + children: List.generate( + modes.length, + (i) => SimpleDialogOption( + child: Text(modes[i].toString()), + onPressed: () async { + settings.displayMode = i; + await settings.save(); + await FlutterDisplayMode.setMode(modes[i]); + Navigator.of(context).pop(); + }, + ))); + }); }, ) ], @@ -364,48 +341,50 @@ class _AppearanceSettingsState extends State { class FontSelector extends StatefulWidget { final Function callback; - FontSelector(this.callback, {Key key}): super(key: key); + FontSelector(this.callback, {Key key}) : super(key: key); @override _FontSelectorState createState() => _FontSelectorState(); } class _FontSelectorState extends State { - String query = ''; List get fonts { - return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList(); + 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: [ - TextButton( - onPressed: () async { - setState(() => settings.font = font); - await settings.save(); - Navigator.of(context).pop(); - widget.callback(); - //Global setState - updateTheme(); - }, - child: Text('Apply'.i18n), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - widget.callback(); - }, - child: Text('Cancel'), - ) - ], - ) - ); + 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: [ + TextButton( + onPressed: () async { + setState(() => settings.font = font); + await settings.save(); + Navigator.of(context).pop(); + widget.callback(); + //Global setState + updateTheme(); + }, + child: Text('Apply'.i18n), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + widget.callback(); + }, + child: Text('Cancel'), + ) + ], + )); } @override @@ -416,23 +395,21 @@ class _FontSelectorState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), child: TextField( - decoration: InputDecoration( - hintText: 'Search'.i18n - ), + decoration: InputDecoration(hintText: 'Search'.i18n), onChanged: (q) => setState(() => query = q), ), ), - ...List.generate(fonts.length, (i) => SimpleDialogOption( - child: Text(fonts[i]), - onPressed: () => onTap(fonts[i]), - )) + ...List.generate( + fonts.length, + (i) => SimpleDialogOption( + child: Text(fonts[i]), + onPressed: () => onTap(fonts[i]), + )) ], ); } } - - class QualitySettings extends StatefulWidget { @override _QualitySettingsState createState() => _QualitySettingsState(); @@ -475,16 +452,14 @@ class _QualitySettingsState extends State { } class QualityPicker extends StatefulWidget { - final String field; - QualityPicker(this.field, {Key key}): super(key: key); + QualityPicker(this.field, {Key key}) : super(key: key); @override _QualityPickerState createState() => _QualityPickerState(); } class _QualityPickerState extends State { - AudioQuality _quality; @override @@ -497,13 +472,17 @@ class _QualityPickerState extends State { void _getQuality() { switch (widget.field) { case 'mobile': - _quality = settings.mobileQuality; break; + _quality = settings.mobileQuality; + break; case 'wifi': - _quality = settings.wifiQuality; break; + _quality = settings.wifiQuality; + break; case 'download': - _quality = settings.downloadQuality; break; + _quality = settings.downloadQuality; + break; case 'offline': - _quality = settings.offlineQuality; break; + _quality = settings.offlineQuality; + break; } } @@ -522,9 +501,11 @@ class _QualityPickerState extends State { settings.updateAudioServiceQuality(); break; case 'download': - settings.downloadQuality = _quality; break; + settings.downloadQuality = _quality; + break; case 'offline': - settings.offlineQuality = _quality; break; + settings.offlineQuality = _quality; + break; } await settings.save(); await settings.updateAudioServiceQuality(); @@ -534,38 +515,30 @@ class _QualityPickerState extends State { Widget build(BuildContext context) { return Column( children: [ - ListTile( + RadioListTile( title: Text('MP3 128kbps'), - leading: Radio( - groupValue: _quality, - value: AudioQuality.MP3_128, - onChanged: (q) => _updateQuality(q), - ), + groupValue: _quality, + value: AudioQuality.MP3_128, + onChanged: (q) => _updateQuality(q), ), - ListTile( + RadioListTile( title: Text('MP3 320kbps'), - leading: Radio( - groupValue: _quality, - value: AudioQuality.MP3_320, - onChanged: (q) => _updateQuality(q), - ), + groupValue: _quality, + value: AudioQuality.MP3_320, + onChanged: (q) => _updateQuality(q), ), - ListTile( + RadioListTile( title: Text('FLAC'), - leading: Radio( - groupValue: _quality, - value: AudioQuality.FLAC, - onChanged: (q) => _updateQuality(q), - ), + groupValue: _quality, + value: AudioQuality.FLAC, + onChanged: (q) => _updateQuality(q), ), if (widget.field == 'download') - ListTile( + RadioListTile( title: Text('Ask before downloading'.i18n), - leading: Radio( - groupValue: _quality, - value: AudioQuality.ASK, - onChanged: (q) => _updateQuality(q), - ) + groupValue: _quality, + value: AudioQuality.ASK, + onChanged: (q) => _updateQuality(q), ) ], ); @@ -578,42 +551,42 @@ class ContentLanguage { ContentLanguage(this.code, this.name); static List get all => [ - ContentLanguage("cs", "Čeština"), - ContentLanguage("da", "Dansk"), - ContentLanguage("de", "Deutsch"), - ContentLanguage("en", "English"), - ContentLanguage("us", "English (us)"), - ContentLanguage("es", "Español"), - ContentLanguage("mx", "Español (latam)"), - ContentLanguage("fr", "Français"), - ContentLanguage("hr", "Hrvatski"), - ContentLanguage("id", "Indonesia"), - ContentLanguage("it", "Italiano"), - ContentLanguage("hu", "Magyar"), - ContentLanguage("ms", "Melayu"), - ContentLanguage("nl", "Nederlands"), - ContentLanguage("no", "Norsk"), - ContentLanguage("pl", "Polski"), - ContentLanguage("br", "Português (br)"), - ContentLanguage("pt", "Português (pt)"), - ContentLanguage("ro", "Română"), - ContentLanguage("sk", "Slovenčina"), - ContentLanguage("sl", "Slovenščina"), - ContentLanguage("sq", "Shqip"), - ContentLanguage("sr", "Srpski"), - ContentLanguage("fi", "Suomi"), - ContentLanguage("sv", "Svenska"), - ContentLanguage("tr", "Türkçe"), - ContentLanguage("bg", "Български"), - ContentLanguage("ru", "Pусский"), - ContentLanguage("uk", "Українська"), - ContentLanguage("he", "עִברִית"), - ContentLanguage("ar", "العربیة"), - ContentLanguage("cn", "中文"), - ContentLanguage("ja", "日本語"), - ContentLanguage("ko", "한국어"), - ContentLanguage("th", "ภาษาไทย"), - ]; + ContentLanguage("cs", "Čeština"), + ContentLanguage("da", "Dansk"), + ContentLanguage("de", "Deutsch"), + ContentLanguage("en", "English"), + ContentLanguage("us", "English (us)"), + ContentLanguage("es", "Español"), + ContentLanguage("mx", "Español (latam)"), + ContentLanguage("fr", "Français"), + ContentLanguage("hr", "Hrvatski"), + ContentLanguage("id", "Indonesia"), + ContentLanguage("it", "Italiano"), + ContentLanguage("hu", "Magyar"), + ContentLanguage("ms", "Melayu"), + ContentLanguage("nl", "Nederlands"), + ContentLanguage("no", "Norsk"), + ContentLanguage("pl", "Polski"), + ContentLanguage("br", "Português (br)"), + ContentLanguage("pt", "Português (pt)"), + ContentLanguage("ro", "Română"), + ContentLanguage("sk", "Slovenčina"), + ContentLanguage("sl", "Slovenščina"), + ContentLanguage("sq", "Shqip"), + ContentLanguage("sr", "Srpski"), + ContentLanguage("fi", "Suomi"), + ContentLanguage("sv", "Svenska"), + ContentLanguage("tr", "Türkçe"), + ContentLanguage("bg", "Български"), + ContentLanguage("ru", "Pусский"), + ContentLanguage("uk", "Українська"), + ContentLanguage("he", "עִברִית"), + ContentLanguage("ar", "العربیة"), + ContentLanguage("cn", "中文"), + ContentLanguage("ja", "日本語"), + ContentLanguage("ko", "한국어"), + ContentLanguage("th", "ภาษาไทย"), + ]; } class DeezerSettings extends StatefulWidget { @@ -630,55 +603,59 @@ class _DeezerSettingsState extends State { children: [ ListTile( title: Text('Content language'.i18n), - subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'), + subtitle: Text('Not app language, used in headers. Now'.i18n + + ': ${settings.deezerLanguage}'), leading: Icon(Icons.language), onTap: () { showDialog( - context: context, - builder: (context) => SimpleDialog( - title: Text('Select language'.i18n), - children: List.generate(ContentLanguage.all.length, (i) => ListTile( - title: Text(ContentLanguage.all[i].name), - subtitle: Text(ContentLanguage.all[i].code), - onTap: () async { - setState(() => settings.deezerLanguage = ContentLanguage.all[i].code); - await settings.save(); - Navigator.of(context).pop(); - }, - )), - ) - ); + context: context, + builder: (context) => SimpleDialog( + title: Text('Select language'.i18n), + children: List.generate( + ContentLanguage.all.length, + (i) => ListTile( + title: Text(ContentLanguage.all[i].name), + subtitle: Text(ContentLanguage.all[i].code), + onTap: () async { + setState(() => settings.deezerLanguage = + ContentLanguage.all[i].code); + await settings.save(); + Navigator.of(context).pop(); + }, + )), + )); }, ), ListTile( title: Text('Content country'.i18n), - subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'), + subtitle: Text('Country used in headers. Now'.i18n + + ': ${settings.deezerCountry}'), leading: Icon(Icons.vpn_lock), onTap: () { showDialog( - context: context, - builder: (context) => CountryPickerDialog( - titlePadding: EdgeInsets.all(8.0), - isSearchable: true, - onValuePicked: (Country country) { - setState(() => settings.deezerCountry = country.isoCode); - settings.save(); - }, - ) - ); + context: context, + builder: (context) => CountryPickerDialog( + titlePadding: EdgeInsets.all(8.0), + isSearchable: true, + onValuePicked: (Country country) { + setState( + () => settings.deezerCountry = country.isoCode); + settings.save(); + }, + )); }, ), - ListTile( + SwitchListTile( title: Text('Log tracks'.i18n), - subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n), - trailing: Switch( - value: settings.logListen, - onChanged: (bool v) { - setState(() => settings.logListen = v); - settings.save(); - }, - ), - leading: Icon(Icons.history_toggle_off), + subtitle: Text( + 'Send track listen logs to Deezer, enable it for features like Flow to work properly' + .i18n), + value: settings.logListen, + secondary: Icon(Icons.history_toggle_off), + onChanged: (bool v) { + setState(() => settings.logListen = v); + settings.save(); + }, ), //TODO: Reimplement proxy // ListTile( @@ -736,17 +713,16 @@ class _DeezerSettingsState extends State { } class FilenameTemplateDialog extends StatefulWidget { - final String initial; final Function onSave; - FilenameTemplateDialog(this.initial, this.onSave, {Key key}): super(key: key); + FilenameTemplateDialog(this.initial, this.onSave, {Key key}) + : super(key: key); @override _FilenameTemplateDialogState createState() => _FilenameTemplateDialogState(); } class _FilenameTemplateDialogState extends State { - TextEditingController _controller; String _new; @@ -771,8 +747,10 @@ class _FilenameTemplateDialogState extends State { ), Container(height: 8.0), Text( - 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' + - "If you want to use custom directory naming - use '/' as directory separator.".i18n, + 'Valid variables are'.i18n + + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' + + "If you want to use custom directory naming - use '/' as directory separator." + .i18n, style: TextStyle( fontSize: 12.0, ), @@ -787,7 +765,8 @@ class _FilenameTemplateDialogState extends State { TextButton( child: Text('Reset'.i18n), onPressed: () { - _controller.value = _controller.value.copyWith(text: '%artist% - %title%'); + _controller.value = + _controller.value.copyWith(text: '%artist% - %title%'); _new = '%artist% - %title%'; }, ), @@ -807,16 +786,15 @@ class _FilenameTemplateDialogState extends State { } } - class DownloadsSettings extends StatefulWidget { @override _DownloadsSettingsState createState() => _DownloadsSettingsState(); } class _DownloadsSettingsState extends State { - double _downloadThreads = settings.downloadThreads.toDouble(); - TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator); + TextEditingController _artistSeparatorController = + TextEditingController(text: settings.artistSeparator); @override Widget build(BuildContext context) { @@ -833,11 +811,13 @@ class _DownloadsSettingsState extends State { if (!(await Permission.storage.request().isGranted)) return; //Navigate Navigator.of(context).push(MaterialPageRoute( - builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) async { - setState(() => settings.downloadPath = p); - await settings.save(); - },) - )); + builder: (context) => DirectoryPicker( + settings.downloadPath, + onSelect: (String p) async { + setState(() => settings.downloadPath = p); + await settings.save(); + }, + ))); }, ), ListTile( @@ -848,208 +828,185 @@ class _DownloadsSettingsState extends State { showDialog( context: context, builder: (context) { - return FilenameTemplateDialog(settings.downloadFilename, (f) async { + return FilenameTemplateDialog(settings.downloadFilename, + (f) async { setState(() => settings.downloadFilename = f); await settings.save(); }); - } - ); + }); }, ), ListTile( title: Text('Singleton naming'.i18n), - subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'), + subtitle: + Text('Currently'.i18n + ': ${settings.singletonFilename}'), leading: Icon(Icons.text_format), onTap: () { showDialog( context: context, builder: (context) { - return FilenameTemplateDialog(settings.singletonFilename, (f) async { + return FilenameTemplateDialog(settings.singletonFilename, + (f) async { setState(() => settings.singletonFilename = f); await settings.save(); }); - } - ); + }); }, ), Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text( - 'Download threads'.i18n + ': ${_downloadThreads.round().toString()}', - style: TextStyle( - fontSize: 16.0 - ), + 'Download threads'.i18n + + ': ${_downloadThreads.round().toString()}', + style: TextStyle(fontSize: 16.0), ), ), Slider( - min: 1, - max: 16, - divisions: 15, - value: _downloadThreads, - label: _downloadThreads.round().toString(), - onChanged: (double v) => setState(() => _downloadThreads = v), - onChangeEnd: (double val) async { - _downloadThreads = val; - setState(() { - settings.downloadThreads = _downloadThreads.round(); - _downloadThreads = settings.downloadThreads.toDouble(); - }); - await settings.save(); + min: 1, + max: 16, + divisions: 15, + value: _downloadThreads, + label: _downloadThreads.round().toString(), + onChanged: (double v) => setState(() => _downloadThreads = v), + onChangeEnd: (double val) async { + _downloadThreads = val; + setState(() { + settings.downloadThreads = _downloadThreads.round(); + _downloadThreads = settings.downloadThreads.toDouble(); + }); + await settings.save(); - //Prevent null - if (val > 8 && cache.threadsWarning != true) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Warning'.i18n), - content: Text('Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n), - actions: [ - TextButton( - child: Text('Dismiss'.i18n), - onPressed: () => Navigator.of(context).pop(), - ) - ], - ); - } - ); + //Prevent null + if (val > 8 && cache.threadsWarning != true) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Warning'.i18n), + content: Text( + 'Using too many concurrent downloads on older/weaker devices might cause crashes!' + .i18n), + actions: [ + TextButton( + child: Text('Dismiss'.i18n), + onPressed: () => Navigator.of(context).pop(), + ) + ], + ); + }); - cache.threadsWarning = true; - await cache.save(); - } - } - ), + cache.threadsWarning = true; + await cache.save(); + } + }), FreezerDivider(), ListTile( title: Text('Tags'.i18n), leading: Icon(Icons.label), - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TagSelectionScreen() - )), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => TagSelectionScreen())), ), - ListTile( + SwitchListTile( title: Text('Create folders for artist'.i18n), - trailing: Switch( - value: settings.artistFolder, - onChanged: (v) { - setState(() => settings.artistFolder = v); - settings.save(); - }, - ), - leading: Icon(Icons.folder), + value: settings.artistFolder, + onChanged: (v) { + setState(() => settings.artistFolder = v); + settings.save(); + }, + secondary: Icon(Icons.folder), ), - ListTile( - title: Text('Create folders for albums'.i18n), - trailing: Switch( + SwitchListTile( + title: Text('Create folders for albums'.i18n), value: settings.albumFolder, onChanged: (v) { setState(() => settings.albumFolder = v); settings.save(); }, - ), - leading: Icon(Icons.folder) - ), - ListTile( - title: Text('Create folder for playlist'.i18n), - trailing: Switch( + secondary: Icon(Icons.folder)), + SwitchListTile( + title: Text('Create folder for playlist'.i18n), value: settings.playlistFolder, onChanged: (v) { setState(() => settings.playlistFolder = v); settings.save(); }, - ), - leading: Icon(Icons.folder) - ), + secondary: Icon(Icons.folder)), FreezerDivider(), - ListTile( - title: Text('Separate albums by discs'.i18n), - trailing: Switch( + SwitchListTile( + title: Text('Separate albums by discs'.i18n), value: settings.albumDiscFolder, onChanged: (v) { setState(() => settings.albumDiscFolder = v); settings.save(); }, - ), - leading: Icon(Icons.album) - ), - ListTile( - title: Text('Overwrite already downloaded files'.i18n), - trailing: Switch( + secondary: Icon(Icons.album)), + SwitchListTile( + title: Text('Overwrite already downloaded files'.i18n), value: settings.overwriteDownload, onChanged: (v) { setState(() => settings.overwriteDownload = v); settings.save(); }, - ), - leading: Icon(Icons.delete) - ), - ListTile( - title: Text('Download .LRC lyrics'.i18n), - trailing: Switch( + secondary: Icon(Icons.delete)), + SwitchListTile( + title: Text('Download .LRC lyrics'.i18n), value: settings.downloadLyrics, onChanged: (v) { setState(() => settings.downloadLyrics = v); settings.save(); }, - ), - leading: Icon(Icons.subtitles) - ), + secondary: Icon(Icons.subtitles)), FreezerDivider(), - ListTile( - title: Text('Save cover file for every track'.i18n), - trailing: Switch( + SwitchListTile( + title: Text('Save cover file for every track'.i18n), value: settings.trackCover, onChanged: (v) { setState(() => settings.trackCover = v); settings.save(); }, - ), - leading: Icon(Icons.image) - ), - ListTile( - title: Text('Save album cover'.i18n), - trailing: Switch( + secondary: Icon(Icons.image)), + SwitchListTile( + title: Text('Save album cover'.i18n), value: settings.albumCover, onChanged: (v) { setState(() => settings.albumCover = v); settings.save(); }, - ), - leading: Icon(Icons.image) - ), + secondary: Icon(Icons.image)), ListTile( - title: Text('Album cover resolution'.i18n), - subtitle: Text("WARNING: Resolutions above 1200 aren't officially supported".i18n), - leading: Icon(Icons.image), - trailing: Container( - width: 75.0, - child: DropdownButton( - value: settings.albumArtResolution, - items: [400, 800, 1000, 1200, 1400, 1600, 1800].map>((int i) => DropdownMenuItem( - value: i, - child: Text(i.toString()), - )).toList(), - onChanged: (int n) async { - setState(() { - settings.albumArtResolution = n; - }); - await settings.save(); - }, - ) - ) - ), - ListTile( - title: Text('Create .nomedia files'.i18n), - subtitle: Text('To prevent gallery being filled with album art'.i18n), - trailing: Switch( + title: Text('Album cover resolution'.i18n), + subtitle: Text( + "WARNING: Resolutions above 1200 aren't officially supported" + .i18n), + leading: Icon(Icons.image), + trailing: Container( + width: 75.0, + child: DropdownButton( + value: settings.albumArtResolution, + items: [400, 800, 1000, 1200, 1400, 1600, 1800] + .map>( + (int i) => DropdownMenuItem( + value: i, + child: Text(i.toString()), + )) + .toList(), + onChanged: (int n) async { + setState(() { + settings.albumArtResolution = n; + }); + await settings.save(); + }, + ))), + SwitchListTile( + title: Text('Create .nomedia files'.i18n), + subtitle: + Text('To prevent gallery being filled with album art'.i18n), value: settings.nomediaFiles, onChanged: (v) { setState(() => settings.nomediaFiles = v); settings.save(); }, - ), - leading: Icon(Icons.insert_drive_file) - ), + secondary: Icon(Icons.insert_drive_file)), ListTile( title: Text('Artist separator'.i18n), leading: Icon(WebSymbols.tag), @@ -1069,8 +1026,7 @@ class _DownloadsSettingsState extends State { title: Text('Download Log'.i18n), leading: Icon(Icons.sticky_note_2), onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (context) => DownloadLogViewer()) - ), + MaterialPageRoute(builder: (context) => DownloadLogViewer())), ) ], ), @@ -1090,7 +1046,6 @@ class TagSelectionScreen extends StatefulWidget { } class _TagSelectionScreenState extends State { - List tags = [ TagOption("Title".i18n, 'title'), TagOption("Album".i18n, 'album'), @@ -1115,26 +1070,28 @@ class _TagSelectionScreenState extends State { 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(); - }, - ), - )), + 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 _GeneralSettingsState createState() => _GeneralSettingsState(); @@ -1147,81 +1104,75 @@ class _GeneralSettingsState extends State { appBar: FreezerAppBar('General'.i18n), body: ListView( children: [ - ListTile( + SwitchListTile( title: Text('Offline mode'.i18n), subtitle: Text('Will be overwritten on start.'.i18n), - trailing: Switch( - value: settings.offlineMode, - onChanged: (bool v) { - if (v) { - setState(() => settings.offlineMode = true); - return; - } - showDialog( - context: context, - builder: (context) { - deezerAPI.authorize().then((v) { - if (v) { - setState(() => settings.offlineMode = false); - } else { - Fluttertoast.showToast( - msg: 'Error logging in, check your internet connections.'.i18n, - gravity: ToastGravity.BOTTOM, - toastLength: Toast.LENGTH_SHORT - ); - } - Navigator.of(context).pop(); - }); - return AlertDialog( - title: Text('Logging in...'.i18n), - content: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator() - ], - ) - ); - } - ); - }, - ), - leading: Icon(Icons.lock), + value: settings.offlineMode, + secondary: Icon(Icons.lock), + onChanged: (bool v) { + if (v) { + setState(() => settings.offlineMode = true); + return; + } + showDialog( + context: context, + builder: (context) { + deezerAPI.authorize().then((v) { + if (v) { + setState(() => settings.offlineMode = false); + } else { + Fluttertoast.showToast( + msg: + 'Error logging in, check your internet connections.' + .i18n, + gravity: ToastGravity.BOTTOM, + toastLength: Toast.LENGTH_SHORT); + } + Navigator.of(context).pop(); + }); + return AlertDialog( + title: Text('Logging in...'.i18n), + content: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + )); + }); + }, ), - ListTile( - title: Text('Copy ARL'.i18n), - subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n), - leading: Icon(Icons.lock), - onTap: () async { - Clipboard.setData(ClipboardData(text: settings.arl)); - await Fluttertoast.showToast( - msg: 'Copied'.i18n, - ); + SwitchListTile( + title: Text('Enable equalizer'.i18n), + subtitle: Text( + 'Might enable some equalizer apps to work. Requires restart of Freezer' + .i18n), + secondary: Icon(Icons.equalizer), + value: settings.enableEqualizer, + onChanged: (v) async { + setState(() => settings.enableEqualizer = v); + settings.save(); + }, + ), + SwitchListTile( + title: Text('Ignore interruptions'.i18n), + subtitle: Text('Requires app restart to apply!'.i18n), + secondary: Icon(Icons.not_interested), + value: settings.ignoreInterruptions, + onChanged: (bool v) async { + setState(() => settings.ignoreInterruptions = v); + await settings.save(); }, ), - 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( - (settings.lastFMPassword != null && settings.lastFMUsername != null) + subtitle: Text((settings.lastFMPassword != null && + settings.lastFMUsername != null) ? 'Log out'.i18n - : 'Login to enable scrobbling.'.i18n - ), + : 'Login to enable scrobbling.'.i18n), leading: Icon(FontAwesome5.lastfm), onTap: () async { //Log out - if (settings.lastFMPassword != null && settings.lastFMUsername != null) { + if (settings.lastFMPassword != null && + settings.lastFMUsername != null) { settings.lastFMUsername = null; settings.lastFMPassword = null; playerHelper.scrobblenaut = null; @@ -1231,28 +1182,31 @@ class _GeneralSettingsState extends State { return; } await showDialog( - context: context, - builder: (context) => LastFMLogin() - ); + context: context, builder: (context) => LastFMLogin()); setState(() {}); }, ), ListTile( - title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),), - leading: Icon(Icons.exit_to_app), - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Log out'.i18n), + title: Text( + 'Log out'.i18n, + style: TextStyle(color: Colors.red), + ), + leading: Icon(Icons.exit_to_app), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Log out'.i18n), // content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n), - content: Text('Restart of app is required to properly log out!'.i18n), - actions: [ - TextButton( - child: Text('Cancel'.i18n), - onPressed: () => Navigator.of(context).pop(), - ), + content: Text( + 'Restart of app is required to properly log out!' + .i18n), + actions: [ + TextButton( + child: Text('Cancel'.i18n), + onPressed: () => Navigator.of(context).pop(), + ), // FlatButton( // child: Text('(ARL ONLY) Continue'.i18n), // onPressed: () async { @@ -1260,33 +1214,34 @@ class _GeneralSettingsState extends State { // Navigator.of(context).pop(); // }, // ), - TextButton( - child: Text('Log out & Exit'.i18n), - onPressed: () async { - try {AudioService.stop();} catch (e) {} - await logOut(); - await DownloadManager.platform.invokeMethod("kill"); - SystemNavigator.pop(); - }, - ) - ], - ); - } - ); - } - ), + TextButton( + child: Text('Log out & Exit'.i18n), + onPressed: () async { + try { + AudioService.stop(); + } catch (e) {} + await logOut(); + await DownloadManager.platform + .invokeMethod("kill"); + SystemNavigator.pop(); + }, + ) + ], + ); + }); + }), ListTile( - title: Text('Ignore interruptions'.i18n), - subtitle: Text('Requires app restart to apply!'.i18n), - leading: Icon(Icons.not_interested), - trailing: Switch( - value: settings.ignoreInterruptions, - onChanged: (bool v) async { - setState(() => settings.ignoreInterruptions = v); - await settings.save(); - }, - ), - ) + title: Text('Copy ARL'.i18n), + subtitle: + Text('Copy userToken/ARL Cookie for use in other apps.'.i18n), + leading: Icon(Icons.lock), + onTap: () async { + Clipboard.setData(ClipboardData(text: settings.arl)); + await Fluttertoast.showToast( + msg: 'Copied'.i18n, + ); + }, + ), ], ), ); @@ -1299,7 +1254,6 @@ class LastFMLogin extends StatefulWidget { } class _LastFMLoginState extends State { - String _username = ''; String _password = ''; @@ -1311,17 +1265,13 @@ class _LastFMLoginState extends State { mainAxisSize: MainAxisSize.min, children: [ TextField( - decoration: InputDecoration( - hintText: 'Username'.i18n - ), + decoration: InputDecoration(hintText: 'Username'.i18n), onChanged: (v) => _username = v, ), Container(height: 8.0), TextField( obscureText: true, - decoration: InputDecoration( - hintText: 'Password'.i18n - ), + decoration: InputDecoration(hintText: 'Password'.i18n), onChanged: (v) => _password = v, ) ], @@ -1340,8 +1290,7 @@ class _LastFMLoginState extends State { apiKey: 'b6ab5ae967bcd8b10b23f68f42493829', apiSecret: '861b0dff9a8a574bec747f9dab8b82bf', username: _username, - password: _password - ); + password: _password); } catch (e) { Fluttertoast.showToast(msg: 'Authorization error!'.i18n); return; @@ -1359,19 +1308,16 @@ class _LastFMLoginState extends State { } } - class DirectoryPicker extends StatefulWidget { - final String initialPath; final Function onSelect; - DirectoryPicker(this.initialPath, {this.onSelect, Key key}): super(key: key); + DirectoryPicker(this.initialPath, {this.onSelect, Key key}) : super(key: key); @override _DirectoryPickerState createState() => _DirectoryPickerState(); } class _DirectoryPickerState extends State { - String _path; String _previous; String _root; @@ -1394,57 +1340,56 @@ class _DirectoryPickerState extends State { 'Pick-a-Path'.i18n, actions: [ IconButton( - icon: Icon(Icons.sd_card), - onPressed: () { - //Chose storage - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Select storage'.i18n), - content: FutureBuilder( - future: PathProviderEx.getStorageInfo(), - builder: (context, snapshot) { - if (snapshot.hasError) return ErrorScreen(); - if (!snapshot.hasData) return Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator() - ], - ), - ); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...List.generate(snapshot.data.length, (i) { - StorageInfo si = snapshot.data[i]; - return ListTile( - title: Text(si.rootDir), - leading: Icon(Icons.sd_card), - trailing: Text(filesize(si.availableBytes)), - onTap: () { - setState(() { - _path = si.appFilesDir; - //Android 5+ blocks sd card, so this prevents going outside - //app data dir, until permission request fix. - _root = si.rootDir; - if (i != 0) _root = si.appFilesDir; - }); - Navigator.of(context).pop(); - }, + icon: Icon(Icons.sd_card), + onPressed: () { + //Chose storage + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Select storage'.i18n), + content: FutureBuilder( + future: PathProviderEx.getStorageInfo(), + builder: (context, snapshot) { + if (snapshot.hasError) return ErrorScreen(); + if (!snapshot.hasData) + return Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator() + ], + ), ); - }) - ], - ); - }, - ), - ); - } - ); - } - ) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...List.generate(snapshot.data.length, (i) { + StorageInfo si = snapshot.data[i]; + return ListTile( + title: Text(si.rootDir), + leading: Icon(Icons.sd_card), + trailing: Text(filesize(si.availableBytes)), + onTap: () { + setState(() { + _path = si.appFilesDir; + //Android 5+ blocks sd card, so this prevents going outside + //app data dir, until permission request fix. + _root = si.rootDir; + if (i != 0) _root = si.appFilesDir; + }); + Navigator.of(context).pop(); + }, + ); + }) + ], + ); + }, + ), + ); + }); + }) ], ), floatingActionButton: FloatingActionButton( @@ -1458,16 +1403,19 @@ class _DirectoryPickerState extends State { body: FutureBuilder( future: Directory(_path).list().toList(), builder: (BuildContext context, AsyncSnapshot snapshot) { - //On error go to last good path - if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () { - if (_previous == null) { - _resetPath(); - return; - } - setState(() => _path = _previous); - }); - if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),); + if (snapshot.hasError) + Future.delayed(Duration(milliseconds: 50), () { + if (_previous == null) { + _resetPath(); + return; + } + setState(() => _path = _previous); + }); + if (!snapshot.hasData) + return Center( + child: CircularProgressIndicator(), + ); List data = snapshot.data; return ListView( @@ -1483,8 +1431,7 @@ class _DirectoryPickerState extends State { if (_root == _path) { Fluttertoast.showToast( msg: 'Permission denied'.i18n, - gravity: ToastGravity.BOTTOM - ); + gravity: ToastGravity.BOTTOM); return; } _previous = _path; @@ -1506,7 +1453,10 @@ class _DirectoryPickerState extends State { }, ); } - return Container(height: 0, width: 0,); + return Container( + height: 0, + width: 0, + ); }) ], ); @@ -1522,7 +1472,6 @@ class CreditsScreen extends StatefulWidget { } class _CreditsScreenState extends State { - String _version = ''; static final List> translators = [ @@ -1564,50 +1513,42 @@ class _CreditsScreenState extends State { Text( _version, textAlign: TextAlign.center, - style: TextStyle( - fontStyle: FontStyle.italic - ), + style: TextStyle(fontStyle: FontStyle.italic), ), FreezerDivider(), ListTile( title: Text('Telegram Channel'.i18n), subtitle: Text('To get latest releases'.i18n), - leading: Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0), - onTap: () { - launch('https://t.me/freezereleases'); - }, + leading: Icon(FontAwesome5.telegram, + color: Color(0xFF27A2DF), size: 36.0), + onTap: () => launch('https://t.me/freezereleases'), ), ListTile( title: Text('Telegram Group'.i18n), subtitle: Text('Official chat'.i18n), - leading: Icon(FontAwesome5.telegram, color: Colors.cyan, size: 36.0), - onTap: () { - launch('https://t.me/freezerandroid'); - }, + leading: + Icon(FontAwesome5.telegram, color: Colors.cyan, size: 36.0), + onTap: () => launch('https://t.me/freezerandroid'), ), ListTile( title: Text('Discord'.i18n), subtitle: Text('Official Discord server'.i18n), - leading: Icon(FontAwesome5.discord, color: Color(0xff7289da), size: 36.0), - onTap: () { - launch('https://discord.gg/qwJpa3r4dQ'); - }, + leading: Icon(FontAwesome5.discord, + color: Color(0xff7289da), size: 36.0), + onTap: () => launch('https://discord.gg/qwJpa3r4dQ'), ), ListTile( title: Text('Repository'.i18n), subtitle: Text('Source code, report issues there.'.i18n), leading: Icon(Icons.code, color: Colors.green, size: 36.0), - onTap: () { - launch('https://git.rip/freezer/'); - }, + onTap: () => launch('https://git.rip/freezer/'), ), ListTile( title: Text('Donate'), - subtitle: Text('You should rather support your favorite artists, instead of this app!'), + subtitle: Text( + 'You should rather support your favorite artists, instead of this app!'), leading: Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0), - onTap: () { - launch('https://paypal.me/exttex'); - }, + onTap: () => launch('https://paypal.me/exttex'), ), FreezerDivider(), ListTile( @@ -1646,22 +1587,22 @@ class _CreditsScreenState extends State { subtitle: Text('Android Auto help'), ), FreezerDivider(), - ...List.generate(translators.length, (i) => ListTile( - title: Text(translators[i][0]), - subtitle: Text(translators[i][1]), - )), + ...List.generate( + translators.length, + (i) => ListTile( + title: Text(translators[i][0]), + subtitle: Text(translators[i][1]), + )), Padding( padding: EdgeInsets.fromLTRB(0, 4, 0, 8), child: Text( 'Huge thanks to all the contributors! <3'.i18n, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0 - ), + style: TextStyle(fontSize: 16.0), ), ) ], ), ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 872cd8e..217e1f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,13 +141,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" - clipboard: - dependency: "direct main" - description: - name: clipboard - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2+8" clock: dependency: transitive description: