This commit is contained in:
PetitPrinc3
2024-11-01 10:37:44 -04:00
parent 2ff12ea5de
commit e09ed94789
47 changed files with 956 additions and 387 deletions

1
.gitignore vendored
View File

@@ -69,6 +69,7 @@ app.*.map.json
# Envied
.env
lib/utils/env.g.dart
lib/.env
# Possible translations remnants
translations/refreezer.zip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -13,6 +13,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowBackground">#0F0D13</item>
</style>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_favorites_background">#3DDC84</color>
<color name="ic_favorites_background">#0F0D13</color>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1C1C14</color>
<color name="ic_launcher_background">#0F0D13</color>
</resources>

BIN
assets/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 58 KiB

BIN
assets/fonts/Deezer.ttf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Submodule custom_navigator deleted from bef1badfa6

Submodule external_path deleted from c753b1a154

160
lib/fonts/deezer_icons.dart Normal file
View File

@@ -0,0 +1,160 @@
/// Flutter icons Deezer
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
/// following in your pubspec.yaml
///
/// flutter:
/// fonts:
/// - family: Deezer
/// fonts:
/// - asset: fonts/Deezer.ttf
///
///
///
import 'package:flutter/widgets.dart';
class DeezerIcons {
DeezerIcons._();
static const _kFontFam = 'Deezer';
static const String? _kFontPkg = null;
static const IconData skip_back =
IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData shuffle_small =
IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData share_android_small =
IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData share_android =
IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData shuffle =
IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings_with_badge =
IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings_small =
IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings =
IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData search =
IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData search_small =
IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData search_fill_small =
IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData search_fill =
IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData repeat_small =
IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData repeat_one_small =
IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData repeat_one =
IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData repeat =
IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue_small =
IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue =
IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData play_before =
IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData play_before_small =
IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData pen_small =
IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData pen =
IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData offline_small =
IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData offline =
IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microphone_small =
IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microphone_show_small =
IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microphone_show =
IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microphone_show_fill_small =
IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microphone_show_fill =
IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData microphone =
IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData house =
IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData house_small =
IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData house_fill =
IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData house_fill_small =
IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData heart_small =
IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData heart_fill_small =
IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData heart =
IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData heart_fill =
IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData download_small =
IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData download_fill_small =
IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData download =
IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData download_fill =
IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData cross_small =
IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData compass_small =
IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData cross =
IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData compass =
IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData compass_fill_small =
IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData compass_fill =
IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData angry_face_small =
IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData angry_face =
IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData album =
IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData album_small =
IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData trash =
IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData trash_small =
IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData skip_next_fill =
IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue_1 =
IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue_small_2 =
IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData play =
IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData play_small =
IconData(0xe851, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData play_fill_small =
IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData pause_small =
IconData(0xe857, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData pause =
IconData(0xe858, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData pause_fill_small =
IconData(0xe859, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData album_small_1 =
IconData(0xe877, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData album_1 =
IconData(0xe879, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData queue_small_1 =
IconData(0xe890, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sort_small =
IconData(0xe8bf, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sort =
IconData(0xe8c1, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@@ -2,6 +2,9 @@ import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:custom_navigator/custom_navigator.dart';
import 'package:refreezer/fonts/deezer_icons.dart';
import 'package:refreezer/ui/restartable.dart';
import 'package:refreezer/ui/settings_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
@@ -12,7 +15,6 @@ import 'package:logging/logging.dart';
import 'package:move_to_background/move_to_background.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:refreezer/ui/restartable.dart';
//import 'package:restart_app/restart_app.dart';
import 'api/cache.dart';
@@ -97,7 +99,7 @@ class _ReFreezerAppState extends State<ReFreezerApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ReFreezer',
title: 'Deezer',
shortcuts: <ShortcutActivator, Intent>{
...WidgetsApp.defaultShortcuts,
LogicalKeySet(LogicalKeyboardKey.select):
@@ -202,8 +204,10 @@ class _MainScreenState extends State<MainScreen>
late final AppLifecycleListener _lifeCycleListener;
final List<Widget> _screens = [
const HomeScreen(),
const LibraryScreen(),
const LibraryPlaylists(),
const SearchScreen(),
const LibraryScreen()
const SettingsScreen()
];
Future<void>? _initialization;
int _selected = 0;
@@ -427,15 +431,26 @@ class _MainScreenState extends State<MainScreen>
onKeyEvent: (event) =>
_handleKey(event, navigationBarFocusNode, screenFocusNode),
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: Container(
margin: EdgeInsets.fromLTRB(6, 0, 6, 0),
decoration: BoxDecoration(
color: Colors.transparent,
),
child: const PlayerBar(),
),
bottomNavigationBar: FocusScope(
node: navigationBarFocusNode,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const PlayerBar(),
BottomNavigationBar(
backgroundColor:
Theme.of(context).bottomAppBarTheme.color,
child: Theme(
data: Theme.of(context).copyWith(
canvasColor:
Theme.of(context).scaffoldBackgroundColor),
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
unselectedItemColor:
Theme.of(context).unselectedWidgetColor,
currentIndex: _selected,
onTap: (int index) async {
//Pop all routes until home screen
@@ -448,29 +463,44 @@ class _MainScreenState extends State<MainScreen>
setState(() {
_selected = index;
});
//Fix statusbar
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
));
},
selectedItemColor: Theme.of(context).primaryColor,
selectedItemColor:
settings.primaryColor.withOpacity(0.8),
showUnselectedLabels: true,
selectedLabelStyle:
TextStyle(color: settings.primaryColor),
unselectedLabelStyle:
TextStyle(color: Settings.secondaryText),
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Icons.home),
activeIcon: const Icon(DeezerIcons.house_fill),
icon: const Icon(DeezerIcons.house),
label: 'Home'.i18n),
BottomNavigationBarItem(
icon: const Icon(Icons.search),
activeIcon:
const Icon(DeezerIcons.compass_fill),
icon: const Icon(DeezerIcons.compass),
label: 'Explore'.i18n),
BottomNavigationBarItem(
activeIcon: const Icon(DeezerIcons.heart_fill),
icon: const Icon(DeezerIcons.heart),
label: 'Favorites'.i18n),
BottomNavigationBarItem(
activeIcon: const Icon(DeezerIcons.search_fill),
icon: const Icon(DeezerIcons.search),
label: 'Search'.i18n,
),
BottomNavigationBarItem(
icon: const Icon(Icons.library_music),
label: 'Library'.i18n)
activeIcon: const Icon(DeezerIcons.settings),
icon: const Icon(DeezerIcons.settings),
label: 'Settings'.i18n)
],
)
],
)),
))),
body: CustomNavigator(
navigatorKey: customNavigatorKey,
home: Focus(
@@ -482,9 +512,11 @@ class _MainScreenState extends State<MainScreen>
));
} else {
// While audio_service is initializing
return const Scaffold(
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
child: CircularProgressIndicator(
color: Theme.of(context).primaryColor,
),
),
);
}

View File

