Merge pull request #39 from PetitPrinc3/master
MOD / Collaboration (Implement Deezer' app UI)
1
.gitignore
vendored
@@ -69,6 +69,7 @@ app.*.map.json
|
||||
# Envied
|
||||
.env
|
||||
lib/utils/env.g.dart
|
||||
lib/.env
|
||||
|
||||
# Possible translations remnants
|
||||
translations/refreezer.zip
|
||||
|
||||
38
README.md
@@ -1,13 +1,13 @@
|
||||

|
||||

|
||||
|
||||
[](../../releases/latest)
|
||||
[](../../releases/latest)
|
||||
[](../../releases)
|
||||
[](../../releases)
|
||||
[](../../releases/latest)
|
||||
[](../../releases/latest)
|
||||
[](../../releases)
|
||||
[](../../releases)
|
||||
[](https://docs.flutter.dev/tools/sdk)
|
||||
[](https://dart.dev/get-dart)
|
||||
[](https://crowdin.com/project/refreezer)
|
||||
[](./LICENSE)
|
||||
[](./LICENSE)
|
||||
|
||||
[](https://dart.dev/)
|
||||
[](https://flutter.dev/)
|
||||
@@ -15,17 +15,26 @@
|
||||
|
||||
---
|
||||
|
||||
An alternative Deezer music streaming & downloading client, based on Freezer.
|
||||
The entire codebase has been updated/rewritten to be compatible with the latest version of flutter, the dart SDK & android (current build target is API level 34).
|
||||
This repo is a MOD of the [ReFreezer](https://github.com/DJDoubleD/ReFreezer) app by @DJDoubleD.
|
||||
My goal is to have a style closer to Deezer's original app.
|
||||
To apply the Deezer theme, select it under Settings > Appearance > Theme.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
<img src="./assets/screenshots/Mod_home.png" width=200>
|
||||
<img src="./assets/screenshots/Mod_player.png" width=200>
|
||||
<img src="./assets/screenshots/Mod_search.png" width=200>
|
||||
</p>
|
||||
|
||||
<details><summary><b>Original ReFreezer App</b></summary>
|
||||
<p align="center">
|
||||
<img src="./assets/screenshots/Login.jpg" width=200>
|
||||
<img src="./assets/screenshots/Home.jpg" width=200>
|
||||
<img src="./assets/screenshots/Player.jpg" width=200>
|
||||
<img src="./assets/screenshots/Lyrics.jpg" width=200>
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details><summary><b>More Android Phone</b></summary>
|
||||
<p align="center">
|
||||
@@ -37,7 +46,6 @@ The entire codebase has been updated/rewritten to be compatible with the latest
|
||||
<img src="./assets/screenshots/PlayerHorizontal.jpg" height=200>
|
||||
</p>
|
||||
</details>
|
||||
</br>
|
||||
<details><summary><b>Android Auto</b></summary>
|
||||
<p align="center">
|
||||
<img src="./assets/screenshots/Android_Auto-Head_Unit-home.png" max-height=400>
|
||||
@@ -49,6 +57,11 @@ The entire codebase has been updated/rewritten to be compatible with the latest
|
||||
|
||||
## Features & changes
|
||||
|
||||
### Not working / On going
|
||||
- Offline playlist / titles access
|
||||
- Explore / Favorites page
|
||||
|
||||
### ReFreezer :
|
||||
- Restored all features of the old Freezer app, most notably:
|
||||
- Restored all login options
|
||||
- Restored Highest quality streaming and download options (premium account required, free accounts limited to MP3 128kbps)
|
||||
@@ -66,6 +79,13 @@ The entire codebase has been updated/rewritten to be compatible with the latest
|
||||
- Removed the need of custom just_audio & audio_service plugin versions & refactored source code to use the latest version of the official plugins
|
||||
- Multiple other fixes
|
||||
|
||||
### MOD :
|
||||
- Floating player bar with background color based on title artwork
|
||||
- Deezer original icons
|
||||
- Deezer original navigation menu (+ settings)
|
||||
- Deezer clone player screen
|
||||
- Deezer similar info menu
|
||||
|
||||
## Compile from source
|
||||
|
||||
Install the latest flutter SDK: <https://flutter.dev/docs/get-started/install>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 32 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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/DJDoubleD.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/PetitPrince.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
assets/app_icon.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 58 KiB |
BIN
assets/fonts/Deezer.ttf
Normal file
BIN
assets/icon.png
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 30 KiB |
BIN
assets/screenshots/Mod_home.png
Normal file
|
After Width: | Height: | Size: 800 KiB |
BIN
assets/screenshots/Mod_player.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/screenshots/Mod_search.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
162
lib/fonts/deezer_icons.dart
Normal file
@@ -0,0 +1,162 @@
|
||||
/// 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);
|
||||
static const IconData more_vert =
|
||||
IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -195,9 +198,13 @@ class Settings {
|
||||
}
|
||||
|
||||
SliderThemeData get _sliderTheme => SliderThemeData(
|
||||
thumbColor: primaryColor,
|
||||
activeTrackColor: primaryColor,
|
||||
inactiveTrackColor: primaryColor.withOpacity(0.2));
|
||||
inactiveTrackColor: primaryColor.withOpacity(0.2),
|
||||
trackHeight: 0.5,
|
||||
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 1),
|
||||
thumbColor: primaryColor,
|
||||
overlayShape: RoundSliderOverlayShape(overlayRadius: 4),
|
||||
overlayColor: primaryColor.withOpacity(0.2));
|
||||
|
||||
//Load settings/init
|
||||
Future<Settings> loadSettings() async {
|
||||
@@ -255,8 +262,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 +399,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 +470,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,
|
||||
|
||||
@@ -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?,
|
||||
),
|
||||
|
||||
@@ -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: 80.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,
|
||||
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: () {
|
||||
@@ -88,7 +91,7 @@ class FreezerTitle extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Image.asset('assets/icon.png', width: 64, height: 64),
|
||||
const Text(
|
||||
'ReFreezer',
|
||||
'Deezer',
|
||||
style: TextStyle(fontSize: 56, fontWeight: FontWeight.w900),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
315
lib/ui/menu.dart
@@ -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),
|
||||
@@ -54,78 +59,94 @@ class MenuSheet {
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 16.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.transparent),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(15),
|
||||
topRight: Radius.circular(15))),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: 'Album art'.i18n,
|
||||
image: true,
|
||||
child: CachedImage(
|
||||
url: track.albumArt?.full ?? '',
|
||||
height: 128,
|
||||
width: 128,
|
||||
),
|
||||
Container(
|
||||
height: 16.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: 240.0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
track.title ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: 'Album art'.i18n,
|
||||
image: true,
|
||||
child: CachedImage(
|
||||
url: track.albumArt?.full ?? '',
|
||||
height: 128,
|
||||
width: 128,
|
||||
circular: true,
|
||||
),
|
||||
Text(
|
||||
track.artistString ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 20.0),
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
SizedBox(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
track.title ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
track.artistString ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
Text(
|
||||
track.album?.title ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
Text(track.durationString ?? '')
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
Text(
|
||||
track.album?.title ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
Text(track.durationString ?? '')
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: 16.0,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape)
|
||||
? 200
|
||||
: 350,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: options),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: 16.0,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: (MediaQuery.of(context).orientation == Orientation.landscape) ? 200 : 350,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: options),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
//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 +159,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 +174,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 +189,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 +198,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 +210,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 +248,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 +263,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 +277,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 +295,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 +311,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 +356,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 +376,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 +400,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 +430,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 +446,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 +463,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 +479,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 +490,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 +500,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 +521,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 +545,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 +573,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 +594,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 +605,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 +641,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 +695,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 +707,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 +746,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 +918,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();
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,15 +15,14 @@ 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:share_plus/share_plus.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
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 +63,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 +90,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 +119,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 +132,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 +144,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 +154,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) {
|
||||
@@ -202,38 +219,50 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 8, 4, 0),
|
||||
child: PlayerScreenTopRow(
|
||||
textSize: ScreenUtil().setSp(28),
|
||||
iconSize: ScreenUtil().setSp(38),
|
||||
textWidth: ScreenUtil().setWidth(150),
|
||||
short: false)),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(30),
|
||||
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(30),
|
||||
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,
|
||||
@@ -248,7 +277,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: const SeekBar(24.0),
|
||||
),
|
||||
PlaybackControls(ScreenUtil().setSp(60)),
|
||||
PlaybackControls(ScreenUtil().setSp(40)),
|
||||
Padding(
|
||||
//padding: EdgeInsets.fromLTRB(4, 0, 4, 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@@ -261,13 +290,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,
|
||||
@@ -298,6 +330,8 @@ class PlayerScreenVertical extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||
final GlobalKey iconButtonKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -323,67 +357,130 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||
),
|
||||
),
|
||||
Container(height: 4.0),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: ScreenUtil().setSp(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),
|
||||
blankSpace: 32.0,
|
||||
startPadding: 10.0,
|
||||
accelerationDuration: const Duration(seconds: 1),
|
||||
pauseAfterRound: const Duration(seconds: 2),
|
||||
)
|
||||
: Text(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displayTitle ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold),
|
||||
)),
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
GetIt.I<AudioPlayerHandler>().mediaItem.value?.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(16),
|
||||
color: Theme.of(context).primaryColor,
|
||||
ActionControls(24.0),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(18, 0, 18, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SeekBar(8.0),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
height: ScreenUtil().setSp(18),
|
||||
child: (GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displayTitle ??
|
||||
'')
|
||||
.length >=
|
||||
26
|
||||
? Marquee(
|
||||
text: GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displayTitle ??
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(16),
|
||||
fontWeight: FontWeight.bold),
|
||||
blankSpace: 32.0,
|
||||
startPadding: 0,
|
||||
accelerationDuration: const Duration(seconds: 1),
|
||||
pauseAfterRound: const Duration(seconds: 2),
|
||||
)
|
||||
: Text(
|
||||
GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displayTitle ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(16),
|
||||
fontWeight: FontWeight.bold),
|
||||
)),
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
GetIt.I<AudioPlayerHandler>()
|
||||
.mediaItem
|
||||
.value
|
||||
?.displaySubtitle ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(12),
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SeekBar(12.0),
|
||||
PlaybackControls(ScreenUtil().setSp(36)),
|
||||
PlaybackControls(ScreenUtil().setSp(25)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
LyricsIconButton(20, afterOnPressed: updateColor),
|
||||
LyricsIconButton(24, afterOnPressed: updateColor),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.file_download,
|
||||
size: ScreenUtil().setWidth(20),
|
||||
DeezerIcons.download,
|
||||
size: ScreenUtil().setWidth(24),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
const QualityInfoWidget(),
|
||||
RepeatButton(ScreenUtil().setWidth(20)),
|
||||
const PlayerMenuButton()
|
||||
RepeatButton(ScreenUtil().setWidth(24)),
|
||||
IconButton(
|
||||
key: iconButtonKey,
|
||||
icon: Icon(
|
||||
//Icons.menu,
|
||||
DeezerIcons.queue,
|
||||
semanticLabel: 'Queue'.i18n,
|
||||
),
|
||||
iconSize: ScreenUtil().setWidth(24),
|
||||
onPressed: () async {
|
||||
//Fix bottom buttons (Not needed anymore?)
|
||||
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));
|
||||
//Navigate
|
||||
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
|
||||
await Navigator.of(context).push(CircularExpansionRoute(
|
||||
widget: const QueueScreen(),
|
||||
//centerAlignment: Alignment.topRight,
|
||||
centerOffset: buttonOffset)); // Expand from icon
|
||||
//Fix colors
|
||||
updateColor();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -407,7 +504,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 +547,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 +574,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 +622,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 +661,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,
|
||||
@@ -583,6 +694,123 @@ class _RepeatButtonState extends State<RepeatButton> {
|
||||
}
|
||||
}
|
||||
|
||||
class ActionControls extends StatefulWidget {
|
||||
final double iconSize;
|
||||
const ActionControls(this.iconSize, {super.key});
|
||||
|
||||
@override
|
||||
_ActionControls createState() => _ActionControls();
|
||||
}
|
||||
|
||||
class _ActionControls extends State<ActionControls> {
|
||||
AudioPlayerHandler audioHandler = GetIt.I<AudioPlayerHandler>();
|
||||
Icon get libraryIcon {
|
||||
if (cache.checkTrackFavorite(
|
||||
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
return Icon(
|
||||
DeezerIcons.heart_fill,
|
||||
color: settings.primaryColor,
|
||||
size: widget.iconSize,
|
||||
semanticLabel: 'Unlove'.i18n,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
DeezerIcons.heart,
|
||||
size: widget.iconSize,
|
||||
semanticLabel: 'Love'.i18n,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String? id = Track.fromMediaItem(audioHandler.mediaItem.value!).id;
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Share.share('https://deezer.com/track/$id');
|
||||
},
|
||||
icon: Icon(
|
||||
DeezerIcons.share_android,
|
||||
size: widget.iconSize,
|
||||
semanticLabel: 'Share'.i18n,
|
||||
)),
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(12, 0, 12, 0),
|
||||
padding: EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Settings.secondaryText, width: 1),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
DeezerIcons.more_vert,
|
||||
size: widget.iconSize * 1.25,
|
||||
semanticLabel: 'Options'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
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)]);
|
||||
} else {
|
||||
m.defaultShowEpisodeMenu(
|
||||
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)]);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: libraryIcon,
|
||||
onPressed: () async {
|
||||
cache.libraryTracks ??= [];
|
||||
|
||||
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);
|
||||
await cache.save();
|
||||
} else {
|
||||
//Add
|
||||
setState(() =>
|
||||
cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI
|
||||
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
||||
await cache.save();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaybackControls extends StatefulWidget {
|
||||
final double iconSize;
|
||||
const PlaybackControls(this.iconSize, {super.key});
|
||||
@@ -593,32 +821,18 @@ 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!))) {
|
||||
return Icon(
|
||||
Icons.favorite,
|
||||
size: widget.iconSize * 0.44,
|
||||
semanticLabel: 'Unlove'.i18n,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
Icons.favorite_border,
|
||||
size: widget.iconSize * 0.44,
|
||||
semanticLabel: 'Love'.i18n,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 64.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
IconButton(
|
||||
/*IconButton(
|
||||
icon: Icon(
|
||||
Icons.sentiment_very_dissatisfied,
|
||||
DeezerIcons.angry_face,
|
||||
size: widget.iconSize * 0.44,
|
||||
semanticLabel: 'Dislike'.i18n,
|
||||
),
|
||||
@@ -627,27 +841,15 @@ class _PlaybackControlsState extends State<PlaybackControls> {
|
||||
if (audioHandler.queueState.hasNext) {
|
||||
audioHandler.skipToNext();
|
||||
}
|
||||
}),
|
||||
PrevNextButton(widget.iconSize, prev: true),
|
||||
PlayPauseButton(widget.iconSize * 1.25),
|
||||
PrevNextButton(widget.iconSize),
|
||||
IconButton(
|
||||
icon: libraryIcon,
|
||||
onPressed: () async {
|
||||
cache.libraryTracks ??= [];
|
||||
|
||||
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);
|
||||
await cache.save();
|
||||
} else {
|
||||
//Add
|
||||
setState(() => cache.libraryTracks?.add(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
||||
await cache.save();
|
||||
}
|
||||
},
|
||||
}),*/
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 12),
|
||||
child: PrevNextButton(widget.iconSize, prev: true),
|
||||
),
|
||||
PlayPauseButton(widget.iconSize),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: PrevNextButton(widget.iconSize),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -679,7 +881,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 +901,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 +923,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 +1005,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 +1032,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,
|
||||
@@ -833,32 +1042,6 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: iconButtonKey,
|
||||
icon: Icon(
|
||||
//Icons.menu,
|
||||
Icons.queue_music,
|
||||
semanticLabel: 'Queue'.i18n,
|
||||
),
|
||||
iconSize: iconSize ?? ScreenUtil().setSp(52),
|
||||
splashRadius: iconSize ?? ScreenUtil().setWidth(52),
|
||||
onPressed: () async {
|
||||
//Fix bottom buttons (Not needed anymore?)
|
||||
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));
|
||||
//Navigate
|
||||
//await Navigator.of(context).push(MaterialPageRoute(builder: (context) => QueueScreen()));
|
||||
await Navigator.of(context).push(CircularExpansionRoute(
|
||||
widget: const QueueScreen(),
|
||||
//centerAlignment: Alignment.topRight,
|
||||
centerOffset: buttonOffset)); // Expand from icon
|
||||
//Fix colors
|
||||
updateColor();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -879,7 +1062,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;
|
||||
}
|
||||
@@ -900,55 +1084,64 @@ class _SeekBarState extends State<SeekBar> {
|
||||
return StreamBuilder(
|
||||
stream: Stream.periodic(const Duration(milliseconds: 250)),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
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)),
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
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)),
|
||||
),
|
||||
Text(
|
||||
_timeString(duration),
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
ScreenUtil().setSp(widget.relativeTextSize)),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
_timeString(duration),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 32.0,
|
||||
child: Slider(
|
||||
focusNode: FocusNode(
|
||||
canRequestFocus: false,
|
||||
skipTraversal: true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||
value: position,
|
||||
max: duration,
|
||||
onChangeStart: (double d) {
|
||||
setState(() {
|
||||
_seeking = true;
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChanged: (double d) {
|
||||
setState(() {
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double d) async {
|
||||
await audioHandler.seek(Duration(milliseconds: d.round()));
|
||||
setState(() {
|
||||
_pos = d;
|
||||
_seeking = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
),
|
||||
SizedBox(
|
||||
height: 32.0,
|
||||
child: Slider(
|
||||
focusNode: FocusNode(
|
||||
canRequestFocus: false,
|
||||
skipTraversal:
|
||||
true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||
value: position,
|
||||
max: duration,
|
||||
onChangeStart: (double d) {
|
||||
setState(() {
|
||||
_seeking = true;
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChanged: (double d) {
|
||||
setState(() {
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double d) async {
|
||||
await audioHandler
|
||||
.seek(Duration(milliseconds: d.round()));
|
||||
setState(() {
|
||||
_pos = d;
|
||||
_seeking = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1003,7 +1196,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 +1208,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();
|
||||
@@ -1027,12 +1222,13 @@ class _QueueScreenState extends State<QueueScreen> with WidgetsBindingObserver {
|
||||
padding: const EdgeInsets.fromLTRB(0, 4, 16, 0),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
DeezerIcons.trash,
|
||||
semanticLabel: 'Clear all'.i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
await audioHandler.clearQueue();
|
||||
mainNavigatorKey.currentState!.popUntil((route) => route.isFirst);
|
||||
mainNavigatorKey.currentState!
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,8 @@ 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/fonts/deezer_icons.dart';
|
||||
import 'package:refreezer/settings.dart';
|
||||
|
||||
import '../api/cache.dart';
|
||||
import '../api/deezer.dart';
|
||||
@@ -161,7 +163,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.file_download,
|
||||
DeezerIcons.download,
|
||||
semanticLabel: 'Download'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -171,7 +173,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.settings,
|
||||
DeezerIcons.settings,
|
||||
semanticLabel: 'Settings'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -218,6 +220,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,
|
||||
@@ -446,7 +449,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
_suggestions.length,
|
||||
(i) => ListTile(
|
||||
title: Text(_suggestions[i]),
|
||||
leading: const Icon(Icons.search),
|
||||
leading: const Icon(DeezerIcons.search),
|
||||
onTap: () {
|
||||
setState(() => _query = _suggestions[i]);
|
||||
_submit(context);
|
||||
|
||||
1
marquee
@@ -153,6 +153,8 @@ flutter:
|
||||
- assets/icon.png
|
||||
- assets/favorites_thumb.jpg
|
||||
- assets/browse_icon.png
|
||||
- assets/DJDoubleD.jpg
|
||||
- assets/PetitPrince.png
|
||||
|
||||
fonts:
|
||||
# - family: Montserrat
|
||||
@@ -174,6 +176,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.
|
||||
|
||||