@@ -117,10 +117,13 @@ class Settings {
//Colors
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
Color primaryColor = Colors.blue;
Color primaryColor = Color(0xFFA238FF);
static const deezerBg = Color(0xFF0F0D13);
static const deezerBottom = Color(0xFF1B191F);
static const secondaryText = Color(0xFFA9A6AA);
static _colorToJson(Color c) => c.value;
static _colorFromJson(int? v) => v == null ? Colors.blue : Color(v);
static _colorFromJson(int? v) => v == null ? Color(0xFFA238FF) : Color(v);
@JsonKey(defaultValue: false)
bool useArtColor = false;
@@ -255,8 +258,6 @@ class Settings {
return true;
}
static const deezerBg = Color(0xFF1F1A16);
static const deezerBottom = Color(0xFF1b1714);
TextTheme? get textTheme => (font == 'Deezer')
? null
: GoogleFonts.getTextTheme(font,
@@ -394,18 +395,24 @@ class Settings {
}),
),
bottomAppBarTheme:
const BottomAppBarTheme(color: Color(0xff424242))),
const BottomAppBarTheme(color: Color(0xFF0F0D13))),
Themes.Deezer: ThemeData(
useMaterial3: false,
brightness: Brightness.dark,
textTheme: textTheme,
fontFamily: _fontFamily,
primaryColor: primaryColor,
unselectedWidgetColor: secondaryText,
sliderTheme: _sliderTheme,
scaffoldBackgroundColor: deezerBg,
dialogBackgroundColor: deezerBottom,
dialogBackgroundColor: deezerBg,
hintColor: secondaryText,
inputDecorationTheme: const InputDecorationTheme(
hintStyle: TextStyle(color: secondaryText),
labelStyle: TextStyle(color: secondaryText),
),
bottomSheetTheme:
const BottomSheetThemeData(backgroundColor: deezerBottom),
const BottomSheetThemeData(backgroundColor: deezerBg),
cardColor: deezerBg,
outlinedButtonTheme: outlinedButtonTheme,
textButtonTheme: textButtonTheme,
@@ -459,15 +466,15 @@ class Settings {
return null;
}),
),
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom)),
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBg)),
Themes.Black: ThemeData(
useMaterial3: false,
brightness: Brightness.dark,
textTheme: textTheme,
fontFamily: _fontFamily,
primaryColor: primaryColor,
scaffoldBackgroundColor: Colors.black,
dialogBackgroundColor: Colors.black,
scaffoldBackgroundColor: deezerBg,
dialogBackgroundColor: deezerBg,
sliderTheme: _sliderTheme,
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.black,

View File

@@ -13,8 +13,9 @@ class LeadingIcon extends StatelessWidget {
return Container(
width: 42.0,
height: 42.0,
decoration:
BoxDecoration(color: (color ?? Theme.of(context).primaryColor).withOpacity(1.0), shape: BoxShape.circle),
decoration: BoxDecoration(
color: (color ?? Theme.of(context).primaryColor).withOpacity(1.0),
shape: BoxShape.circle),
child: Icon(
icon,
color: Colors.white,
@@ -40,7 +41,8 @@ class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
//Should be specified if bottom is specified
final double height;
const FreezerAppBar(this.title, {super.key, this.actions = const [], this.bottom, this.height = 56.0});
const FreezerAppBar(this.title,
{super.key, this.actions = const [], this.bottom, this.height = 64.0});
@override
Size get preferredSize => Size.fromHeight(height);
@@ -48,18 +50,26 @@ class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(primaryColor: (Theme.of(context).brightness == Brightness.light) ? Colors.white : Colors.black),
data: ThemeData(
primaryColor: (Theme.of(context).brightness == Brightness.light)
? Colors.white
: Colors.black),
child: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Theme.of(context).brightness),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Theme.of(context).brightness),
elevation: 0.0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: (Theme.of(context).brightness == Brightness.light) ? Colors.black : Colors.white,
title: Text(
foregroundColor: (Theme.of(context).brightness == Brightness.light)
? Colors.black
: Colors.white,
title: Container(
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 48,
),
),
)),
actions: actions,
bottom: bottom as PreferredSizeWidget?,
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:refreezer/fonts/deezer_icons.dart';
import '../api/deezer.dart';
import '../api/definitions.dart';
@@ -23,6 +24,8 @@ class HomeScreen extends StatelessWidget {
return Scaffold(
appBar: const HomeAppBar(),
body: SingleChildScrollView(
child: Container(
padding: EdgeInsets.only(bottom: 72.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@@ -32,7 +35,7 @@ class HomeScreen extends StatelessWidget {
)
],
),
));
)));
}
}
@@ -49,7 +52,7 @@ class HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
actions: <Widget>[
IconButton(
icon: Icon(
Icons.file_download,
DeezerIcons.download_fill,
semanticLabel: 'Download'.i18n,
),
onPressed: () {
@@ -59,7 +62,7 @@ class HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
),
IconButton(
icon: Icon(
Icons.settings,
DeezerIcons.settings,
semanticLabel: 'Settings'.i18n,
),
onPressed: () {

View File

@@ -6,6 +6,7 @@ import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:refreezer/fonts/deezer_icons.dart';
import '../api/cache.dart';
import '../api/deezer.dart';
@@ -37,7 +38,7 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
actions: <Widget>[
IconButton(
icon: Icon(
Icons.file_download,
DeezerIcons.download,
semanticLabel: 'Download'.i18n,
),
onPressed: () {
@@ -47,7 +48,7 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
),
IconButton(
icon: Icon(
Icons.settings,
DeezerIcons.settings,
semanticLabel: 'Settings'.i18n,
),
onPressed: () {
@@ -76,7 +77,7 @@ class LibraryScreen extends StatelessWidget {
ListTile(
title: Text('Downloads'.i18n),
leading:
const LeadingIcon(Icons.file_download, color: Colors.grey),
const LeadingIcon(DeezerIcons.download, color: Colors.grey),
subtitle: Text(
'Downloading is currently stopped, click here to resume.'
.i18n),
@@ -88,7 +89,8 @@ class LibraryScreen extends StatelessWidget {
),
ListTile(
title: Text('Shuffle'.i18n),
leading: const LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
leading: const LeadingIcon(DeezerIcons.shuffle,
color: Color(0xffeca704)),
onTap: () async {
List<Track> tracks = await deezerAPI.libraryShuffle();
GetIt.I<AudioPlayerHandler>().playFromTrackList(
@@ -112,7 +114,8 @@ class LibraryScreen extends StatelessWidget {
),
ListTile(
title: Text('Albums'.i18n),
leading: const LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
leading:
const LeadingIcon(DeezerIcons.album, color: Color(0xff4b2e7e)),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const LibraryAlbums()));
@@ -224,7 +227,7 @@ class LibraryScreen extends StatelessWidget {
),
ListTile(
title: Text('Offline albums'.i18n),
leading: const Icon(Icons.album),
leading: const Icon(DeezerIcons.album),
trailing: Text(data[1]),
),
ListTile(
@@ -715,7 +718,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
child: const Icon(Icons.sort, size: 32.0),
child: const Icon(DeezerIcons.sort, size: 32.0),
onSelected: (SortType s) async {
setState(() => _sort.type = s);
//Save to cache
@@ -962,7 +965,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
),
],
child: const Icon(Icons.sort, size: 32.0),
child: const Icon(DeezerIcons.sort, size: 32.0),
),
Container(width: 8.0),
],
@@ -1138,7 +1141,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
],
child: const Icon(Icons.sort, size: 32.0),
child: const Icon(DeezerIcons.sort, size: 32.0),
),
Container(width: 8.0),
],

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart';
import 'package:numberpicker/numberpicker.dart';
import 'package:refreezer/fonts/deezer_icons.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@@ -23,7 +24,8 @@ class MenuSheet {
Function navigateCallback;
// Use no-op callback if not provided
MenuSheet({Function? navigateCallback}) : navigateCallback = navigateCallback ?? (() {});
MenuSheet({Function? navigateCallback})
: navigateCallback = navigateCallback ?? (() {});
//===================
// DEFAULT
@@ -36,7 +38,10 @@ class MenuSheet {
builder: (BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: (MediaQuery.of(context).orientation == Orientation.landscape) ? 220 : 350,
maxHeight:
(MediaQuery.of(context).orientation == Orientation.landscape)
? 220
: 350,
),
child: SingleChildScrollView(
child: Column(children: options),
@@ -83,7 +88,8 @@ class MenuSheet {
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 22.0, fontWeight: FontWeight.bold),
),
Text(
track.artistString ?? '',
@@ -112,7 +118,10 @@ class MenuSheet {
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: (MediaQuery.of(context).orientation == Orientation.landscape) ? 200 : 350,
maxHeight: (MediaQuery.of(context).orientation ==
Orientation.landscape)
? 200
: 350,
),
child: SingleChildScrollView(
child: Column(children: options),
@@ -125,7 +134,9 @@ class MenuSheet {
//Default track options
void defaultTrackMenu(Track track,
{required BuildContext context, List<Widget> options = const [], Function? onRemove}) {
{required BuildContext context,
List<Widget> options = const [],
Function? onRemove}) {
showWithTrack(context, track, [
addToQueueNext(track, context),
addToQueue(track, context),
@@ -138,7 +149,8 @@ class MenuSheet {
shareTile('track', track.id ?? ''),
playMix(track, context),
showAlbum(track.album!, context),
...List.generate(track.artists?.length ?? 0, (i) => showArtist(track.artists![i], context)),
...List.generate(track.artists?.length ?? 0,
(i) => showArtist(track.artists![i], context)),
...options
]);
}
@@ -152,7 +164,8 @@ class MenuSheet {
leading: const Icon(Icons.playlist_play),
onTap: () async {
//-1 = next
await GetIt.I<AudioPlayerHandler>().insertQueueItem(-1, t.toMediaItem());
await GetIt.I<AudioPlayerHandler>()
.insertQueueItem(-1, t.toMediaItem());
if (context.mounted) _close(context);
});
@@ -166,7 +179,7 @@ class MenuSheet {
Widget addTrackFavorite(Track t, BuildContext context) => ListTile(
title: Text('Add track to favorites'.i18n),
leading: const Icon(Icons.favorite),
leading: const Icon(DeezerIcons.heart_fill),
onTap: () async {
await deezerAPI.addFavoriteTrack(t.id!);
//Make track offline, if favorites are offline
@@ -175,7 +188,9 @@ class MenuSheet {
downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: 'Added to library'.i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
msg: 'Added to library'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
//Add to cache
cache.libraryTracks ??= [];
cache.libraryTracks?.add(t.id!);
@@ -185,9 +200,11 @@ class MenuSheet {
Widget downloadTrack(Track t, BuildContext context) => ListTile(
title: Text('Download'.i18n),
leading: const Icon(Icons.file_download),
leading: const Icon(DeezerIcons.download),
onTap: () async {
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
if (await downloadManager.addOfflineTrack(t,
private: false, isSingleton: true) !=
false) {
showDownloadStartedToast();
}
if (context.mounted) _close(context);
@@ -221,9 +238,10 @@ class MenuSheet {
},
);
Widget removeFromPlaylist(Track t, Playlist p, BuildContext context) => ListTile(
Widget removeFromPlaylist(Track t, Playlist p, BuildContext context) =>
ListTile(
title: Text('Remove from playlist'.i18n),
leading: const Icon(Icons.delete),
leading: const Icon(DeezerIcons.trash),
onTap: () async {
await deezerAPI.removeFromPlaylist(t.id!, p.id!);
Fluttertoast.showToast(
@@ -235,9 +253,10 @@ class MenuSheet {
},
);
Widget removeFavoriteTrack(Track t, BuildContext context, {onUpdate}) => ListTile(
Widget removeFavoriteTrack(Track t, BuildContext context, {onUpdate}) =>
ListTile(
title: Text('Remove favorite'.i18n),
leading: const Icon(Icons.delete),
leading: const Icon(DeezerIcons.trash),
onTap: () async {
await deezerAPI.removeFavorite(t.id!);
//Check if favorites playlist is offline, update it
@@ -248,7 +267,9 @@ class MenuSheet {
//Remove from cache
cache.libraryTracks?.removeWhere((i) => i == t.id);
Fluttertoast.showToast(
msg: 'Track removed from library'.i18n, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM);
msg: 'Track removed from library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM);
if (onUpdate != null) onUpdate();
if (context.mounted) _close(context);
},
@@ -264,7 +285,8 @@ class MenuSheet {
leading: const Icon(Icons.recent_actors),
onTap: () {
if (context.mounted) _close(context);
customNavigatorKey.currentState?.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
customNavigatorKey.currentState
?.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
navigateCallback();
},
@@ -279,7 +301,8 @@ class MenuSheet {
leading: const Icon(Icons.album),
onTap: () {
if (context.mounted) _close(context);
customNavigatorKey.currentState?.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
customNavigatorKey.currentState
?.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
navigateCallback();
},
@@ -323,7 +346,9 @@ class MenuSheet {
//Default album options
void defaultAlbumMenu(Album album,
{required BuildContext context, List<Widget> options = const [], Function? onRemove}) {
{required BuildContext context,
List<Widget> options = const [],
Function? onRemove}) {
show(context, [
(album.library != null && onRemove != null)
? removeAlbum(album, context, onRemove: onRemove)
@@ -341,7 +366,7 @@ class MenuSheet {
Widget downloadAlbum(Album a, BuildContext context) => ListTile(
title: Text('Download'.i18n),
leading: const Icon(Icons.file_download),
leading: const Icon(DeezerIcons.download),
onTap: () async {
if (context.mounted) _close(context);
if (await downloadManager.addOfflineAlbum(a, private: false) != false) {
@@ -365,15 +390,18 @@ class MenuSheet {
leading: const Icon(Icons.library_music),
onTap: () async {
await deezerAPI.addFavoriteAlbum(a.id!);
Fluttertoast.showToast(msg: 'Added to library'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Added to library'.i18n, gravity: ToastGravity.BOTTOM);
if (context.mounted) _close(context);
},
);
//Remove album from favorites
Widget removeAlbum(Album a, BuildContext context, {required Function onRemove}) => ListTile(
Widget removeAlbum(Album a, BuildContext context,
{required Function onRemove}) =>
ListTile(
title: Text('Remove album'.i18n),
leading: const Icon(Icons.delete),
leading: const Icon(DeezerIcons.trash),
onTap: () async {
await deezerAPI.removeAlbum(a.id!);
await downloadManager.removeOfflineAlbum(a.id!);
@@ -392,9 +420,13 @@ class MenuSheet {
//===================
void defaultArtistMenu(Artist artist,
{required BuildContext context, List<Widget> options = const [], Function? onRemove}) {
{required BuildContext context,
List<Widget> options = const [],
Function? onRemove}) {
show(context, [
(artist.library != null) ? removeArtist(artist, context, onRemove: onRemove) : favoriteArtist(artist, context),
(artist.library != null)
? removeArtist(artist, context, onRemove: onRemove)
: favoriteArtist(artist, context),
shareTile('artist', artist.id!),
...options
]);
@@ -404,13 +436,16 @@ class MenuSheet {
// ARTIST OPTIONS
//===================
Widget removeArtist(Artist a, BuildContext context, {Function? onRemove}) => ListTile(
Widget removeArtist(Artist a, BuildContext context, {Function? onRemove}) =>
ListTile(
title: Text('Remove from favorites'.i18n),
leading: const Icon(Icons.delete),
leading: const Icon(DeezerIcons.trash),
onTap: () async {
await deezerAPI.removeArtist(a.id!);
Fluttertoast.showToast(
msg: 'Artist removed from library'.i18n, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM);
msg: 'Artist removed from library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM);
if (onRemove != null) onRemove();
if (context.mounted) _close(context);
},
@@ -418,11 +453,13 @@ class MenuSheet {
Widget favoriteArtist(Artist a, BuildContext context) => ListTile(
title: Text('Add to favorites'.i18n),
leading: const Icon(Icons.favorite),
leading: const Icon(DeezerIcons.heart_fill),
onTap: () async {
await deezerAPI.addFavoriteArtist(a.id!);
Fluttertoast.showToast(
msg: 'Added to library'.i18n, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM);
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM);
if (context.mounted) _close(context);
},
);
@@ -432,7 +469,10 @@ class MenuSheet {
//===================
void defaultPlaylistMenu(Playlist playlist,
{required BuildContext context, List<Widget> options = const [], Function? onRemove, Function? onUpdate}) {
{required BuildContext context,
List<Widget> options = const [],
Function? onRemove,
Function? onUpdate}) {
show(context, [
(playlist.library != null)
? removePlaylistLibrary(playlist, context, onRemove: onRemove)
@@ -440,7 +480,8 @@ class MenuSheet {
addPlaylistOffline(playlist, context),
downloadPlaylist(playlist, context),
shareTile('playlist', playlist.id!),
if (playlist.user?.id == deezerAPI.userId) editPlaylist(playlist, context: context, onUpdate: onUpdate),
if (playlist.user?.id == deezerAPI.userId)
editPlaylist(playlist, context: context, onUpdate: onUpdate),
...options
]);
}
@@ -449,9 +490,11 @@ class MenuSheet {
// PLAYLIST OPTIONS
//===================
Widget removePlaylistLibrary(Playlist p, BuildContext context, {Function? onRemove}) => ListTile(
Widget removePlaylistLibrary(Playlist p, BuildContext context,
{Function? onRemove}) =>
ListTile(
title: Text('Remove from library'.i18n),
leading: const Icon(Icons.delete),
leading: const Icon(DeezerIcons.trash),
onTap: () async {
if (p.user?.id?.trim() == deezerAPI.userId) {
//Delete playlist if own
@@ -468,10 +511,12 @@ class MenuSheet {
Widget addPlaylistLibrary(Playlist p, BuildContext context) => ListTile(
title: Text('Add playlist to library'.i18n),
leading: const Icon(Icons.favorite),
leading: const Icon(DeezerIcons.heart_fill),
onTap: () async {
await deezerAPI.addPlaylist(p.id!);
Fluttertoast.showToast(msg: 'Added playlist to library'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Added playlist to library'.i18n,
gravity: ToastGravity.BOTTOM);
if (context.mounted) _close(context);
},
);
@@ -490,20 +535,25 @@ class MenuSheet {
Widget downloadPlaylist(Playlist p, BuildContext context) => ListTile(
title: Text('Download playlist'.i18n),
leading: const Icon(Icons.file_download),
leading: const Icon(DeezerIcons.download),
onTap: () async {
if (context.mounted) _close(context);
if (await downloadManager.addOfflinePlaylist(p, private: false) != false) {
if (await downloadManager.addOfflinePlaylist(p, private: false) !=
false) {
showDownloadStartedToast();
}
},
);
Widget editPlaylist(Playlist p, {required BuildContext context, Function? onUpdate}) => ListTile(
Widget editPlaylist(Playlist p,
{required BuildContext context, Function? onUpdate}) =>
ListTile(
title: Text('Edit playlist'.i18n),
leading: const Icon(Icons.edit),
leading: const Icon(DeezerIcons.pen),
onTap: () async {
await showDialog(context: context, builder: (context) => CreatePlaylistDialog(playlist: p));
await showDialog(
context: context,
builder: (context) => CreatePlaylistDialog(playlist: p));
if (context.mounted) _close(context);
if (onUpdate != null) onUpdate();
},
@@ -513,13 +563,19 @@ class MenuSheet {
// SHOW/EPISODE
//===================
defaultShowEpisodeMenu(Show s, ShowEpisode e, {required BuildContext context, List<Widget> options = const []}) {
show(context, [shareTile('episode', e.id!), shareShow(s.id!), downloadExternalEpisode(e), ...options]);
defaultShowEpisodeMenu(Show s, ShowEpisode e,
{required BuildContext context, List<Widget> options = const []}) {
show(context, [
shareTile('episode', e.id!),
shareShow(s.id!),
downloadExternalEpisode(e),
...options
]);
}
Widget shareShow(String id) => ListTile(
title: Text('Share show'.i18n),
leading: const Icon(Icons.share),
leading: const Icon(DeezerIcons.share_android),
onTap: () async {
Share.share('https://deezer.com/show/$id');
},
@@ -528,7 +584,7 @@ class MenuSheet {
//Open direct download link in browser
Widget downloadExternalEpisode(ShowEpisode e) => ListTile(
title: Text('Download externally'.i18n),
leading: const Icon(Icons.file_download),
leading: const Icon(DeezerIcons.download),
onTap: () async {
if (e.url != null) await launchUrlString(e.url!);
},
@@ -539,7 +595,10 @@ class MenuSheet {
//===================
showDownloadStartedToast() {
Fluttertoast.showToast(msg: 'Downloads added!'.i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
Fluttertoast.showToast(
msg: 'Downloads added!'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
}
//Create playlist
@@ -572,20 +631,24 @@ class MenuSheet {
);
Widget wakelock(BuildContext context) => ListTile(
title: Text(cache.wakelock ? 'Allow screen to turn off'.i18n : 'Keep the screen on'.i18n),
title: Text(cache.wakelock
? 'Allow screen to turn off'.i18n
: 'Keep the screen on'.i18n),
leading: const Icon(Icons.screen_lock_portrait),
onTap: () async {
_close(context);
//Enable
if (!cache.wakelock) {
WakelockPlus.enable();
Fluttertoast.showToast(msg: 'Wakelock enabled!'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Wakelock enabled!'.i18n, gravity: ToastGravity.BOTTOM);
cache.wakelock = true;
return;
}
//Disable
WakelockPlus.disable();
Fluttertoast.showToast(msg: 'Wakelock disabled!'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Wakelock disabled!'.i18n, gravity: ToastGravity.BOTTOM);
cache.wakelock = false;
},
);
@@ -622,7 +685,11 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
mainAxisSize: MainAxisSize.min,
children: [
Text('Hours:'.i18n),
NumberPicker(value: hours, minValue: 0, maxValue: 69, onChanged: (v) => setState(() => hours = v)),
NumberPicker(
value: hours,
minValue: 0,
maxValue: 69,
onChanged: (v) => setState(() => hours = v)),
],
),
Column(
@@ -630,7 +697,10 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
children: [
Text('Minutes:'.i18n),
NumberPicker(
value: minutes, minValue: 0, maxValue: 60, onChanged: (v) => setState(() => minutes = v)),
value: minutes,
minValue: 0,
maxValue: 60,
onChanged: (v) => setState(() => minutes = v)),
],
),
],
@@ -666,7 +736,8 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
Duration duration = Duration(hours: hours, minutes: minutes);
cache.sleepTimer?.cancel();
//Create timer
cache.sleepTimer = Stream.fromFuture(Future.delayed(duration)).listen((_) {
cache.sleepTimer =
Stream.fromFuture(Future.delayed(duration)).listen((_) {
GetIt.I<AudioPlayerHandler>().pause();
cache.sleepTimer?.cancel();
cache.sleepTimerTime = null;
@@ -837,16 +908,20 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
onPressed: () async {
if (edit) {
//Update
await deezerAPI.updatePlaylist(
widget.playlist!.id!, _titleController!.value.text, _descController!.value.text,
await deezerAPI.updatePlaylist(widget.playlist!.id!,
_titleController!.value.text, _descController!.value.text,
status: _playlistType);
Fluttertoast.showToast(msg: 'Playlist updated!'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Playlist updated!'.i18n, gravity: ToastGravity.BOTTOM);
} else {
List<String> tracks = [];
tracks = widget.tracks?.map<String>((t) => t.id!).toList() ?? [];
await deezerAPI.createPlaylist(_title,
status: _playlistType, description: _description, trackIds: tracks);
Fluttertoast.showToast(msg: 'Playlist created!'.i18n, gravity: ToastGravity.BOTTOM);
status: _playlistType,
description: _description,
trackIds: tracks);
Fluttertoast.showToast(
msg: 'Playlist created!'.i18n, gravity: ToastGravity.BOTTOM);
}
if (context.mounted) Navigator.of(context).pop();
},

View File

@@ -1,7 +1,12 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:refreezer/fonts/deezer_icons.dart';
import '../service/audio_service.dart';
import '../settings.dart';
@@ -10,6 +15,8 @@ import '../ui/router.dart';
import 'cached_image.dart';
import 'player_screen.dart';
late Function updateColor;
class PlayerBar extends StatefulWidget {
const PlayerBar({super.key});
@@ -18,110 +25,184 @@ class PlayerBar extends StatefulWidget {
}
class _PlayerBarState extends State<PlayerBar> {
final double iconSize = 28;
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
Color? _bgColor;
StreamSubscription? _mediaItemSub;
final double iconSize = 20;
//bool _gestureRegistered = false;
//Recover dominant color
Future _updateColor() async {
if (audioHandler.mediaItem.value == null) return;
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider(
audioHandler.mediaItem.value?.extras?['thumb'] ??
audioHandler.mediaItem.value?.artUri));
setState(() => _bgColor = palette.dominantColor!.color.withOpacity(1));
}
@override
void initState() {
_updateColor;
_mediaItemSub = audioHandler.mediaItem.listen((event) {
_updateColor();
});
updateColor = _updateColor;
super.initState();
}
@override
void dispose() {
_mediaItemSub?.cancel();
super.dispose();
}
double get _progress {
if (GetIt.I<AudioPlayerHandler>().playbackState.value.processingState == AudioProcessingState.idle) return 0.0;
if (GetIt.I<AudioPlayerHandler>().playbackState.value.processingState ==
AudioProcessingState.idle) return 0.0;
if (GetIt.I<AudioPlayerHandler>().mediaItem.value == null) return 0.0;
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.duration!.inSeconds == 0) return 0.0; //Division by 0
return GetIt.I<AudioPlayerHandler>().playbackState.value.position.inSeconds /
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.duration!.inSeconds == 0)
return 0.0; //Division by 0
return GetIt.I<AudioPlayerHandler>()
.playbackState
.value
.position
.inSeconds /
GetIt.I<AudioPlayerHandler>().mediaItem.value!.duration!.inSeconds;
}
@override
Widget build(BuildContext context) {
scaffoldBackgroundColor = Theme.of(context).scaffoldBackgroundColor;
var focusNode = FocusNode();
return GestureDetector(
key: UniqueKey(),
onHorizontalDragEnd: (DragEndDetails details) async {
if ((details.primaryVelocity ?? 0) < -100) {
// Swiped left
await GetIt.I<AudioPlayerHandler>().skipToPrevious();
} else if ((details.primaryVelocity ?? 0) > 100) {
// Swiped right
await GetIt.I<AudioPlayerHandler>().skipToNext();
}
},
onVerticalDragEnd: (DragEndDetails details) async {
if ((details.primaryVelocity ?? 0) < -100) {
// Swiped up
Navigator.of(context).push(SlideBottomRoute(widget: const PlayerScreen()));
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.scaffoldBackgroundColor,
));
} /*else if ((details.primaryVelocity ?? 0) > 100) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: scaffoldBackgroundColor,
border: Border.all(
color: _bgColor != null
? _bgColor!.withOpacity(0.7)
: Theme.of(context).scaffoldBackgroundColor),
borderRadius: BorderRadius.circular(15)),
child: GestureDetector(
key: UniqueKey(),
onHorizontalDragEnd: (DragEndDetails details) async {
if ((details.primaryVelocity ?? 0) < -100) {
// Swiped left
await GetIt.I<AudioPlayerHandler>().skipToNext();
updateColor();
} else if ((details.primaryVelocity ?? 0) > 100) {
// Swiped right
await GetIt.I<AudioPlayerHandler>().skipToPrevious();
updateColor();
}
},
onVerticalDragEnd: (DragEndDetails details) async {
if ((details.primaryVelocity ?? 0) < -100) {
// Swiped up
Navigator.of(context)
.push(SlideBottomRoute(widget: const PlayerScreen()));
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: _bgColor,
));
} /*else if ((details.primaryVelocity ?? 0) > 100) {
// Swiped down => no action
}*/
},
child: StreamBuilder(
stream: Stream.periodic(const Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (GetIt.I<AudioPlayerHandler>().mediaItem.value == null) {
return const SizedBox(
width: 0,
height: 0,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
// For Android TV: indicate focus by grey
color: focusNode.hasFocus ? Colors.black26 : Theme.of(context).bottomAppBarTheme.color,
child: ListTile(
dense: true,
focusNode: focusNode,
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
onTap: () {
Navigator.of(context).push(SlideBottomRoute(widget: const PlayerScreen()));
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.scaffoldBackgroundColor,
));
},
leading: CachedImage(
width: 50,
height: 50,
url: GetIt.I<AudioPlayerHandler>().mediaItem.value?.extras?['thumb'] ??
GetIt.I<AudioPlayerHandler>().mediaItem.value?.artUri,
),
title: Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
overflow: TextOverflow.clip,
maxLines: 1,
),
subtitle: Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ?? '',
overflow: TextOverflow.clip,
maxLines: 1,
),
trailing: IconTheme(
data: IconThemeData(color: settings.isDark ? Colors.white : Colors.grey[600]),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PrevNextButton(
iconSize,
prev: true,
hidePrev: true,
),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
),
)),
),
SizedBox(
height: 3.0,
child: LinearProgressIndicator(
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
color: Theme.of(context).primaryColor,
value: _progress,
updateColor();
},
child: StreamBuilder(
stream: Stream.periodic(const Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (GetIt.I<AudioPlayerHandler>().mediaItem.value == null) {
return const SizedBox(
width: 0,
height: 0,
);
}
return Container(
decoration: BoxDecoration(
color: _bgColor?.withOpacity(0.7),
),
)
],
);
}),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
dense: true,
focusNode: focusNode,
contentPadding:
const EdgeInsets.symmetric(horizontal: 8.0),
onTap: () {
Navigator.of(context).push(
SlideBottomRoute(widget: const PlayerScreen()));
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor: Settings.deezerBottom,
));
},
leading: CachedImage(
width: 50,
height: 50,
url: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.extras?['thumb'] ??
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.artUri,
),
title: Text(
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'',
overflow: TextOverflow.clip,
maxLines: 1,
),
subtitle: Text(
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displaySubtitle ??
'',
overflow: TextOverflow.clip,
maxLines: 1,
),
trailing: IconTheme(
data: IconThemeData(
color: settings.isDark
? Colors.white
: Colors.grey[600]),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PrevNextButton(
iconSize,
prev: true,
hidePrev: true,
),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
),
)),
SizedBox(
height: 3.0,
child: LinearProgressIndicator(
backgroundColor: _bgColor!.withOpacity(0.1),
color: _bgColor,
value: _progress,
),
)
],
));
}),
),
);
}
}
@@ -131,7 +212,8 @@ class PrevNextButton extends StatelessWidget {
final bool prev;
final bool hidePrev;
const PrevNextButton(this.size, {super.key, this.prev = false, this.hidePrev = false});
const PrevNextButton(this.size,
{super.key, this.prev = false, this.hidePrev = false});
@override
Widget build(BuildContext context) {
@@ -143,7 +225,7 @@ class PrevNextButton extends StatelessWidget {
if (!(queueState?.hasNext ?? false)) {
return IconButton(
icon: Icon(
Icons.skip_next,
DeezerIcons.skip_next_fill,
semanticLabel: 'Play next'.i18n,
),
iconSize: size,
@@ -152,7 +234,7 @@ class PrevNextButton extends StatelessWidget {
}
return IconButton(
icon: Icon(
Icons.skip_next,
DeezerIcons.skip_next_fill,
semanticLabel: 'Play next'.i18n,
),
iconSize: size,
@@ -169,7 +251,7 @@ class PrevNextButton extends StatelessWidget {
}
return IconButton(
icon: Icon(
Icons.skip_previous,
DeezerIcons.skip_back,
semanticLabel: 'Play previous'.i18n,
),
iconSize: size,
@@ -178,7 +260,7 @@ class PrevNextButton extends StatelessWidget {
}
return IconButton(
icon: Icon(
Icons.skip_previous,
DeezerIcons.skip_back,
semanticLabel: 'Play previous'.i18n,
),
iconSize: size,
@@ -199,14 +281,17 @@ class PlayPauseButton extends StatefulWidget {
_PlayPauseButtonState createState() => _PlayPauseButtonState();
}
class _PlayPauseButtonState extends State<PlayPauseButton> with SingleTickerProviderStateMixin {
class _PlayPauseButtonState extends State<PlayPauseButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
_animation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
_animation = Tween<double>(begin: 0, end: 1)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
super.initState();
}
@@ -227,7 +312,9 @@ class _PlayPauseButtonState extends State<PlayPauseButton> with SingleTickerProv
// Animated icon by pato05
// Morph from pause to play or from play to pause
if (playing || processingState == AudioProcessingState.ready || processingState == AudioProcessingState.idle) {
if (playing ||
processingState == AudioProcessingState.ready ||
processingState == AudioProcessingState.idle) {
if (playing) {
_controller.forward();
} else {
@@ -236,14 +323,25 @@ class _PlayPauseButtonState extends State<PlayPauseButton> with SingleTickerProv
return IconButton(
splashRadius: widget.size,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animation,
semanticLabel: playing ? 'Pause'.i18n : 'Play'.i18n,
),
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, anim) => RotationTransition(
turns: child.key == ValueKey('icon1')
? Tween<double>(begin: 1, end: 0.75).animate(anim)
: Tween<double>(begin: 0.75, end: 1).animate(anim),
child: FadeTransition(opacity: anim, child: child),
),
child: !playing
? Icon(DeezerIcons.play_fill_small,
key: const ValueKey('Play'))
: Icon(
DeezerIcons.pause_fill_small,
key: const ValueKey('Pause'),
)),
iconSize: widget.size,
onPressed:
playing ? () => GetIt.I<AudioPlayerHandler>().pause() : () => GetIt.I<AudioPlayerHandler>().play());
onPressed: playing
? () => GetIt.I<AudioPlayerHandler>().pause()
: () => GetIt.I<AudioPlayerHandler>().play());
}
switch (processingState) {
@@ -256,7 +354,8 @@ class _PlayPauseButtonState extends State<PlayPauseButton> with SingleTickerProv
child: Center(
child: Transform.scale(
scale: 0.85, // Adjust the scale to 75% of the original size
child: const CircularProgressIndicator(),
child: CircularProgressIndicator(
color: Theme.of(context).primaryColor),
),
),
);

View File

@@ -5,6 +5,8 @@ import 'dart:ui';
import 'package:async/async.dart';
import 'package:audio_service/audio_service.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:refreezer/fonts/deezer_icons.dart';
import 'package:refreezer/utils/navigator_keys.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -13,7 +15,6 @@ import 'package:get_it/get_it.dart';
import 'package:just_audio/just_audio.dart';
import 'package:marquee/marquee.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:refreezer/utils/navigator_keys.dart';
import 'package:rxdart/rxdart.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -21,7 +22,6 @@ import '../api/cache.dart';
import '../api/deezer.dart';
import '../api/definitions.dart';
import '../api/download.dart';
import '../fonts/refreezer_icons.dart';
import '../service/audio_service.dart';
import '../settings.dart';
import '../translations.i18n.dart';
@@ -62,21 +62,25 @@ class _PlayerScreenState extends State<PlayerScreen> {
//BG Image
if (settings.blurPlayerBackground) {
setState(() {
_blurImage =
NetworkImage(audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri);
_blurImage = NetworkImage(
audioHandler.mediaItem.value?.extras?['thumb'] ??
audioHandler.mediaItem.value?.artUri);
});
}
//Run in isolate
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(
audioHandler.mediaItem.value?.extras?['thumb'] ?? audioHandler.mediaItem.value?.artUri));
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider(
audioHandler.mediaItem.value?.extras?['thumb'] ??
audioHandler.mediaItem.value?.artUri));
//Update notification
if (settings.blurPlayerBackground) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: palette.dominantColor!.color.withOpacity(0.25),
systemNavigationBarColor:
Color.alphaBlend(palette.dominantColor!.color.withOpacity(0.25), scaffoldBackgroundColor)));
systemNavigationBarColor: Color.alphaBlend(
palette.dominantColor!.color.withOpacity(0.25),
scaffoldBackgroundColor)));
}
//Color gradient
@@ -85,10 +89,16 @@ class _PlayerScreenState extends State<PlayerScreen> {
statusBarColor: palette.dominantColor!.color.withOpacity(0.7),
));
setState(() => _bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [palette.dominantColor!.color.withOpacity(0.7), const Color.fromARGB(0, 0, 0, 0)],
stops: const [0.0, 0.6]));
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
palette.dominantColor!.color.withOpacity(0.7),
Settings.deezerBg
],
stops: const [
0.0,
0.6
]));
}
}
@@ -108,7 +118,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
_mediaItemSub?.cancel();
//Fix bottom buttons
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color, statusBarColor: Colors.transparent));
systemNavigationBarColor: settings.themeData.bottomAppBarTheme.color,
statusBarColor: Colors.transparent));
super.dispose();
}
@@ -120,7 +131,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
return Scaffold(
body: SafeArea(
child: Container(
decoration: BoxDecoration(gradient: settings.blurPlayerBackground ? null : _bgGradient),
decoration: BoxDecoration(
gradient:
settings.blurPlayerBackground ? null : _bgGradient),
child: Stack(
children: [
if (settings.blurPlayerBackground)
@@ -130,7 +143,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
image: DecorationImage(
image: _blurImage ?? const NetworkImage(''),
fit: BoxFit.fill,
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.25), BlendMode.dstATop))),
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(0.25),
BlendMode.dstATop))),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(color: Colors.transparent),
@@ -138,7 +153,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
),
),
StreamBuilder(
stream: StreamZip([audioHandler.playbackState, audioHandler.mediaItem]),
stream: StreamZip(
[audioHandler.playbackState, audioHandler.mediaItem]),
builder: (BuildContext context, AsyncSnapshot snapshot) {
//When disconnected
if (audioHandler.mediaItem.value == null) {
@@ -214,26 +230,45 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
children: <Widget>[
SizedBox(
height: ScreenUtil().setSp(50),
child: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!.length >= 22
child: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displayTitle!
.length >=
22
? Marquee(
text: GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
text: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displayTitle!,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: const Duration(seconds: 1),
pauseAfterRound: const Duration(seconds: 2),
)
: Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displayTitle!,
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displayTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold),
)),
Container(
height: 4,
),
Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value!.displaySubtitle ?? '',
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.displaySubtitle ??
'',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
@@ -261,13 +296,16 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
LyricsIconButton(12, afterOnPressed: updateColor),
IconButton(
icon: Icon(
Icons.file_download,
DeezerIcons.download,
size: ScreenUtil().setWidth(12),
semanticLabel: 'Download'.i18n,
),
onPressed: () async {
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
Track t = Track.fromMediaItem(
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t,
private: false, isSingleton: true) !=
false) {
Fluttertoast.showToast(
msg: 'Downloads added!'.i18n,
gravity: ToastGravity.BOTTOM,
@@ -328,26 +366,45 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
children: <Widget>[
SizedBox(
height: ScreenUtil().setSp(26),
child: (GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '').length >= 26
child: (GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'')
.length >=
26
? Marquee(
text: GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
text: GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'',
style: TextStyle(
fontSize: ScreenUtil().setSp(22),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: const Duration(seconds: 1),
pauseAfterRound: const Duration(seconds: 2),
)
: Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
GetIt.I<AudioPlayerHandler>()
.mediaItem
.value
?.displayTitle ??
'',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: ScreenUtil().setSp(22),
fontWeight: FontWeight.bold),
)),
Container(
height: 4,
),
Text(
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ?? '',
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ??
'',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
@@ -369,15 +426,20 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
LyricsIconButton(20, afterOnPressed: updateColor),
IconButton(
icon: Icon(
Icons.file_download,
DeezerIcons.download,
size: ScreenUtil().setWidth(20),
semanticLabel: 'Download'.i18n,
),
onPressed: () async {
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) {
Track t = Track.fromMediaItem(
GetIt.I<AudioPlayerHandler>().mediaItem.value!);
if (await downloadManager.addOfflineTrack(t,
private: false, isSingleton: true) !=
false) {
Fluttertoast.showToast(
msg: 'Downloads added!'.i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
msg: 'Downloads added!'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
}
},
),
@@ -407,7 +469,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
//Load data from native
void _load() async {
if (audioHandler.mediaItem.value == null) return;
Map? data = await DownloadManager.platform.invokeMethod('getStreamInfo', {'id': audioHandler.mediaItem.value!.id});
Map? data = await DownloadManager.platform.invokeMethod(
'getStreamInfo', {'id': audioHandler.mediaItem.value!.id});
//N/A
if (data == null) {
if (mounted) setState(() => value = '');
@@ -449,7 +512,8 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
return TextButton(
child: Text(value),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QualitySettings()));
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const QualitySettings()));
},
);
}
@@ -475,26 +539,30 @@ class LyricsIconButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
Track track = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
Track track =
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
bool isEnabled = (track.lyrics?.id ?? '0') != '0';
return Opacity(
opacity: isEnabled ? 1.0 : 0.7, // Full opacity for enabled, reduced for disabled
opacity: isEnabled
? 1.0
: 0.7, // Full opacity for enabled, reduced for disabled
child: IconButton(
icon: Icon(
//Icons.lyrics,
ReFreezerIcons.lyrics_mic,
DeezerIcons.microphone,
size: ScreenUtil().setWidth(width),
semanticLabel: 'Lyrics'.i18n,
),
onPressed: isEnabled
? () async {
//Fix bottom buttons
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent));
await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => LyricsScreen(trackId: track.id!)));
await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => LyricsScreen(trackId: track.id!)));
if (afterOnPressed != null) {
afterOnPressed!();
@@ -519,16 +587,24 @@ class PlayerMenuButton extends StatelessWidget {
semanticLabel: 'Options'.i18n,
),
onPressed: () {
Track t = Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
Track t =
Track.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!);
MenuSheet m = MenuSheet(navigateCallback: () {
Navigator.of(context).pop();
});
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] == null) {
m.defaultTrackMenu(t, context: context, options: [m.sleepTimer(context), m.wakelock(context)]);
if (GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'] ==
null) {
m.defaultTrackMenu(t,
context: context,
options: [m.sleepTimer(context), m.wakelock(context)]);
} else {
m.defaultShowEpisodeMenu(
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>().mediaItem.value!.extras?['show'])),
ShowEpisode.fromMediaItem(GetIt.I<AudioPlayerHandler>().mediaItem.value!),
Show.fromJson(jsonDecode(GetIt.I<AudioPlayerHandler>()
.mediaItem
.value!
.extras?['show'])),
ShowEpisode.fromMediaItem(
GetIt.I<AudioPlayerHandler>().mediaItem.value!),
context: context,
options: [m.sleepTimer(context), m.wakelock(context)]);
}
@@ -550,20 +626,20 @@ class _RepeatButtonState extends State<RepeatButton> {
switch (GetIt.I<AudioPlayerHandler>().getLoopMode()) {
case LoopMode.off:
return Icon(
Icons.repeat,
DeezerIcons.repeat,
size: widget.iconSize,
semanticLabel: 'Repeat off'.i18n,
);
case LoopMode.all:
return Icon(
Icons.repeat,
DeezerIcons.repeat,
color: Theme.of(context).primaryColor,
size: widget.iconSize,
semanticLabel: 'Repeat'.i18n,
);
case LoopMode.one:
return Icon(
Icons.repeat_one,
DeezerIcons.repeat_one,
color: Theme.of(context).primaryColor,
size: widget.iconSize,
semanticLabel: 'Repeat one'.i18n,
@@ -594,15 +670,17 @@ class PlaybackControls extends StatefulWidget {
class _PlaybackControlsState extends State<PlaybackControls> {
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
Icon get libraryIcon {
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
return Icon(
Icons.favorite,
DeezerIcons.heart_fill,
color: settings.primaryColor,
size: widget.iconSize * 0.44,
semanticLabel: 'Unlove'.i18n,
);
}
return Icon(
Icons.favorite_border,
DeezerIcons.heart,
size: widget.iconSize * 0.44,
semanticLabel: 'Love'.i18n,
);
@@ -618,7 +696,7 @@ class _PlaybackControlsState extends State<PlaybackControls> {
children: [
IconButton(
icon: Icon(
Icons.sentiment_very_dissatisfied,
DeezerIcons.angry_face,
size: widget.iconSize * 0.44,
semanticLabel: 'Dislike'.i18n,
),
@@ -629,22 +707,27 @@ class _PlaybackControlsState extends State<PlaybackControls> {
}
}),
PrevNextButton(widget.iconSize, prev: true),
PlayPauseButton(widget.iconSize * 1.25),
PlayPauseButton(widget.iconSize),
PrevNextButton(widget.iconSize),
IconButton(
icon: libraryIcon,
onPressed: () async {
cache.libraryTracks ??= [];
if (cache.checkTrackFavorite(Track.fromMediaItem(audioHandler.mediaItem.value!))) {
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
//Remove from library
setState(() => cache.libraryTracks?.remove(audioHandler.mediaItem.value!.id));
await deezerAPI.removeFavorite(audioHandler.mediaItem.value!.id);
setState(() => cache.libraryTracks
?.remove(audioHandler.mediaItem.value!.id));
await deezerAPI
.removeFavorite(audioHandler.mediaItem.value!.id);
await cache.save();
} else {
//Add
setState(() => cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
setState(() =>
cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
await deezerAPI
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
await cache.save();
}
},
@@ -679,7 +762,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
_imageList = _getImageList(audioHandler.queue.value);
_currentItemAndQueueSub = Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
_currentItemAndQueueSub =
Rx.combineLatest2<MediaItem?, List<MediaItem>, void>(
audioHandler.mediaItem,
audioHandler.queue,
(mediaItem, queue) {
@@ -698,7 +782,9 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
}
List<ZoomableImage> _getImageList(List<MediaItem> queue) {
return queue.map((item) => ZoomableImage(url: item.artUri?.toString() ?? '')).toList();
return queue
.map((item) => ZoomableImage(url: item.artUri?.toString() ?? ''))
.toList();
}
bool _didQueueChange(List<MediaItem> newQueue) {
@@ -718,7 +804,8 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
void _handleMediaItemChange(MediaItem? item) async {
final targetItemId = item?.id ?? '';
final targetPage = audioHandler.queue.value.indexWhere((item) => item.id == targetItemId);
final targetPage =
audioHandler.queue.value.indexWhere((item) => item.id == targetItemId);
if (targetPage == -1) return;
// No need to animating to the same page
@@ -799,7 +886,8 @@ class PlayerScreenTopRow extends StatelessWidget {
final double? textWidth;
final bool? short;
final GlobalKey iconButtonKey = GlobalKey();
PlayerScreenTopRow({super.key, this.textSize, this.iconSize, this.textWidth, this.short});
PlayerScreenTopRow(
{super.key, this.textSize, this.iconSize, this.textWidth, this.short});
@override
Widget build(BuildContext context) {
@@ -825,7 +913,9 @@ class PlayerScreenTopRow extends StatelessWidget {
child: Text(
(short ?? false)
? (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? '')
: 'Playing from:'.i18n + ' ' + (GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
: 'Playing from:'.i18n +
' ' +
(GetIt.I<AudioPlayerHandler>().queueSource?.text ?? ''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
@@ -844,11 +934,14 @@ class PlayerScreenTopRow extends StatelessWidget {
splashRadius: iconSize ?? ScreenUtil().setWidth(52),
onPressed: () async {
//Fix bottom buttons (Not needed anymore?)
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
// Calculate the center of the icon
final RenderBox buttonRenderBox = iconButtonKey.currentContext!.findRenderObject() as RenderBox;
final Offset buttonOffset = buttonRenderBox.localToGlobal(buttonRenderBox.size.center(Offset.zero));
final RenderBox buttonRenderBox =
iconButtonKey.currentContext!.findRenderObject() as RenderBox;
final Offset buttonOffset = buttonRenderBox
.localToGlobal(buttonRenderBox.size.center(Offset.zero));
//Navigate
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
await Navigator.of(context).push(CircularExpansionRoute(
@@ -879,7 +972,8 @@ class _SeekBarState extends State<SeekBar> {
double get position {
if (_seeking) return _pos;
double p = audioHandler.playbackState.value.position.inMilliseconds.toDouble();
double p =
audioHandler.playbackState.value.position.inMilliseconds.toDouble();
if (p > duration) return duration;
return p;
}
@@ -904,17 +998,20 @@ class _SeekBarState extends State<SeekBar> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
padding:
const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
_timeString(position),
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
style: TextStyle(
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
),
Text(
_timeString(duration),
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
style: TextStyle(
fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
)
],
),
@@ -924,7 +1021,8 @@ class _SeekBarState extends State<SeekBar> {
child: Slider(
focusNode: FocusNode(
canRequestFocus: false,
skipTraversal: true), // Don't focus on Slider - it doesn't work (and not needed)
skipTraversal:
true), // Don't focus on Slider - it doesn't work (and not needed)
value: position,
max: duration,
onChangeStart: (double d) {
@@ -1003,7 +1101,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
final queueState = audioHandler.queueState;
final shuffleModeEnabled = queueState.shuffleMode == AudioServiceShuffleMode.all;
final shuffleModeEnabled =
queueState.shuffleMode == AudioServiceShuffleMode.all;
return Scaffold(
appBar: FreezerAppBar(
@@ -1014,9 +1113,10 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
child: IconButton(
icon: Icon(
//cons.shuffle,
ReFreezerIcons.shuffle,
DeezerIcons.shuffle,
semanticLabel: 'Shuffle'.i18n,
color: shuffleModeEnabled ? Theme.of(context).primaryColor : null,
color:
shuffleModeEnabled ? Theme.of(context).primaryColor : null,
),
onPressed: () async {
await audioHandler.toggleShuffle();
@@ -1032,7 +1132,8 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
),
onPressed: () async {
await audioHandler.clearQueue();
mainNavigatorKey.currentState!.popUntil((route) => route.isFirst);
mainNavigatorKey.currentState!
.popUntil((route) => route.isFirst);
},
),
)

View File

@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttericon/typicons_icons.dart';
import 'package:get_it/get_it.dart';
import 'package:refreezer/settings.dart';
import '../api/cache.dart';
import '../api/deezer.dart';
@@ -218,6 +219,7 @@ class _SearchScreenState extends State<SearchScreen> {
focusNode: _textFieldFocusNode,
decoration: InputDecoration(
labelText: 'Search or paste URL'.i18n,
hintStyle: TextStyle(color: settings.primaryColor),
fillColor:
Theme.of(context).bottomAppBarTheme.color,
filled: true,

View File

@@ -56,33 +56,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: <Widget>[
ListTile(
title: Text('General'.i18n),
leading: const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const GeneralSettings())),
leading:
const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const GeneralSettings())),
),
ListTile(
title: Text('Download Settings'.i18n),
leading: const LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadsSettings())),
leading: const LeadingIcon(Icons.cloud_download,
color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const DownloadsSettings())),
),
ListTile(
title: Text('Appearance'.i18n),
leading: const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const AppearanceSettings())),
leading:
const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AppearanceSettings())),
),
ListTile(
title: Text('Quality'.i18n),
leading: const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const QualitySettings())),
leading:
const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const QualitySettings())),
),
ListTile(
title: Text('Deezer'.i18n),
leading: const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const DeezerSettings())),
leading:
const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeezerSettings())),
),
//Language select
ListTile(
title: Text('Language'.i18n),
leading: const LeadingIcon(Icons.language, color: Color(0xff009a85)),
leading:
const LeadingIcon(Icons.language, color: Color(0xff009a85)),
onTap: () {
showDialog(
context: context,
@@ -94,8 +111,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: Text(l.name),
subtitle: Text('${l.locale}-${l.country}'),
onTap: () async {
I18n.of(customNavigatorKey.currentContext!).locale = Locale(l.locale, l.country);
setState(() => settings.language = '${l.locale}_${l.country}');
I18n.of(customNavigatorKey.currentContext!).locale =
Locale(l.locale, l.country);
setState(() =>
settings.language = '${l.locale}_${l.country}');
await settings.save();
// Close the SimpleDialog
if (context.mounted) Navigator.of(context).pop();
@@ -107,12 +126,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
ListTile(
title: Text('Updates'.i18n),
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const UpdaterScreen())),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => const UpdaterScreen())),
),
ListTile(
title: Text('About'.i18n),
leading: const LeadingIcon(Icons.info, color: Colors.grey),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CreditsScreen())),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => const CreditsScreen())),
),
],
),
@@ -138,7 +159,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
children: <Widget>[
ListTile(
title: Text('Theme'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
subtitle: Text('Currently'.i18n +
': ${settings.theme.toString().split('.').last}'),
leading: const Icon(Icons.color_lens),
onTap: () {
showDialog(
@@ -206,7 +228,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
leading: const Icon(Icons.font_download),
subtitle: Text(settings.font),
onTap: () {
showDialog(context: context, builder: (context) => FontSelector(() => Navigator.of(context).pop()));
showDialog(
context: context,
builder: (context) =>
FontSelector(() => Navigator.of(context).pop()));
},
),
ListTile(
@@ -234,7 +259,9 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
),
ListTile(
title: Text('Visualizer'.i18n),
subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n),
subtitle: Text(
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
.i18n),
leading: const Icon(Icons.equalizer),
trailing: Switch(
value: settings.lyricsVisualizer,
@@ -260,20 +287,19 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Primary color'.i18n),
title: Text('Primary colors'.i18n),
content: SizedBox(
height: 240,
child: MaterialColorPicker(
colors: [
...Colors.primaries,
//Logo colors
_swatch(0xffeca704),
_swatch(0xffbe3266),
_swatch(0xFFFF3386),
_swatch(0xff4b2e7e),
_swatch(0xff384697),
_swatch(0xff0880b5),
_swatch(0xff009a85),
_swatch(0xff2ba766)
_swatch(0xFFA238FF),
],
allowShades: false,
selectedColor: settings.primaryColor,
@@ -291,15 +317,6 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
});
},
),
ListTile(
title: Text('Use album art primary color'.i18n),
subtitle: Text('Warning: might be buggy'.i18n),
leading: const Icon(Icons.invert_colors),
trailing: Switch(
value: settings.useArtColor,
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
),
),
//Display mode
ListTile(
leading: const Icon(Icons.screen_lock_portrait),
@@ -320,7 +337,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
onPressed: () async {
settings.displayMode = i;
await settings.save();
await FlutterDisplayMode.setPreferredMode(modes[i]);
await FlutterDisplayMode.setPreferredMode(
modes[i]);
if (context.mounted) {
Navigator.of(context).pop();
}
@@ -347,7 +365,9 @@ class FontSelector extends StatefulWidget {
class _FontSelectorState extends State<FontSelector> {
String query = '';
List<String> get fonts {
return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList();
return settings.fonts
.where((f) => f.toLowerCase().contains(query))
.toList();
}
//Font selected
@@ -421,25 +441,29 @@ class _QualitySettingsState extends State<QualitySettings> {
children: <Widget>[
ListTile(
title: Text('Mobile streaming'.i18n),
leading: const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
leading:
const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
),
const QualityPicker('mobile'),
const FreezerDivider(),
ListTile(
title: Text('Wifi streaming'.i18n),
leading: const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
leading:
const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
),
const QualityPicker('wifi'),
const FreezerDivider(),
ListTile(
title: Text('Offline'.i18n),
leading: const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
leading:
const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
),
const QualityPicker('offline'),
const FreezerDivider(),
ListTile(
title: Text('External downloads'.i18n),
leading: const LeadingIcon(Icons.file_download, color: Color(0xff2ba766)),
leading: const LeadingIcon(Icons.file_download,
color: Color(0xff2ba766)),
),
const QualityPicker('download'),
],
@@ -608,7 +632,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
children: <Widget>[
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: const Icon(Icons.language),
onTap: () {
showDialog(
@@ -621,7 +646,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
title: Text(ContentLanguage.all[i].name),
subtitle: Text(ContentLanguage.all[i].code),
onTap: () async {
setState(() => settings.deezerLanguage = ContentLanguage.all[i].code);
setState(() => settings.deezerLanguage =
ContentLanguage.all[i].code);
await settings.save();
if (context.mounted) {
Navigator.of(context).pop();
@@ -633,7 +659,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
),
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: const Icon(Icons.vpn_lock),
onTap: () {
showDialog(
@@ -655,7 +682,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
],
),
onValuePicked: (Country country) {
setState(() => settings.deezerCountry = country.isoCode ?? 'us');
setState(() =>
settings.deezerCountry = country.isoCode ?? 'us');
settings.save();
},
));
@@ -663,7 +691,9 @@ class _DeezerSettingsState extends State<DeezerSettings> {
),
ListTile(
title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.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) {
@@ -764,7 +794,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
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,
"If you want to use custom directory naming - use '/' as directory separator."
.i18n,
style: const TextStyle(
fontSize: 12.0,
),
@@ -779,7 +810,8 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
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%';
},
),
@@ -808,7 +840,8 @@ class DownloadsSettings extends StatefulWidget {
class _DownloadsSettingsState extends State<DownloadsSettings> {
double _downloadThreads = settings.downloadThreads.toDouble();
final TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator);
final TextEditingController _artistSeparatorController =
TextEditingController(text: settings.artistSeparator);
@override
Widget build(BuildContext context) {
@@ -852,7 +885,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
showDialog(
context: context,
builder: (context) {
return FilenameTemplateDialog(settings.downloadFilename, (f) async {
return FilenameTemplateDialog(settings.downloadFilename,
(f) async {
setState(() => settings.downloadFilename = f);
await settings.save();
});
@@ -861,13 +895,15 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
),
ListTile(
title: Text('Singleton naming'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'),
subtitle:
Text('Currently'.i18n + ': ${settings.singletonFilename}'),
leading: const 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();
});
@@ -877,7 +913,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Download threads'.i18n + ': ${_downloadThreads.round().toString()}',
'Download threads'.i18n +
': ${_downloadThreads.round().toString()}',
style: const TextStyle(fontSize: 16.0),
),
),
@@ -897,14 +934,17 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
await settings.save();
//Prevent null
if (val > 8 && cache.threadsWarning != true && context.mounted) {
if (val > 8 &&
cache.threadsWarning != true &&
context.mounted) {
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),
'Using too many concurrent downloads on older/weaker devices might cause crashes!'
.i18n),
actions: [
TextButton(
child: Text('Dismiss'.i18n),
@@ -922,8 +962,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
ListTile(
title: Text('Tags'.i18n),
leading: const Icon(Icons.label),
onTap: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TagSelectionScreen())),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const TagSelectionScreen())),
),
ListTile(
title: Text('Create folders for artist'.i18n),
@@ -1010,17 +1050,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
leading: const Icon(Icons.image)),
ListTile(
title: Text('Album cover resolution'.i18n),
subtitle: Text("WARNING: Resolutions above 1200 aren't officially supported".i18n),
subtitle: Text(
"WARNING: Resolutions above 1200 aren't officially supported"
.i18n),
leading: const Icon(Icons.image),
trailing: SizedBox(
width: 75.0,
child: DropdownButton<int>(
value: settings.albumArtResolution,
items: [400, 800, 1000, 1200, 1400, 1600, 1800]
.map<DropdownMenuItem<int>>((int i) => DropdownMenuItem<int>(
value: i,
child: Text(i.toString()),
))
.map<DropdownMenuItem<int>>(
(int i) => DropdownMenuItem<int>(
value: i,
child: Text(i.toString()),
))
.toList(),
onChanged: (int? n) async {
setState(() {
@@ -1031,7 +1074,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
))),
ListTile(
title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n),
subtitle:
Text('To prevent gallery being filled with album art'.i18n),
trailing: Switch(
value: settings.nomediaFiles,
onChanged: (v) {
@@ -1058,7 +1102,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
ListTile(
title: Text('Download Log'.i18n),
leading: const Icon(Icons.sticky_note_2),
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadLogViewer())),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const DownloadLogViewer())),
)
],
),
@@ -1159,7 +1204,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
setState(() => settings.offlineMode = false);
} else {
Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.'.i18n,
msg:
'Error logging in, check your internet connections.'
.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
}
@@ -1179,7 +1226,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
ListTile(
title: Text('Copy ARL'.i18n),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
subtitle:
Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
leading: const Icon(Icons.lock),
onTap: () async {
await FlutterClipboard.copy(settings.arl ?? '');
@@ -1190,7 +1238,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
ListTile(
title: Text('Enable equalizer'.i18n),
subtitle: Text('Might enable some equalizer apps to work. Requires restart of ReFreezer'.i18n),
subtitle: Text(
'Might enable some equalizer apps to work. Requires restart of ReFreezer'
.i18n),
leading: const Icon(Icons.equalizer),
trailing: Switch(
value: settings.enableEqualizer,
@@ -1202,7 +1252,9 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
ListTile(
title: Text('LastFM'.i18n),
subtitle: Text((settings.lastFMUsername != null) ? 'Log out'.i18n : 'Login to enable scrobbling.'.i18n),
subtitle: Text((settings.lastFMUsername != null)
? 'Log out'.i18n
: 'Login to enable scrobbling.'.i18n),
leading: const Icon(FontAwesome5.lastfm),
onTap: () async {
if (settings.lastFMUsername != null) {
@@ -1241,8 +1293,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
ListTile(
title: Text('Application Log'.i18n),
leading: const Icon(Icons.sticky_note_2),
onTap: () =>
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ApplicationLogViewer())),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const ApplicationLogViewer())),
),
const FreezerDivider(),
ListTile(
@@ -1341,7 +1393,10 @@ class _LastFMLoginState extends State<LastFMLogin> {
LastFM last;
try {
last = await LastFM.authenticate(
apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: _username, password: _password);
apiKey: Env.lastFmApiKey,
apiSecret: Env.lastFmApiSecret,
username: _username,
password: _password);
} catch (e) {
Logger.root.severe('Error authorizing LastFM: $e');
Fluttertoast.showToast(msg: 'Authorization error!'.i18n);
@@ -1365,23 +1420,30 @@ class StorageInfo {
final String appFilesDir;
final int availableBytes;
StorageInfo({required this.rootDir, required this.appFilesDir, required this.availableBytes});
StorageInfo(
{required this.rootDir,
required this.appFilesDir,
required this.availableBytes});
}
Future<List<StorageInfo>> getStorageInfo() async {
final externalDirectories = await ExternalPath.getExternalStorageDirectories();
final externalDirectories =
await ExternalPath.getExternalStorageDirectories();
List<StorageInfo> storageInfoList = [];
if (externalDirectories.isNotEmpty) {
for (var dir in externalDirectories) {
var availableMegaBytes = (await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
var availableMegaBytes =
(await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0;
storageInfoList.add(
StorageInfo(
rootDir: dir,
appFilesDir: dir,
availableBytes: availableMegaBytes > 0 ? (availableMegaBytes * 1000000).floor() : 0,
availableBytes: availableMegaBytes > 0
? (availableMegaBytes * 1000000).floor()
: 0,
),
);
}
@@ -1453,14 +1515,17 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
children: <Widget>[
CircularProgressIndicator()
],
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
...List.generate(snapshot.data?.length ?? 0, (i) {
...List.generate(snapshot.data?.length ?? 0,
(i) {
StorageInfo si = snapshot.data![i];
return ListTile(
title: Text(si.rootDir),
@@ -1524,7 +1589,9 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
onTap: () {
setState(() {
if (_root == _path) {
Fluttertoast.showToast(msg: 'Permission denied'.i18n, gravity: ToastGravity.BOTTOM);
Fluttertoast.showToast(
msg: 'Permission denied'.i18n,
gravity: ToastGravity.BOTTOM);
return;
}
_previous = _path;
@@ -1618,8 +1685,10 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
const FreezerDivider(),
const ListTile(
title: Text('DJDoubleD', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('Developer, tester, new icon & logo, some translations, ...'),
title: Text('DJDoubleD',
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
'Developer, tester, new icon & logo, some translations, ...'),
),
const FreezerDivider(),
/*ListTile(
@@ -1657,7 +1726,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
ListTile(
title: Text('Crowdin'.i18n),
subtitle: Text('Help translating this app on Crowdin!'.i18n),
leading: const Icon(ReFreezerIcons.crowdin, color: Color(0xffbdc1c6), size: 36.0),
leading: const Icon(ReFreezerIcons.crowdin,
color: Color(0xffbdc1c6), size: 36.0),
onTap: () {
launchUrlString('https://crowdin.com/project/refreezer');
},
@@ -1665,15 +1735,20 @@ class _CreditsScreenState extends State<CreditsScreen> {
ListTile(
isThreeLine: true,
title: Text('Donate'.i18n),
subtitle: Text('You should rather support your favorite artists, instead of this app!'.i18n),
leading: const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
subtitle: Text(
'You should rather support your favorite artists, instead of this app!'
.i18n),
leading:
const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Donate'.i18n),
content: Text('No really, go support your favorite artists instead ;)'.i18n),
content: Text(
'No really, go support your favorite artists instead ;)'
.i18n),
actions: [
TextButton(
child: const Text('OK'),
@@ -1704,10 +1779,12 @@ class _CreditsScreenState extends State<CreditsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
Image.asset('assets/icon_legacy.png',
width: 24, height: 24),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'The original freezer development team'.i18n,
textAlign: TextAlign.center,
@@ -1720,7 +1797,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
),
),
Image.asset('assets/icon_legacy.png', width: 24, height: 24),
Image.asset('assets/icon_legacy.png',
width: 24, height: 24),
],
),
],
@@ -1739,7 +1817,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
const ListTile(
title: Text('Bas Curtiz'),
subtitle: Text('Icon, logo, banner, design suggestions, tester'),
subtitle:
Text('Icon, logo, banner, design suggestions, tester'),
),
const ListTile(
title: Text('Tobs'),

Submodule marquee deleted from 4241bdf46d

View File

@@ -174,6 +174,9 @@ flutter:
- family: ReFreezerIcons
fonts:
- asset: assets/fonts/ReFreezerIcons.ttf
- family: Deezer
fonts:
- asset: assets/fonts/Deezer.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.

Submodule scrobblenaut deleted from 6966422e6a