diff --git a/.gitignore b/.gitignore index 775807d..e2b0a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ app.*.map.json # Envied .env lib/utils/env.g.dart +lib/.env # Possible translations remnants translations/refreezer.zip diff --git a/README.md b/README.md index e1a4c10..1eb4bc5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -![ReFreezer](./assets/banner.png?raw=true) +![Deezer](./assets/banner.png?raw=true) -[![Latest Version](https://img.shields.io/github/v/release/DJDoubleD/ReFreezer?color=blue)](../../releases/latest) -[![Release date](https://img.shields.io/github/release-date/DJDoubleD/ReFreezer)](../../releases/latest) -[![Downloads Latest](https://img.shields.io/github/downloads/DJDoubleD/ReFreezer/latest/total?color=blue&label=downloads%20latest)](../../releases) -[![Downloads Total](https://img.shields.io/github/downloads/DJDoubleD/ReFreezer/total?color=blue&label=downloads%20total)](../../releases) +[![Latest Version](https://img.shields.io/github/v/release/PetitPrinc3/Deezer?color=blue)](../../releases/latest) +[![Release date](https://img.shields.io/github/release-date/PetitPrinc3/Deezer)](../../releases/latest) +[![Downloads Original](https://img.shields.io/github/downloads/DJDoubleD/ReFreezer/total?color=blue&label=downloads%20total)](../../releases) +[![Downloads MOD](https://img.shields.io/github/downloads/PetitPrinc3/Deezer/total?color=blue&label=downloads%20total)](../../releases) [![Flutter Version](https://shields.io/badge/Flutter-v3.24.4-darkgreen.svg)](https://docs.flutter.dev/tools/sdk) [![Dart Version](https://shields.io/badge/Dart-v3.5.4-darkgreen.svg)](https://dart.dev/get-dart) [![Crowdin](https://badges.crowdin.net/refreezer/localized.svg)](https://crowdin.com/project/refreezer) -[![License](https://img.shields.io/github/license/DJDoubleD/ReFreezer?flat)](./LICENSE) +[![License](https://img.shields.io/github/license/PetitPrinc3/Deezer?flat)](./LICENSE) [![Dart](https://img.shields.io/badge/Dart-0175C2?style=for-the-badge&logo=dart&logoColor=white)](https://dart.dev/) [![Flutter](https://img.shields.io/badge/Flutter-02569B?style=for-the-badge&logo=flutter&logoColor=white)](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 +

+ + + +

+ +
Original ReFreezer App

+
More Android Phone

@@ -37,7 +46,6 @@ The entire codebase has been updated/rewritten to be compatible with the latest

-
Android Auto

@@ -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: diff --git a/android/app/src/main/res/drawable-hdpi/ic_logo.png b/android/app/src/main/res/drawable-hdpi/ic_logo.png index 691dd95..360ef00 100644 Binary files a/android/app/src/main/res/drawable-hdpi/ic_logo.png and b/android/app/src/main/res/drawable-hdpi/ic_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_logo.png b/android/app/src/main/res/drawable-mdpi/ic_logo.png index 32d0fd5..be8d608 100644 Binary files a/android/app/src/main/res/drawable-mdpi/ic_logo.png and b/android/app/src/main/res/drawable-mdpi/ic_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_logo.png b/android/app/src/main/res/drawable-xhdpi/ic_logo.png index 5da585d..ae86f2e 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_logo.png and b/android/app/src/main/res/drawable-xhdpi/ic_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_logo.png b/android/app/src/main/res/drawable-xxhdpi/ic_logo.png index 1611d13..ec0bf82 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_logo.png and b/android/app/src/main/res/drawable-xxhdpi/ic_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_logo.png b/android/app/src/main/res/drawable-xxxhdpi/ic_logo.png index 78a8ea4..e1fd8c3 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_logo.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_logo.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 80254ed..4b42b70 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 7bfd6f7..b5a2e54 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index bb82d93..4b42b70 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 134334d..11ada26 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index ba1669c..c79e842 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index ce5e2ef..11ada26 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 967e08a..d7ff0c0 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 72be683..231914e 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 37abb73..d7ff0c0 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 79a2b3d..e9d29c9 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 02a80a5..455ffcd 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 6be04b4..e9d29c9 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 801d216..5a6d35c 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index 1fc6edf..705648b 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 8d56068..5a6d35c 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 06952be..0277e8f 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -13,6 +13,6 @@ This Theme is only used starting with V2 of Flutter's Android embedding. --> diff --git a/android/app/src/main/res/values/ic_favorites_background.xml b/android/app/src/main/res/values/ic_favorites_background.xml index e5d934a..0069995 100644 --- a/android/app/src/main/res/values/ic_favorites_background.xml +++ b/android/app/src/main/res/values/ic_favorites_background.xml @@ -1,4 +1,4 @@ - #3DDC84 + #0F0D13 \ No newline at end of file diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml index e2f223d..2f48864 100644 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #1C1C14 + #0F0D13 \ No newline at end of file diff --git a/assets/DJDoubleD.jpg b/assets/DJDoubleD.jpg new file mode 100644 index 0000000..86f0c50 Binary files /dev/null and b/assets/DJDoubleD.jpg differ diff --git a/assets/PetitPrince.png b/assets/PetitPrince.png new file mode 100644 index 0000000..dd68f76 Binary files /dev/null and b/assets/PetitPrince.png differ diff --git a/assets/app_icon.png b/assets/app_icon.png new file mode 100644 index 0000000..05d36de Binary files /dev/null and b/assets/app_icon.png differ diff --git a/assets/banner.png b/assets/banner.png index fac6d23..8bee473 100644 Binary files a/assets/banner.png and b/assets/banner.png differ diff --git a/assets/fonts/Deezer.ttf b/assets/fonts/Deezer.ttf new file mode 100644 index 0000000..4992a47 Binary files /dev/null and b/assets/fonts/Deezer.ttf differ diff --git a/assets/icon.png b/assets/icon.png index 42ba89e..18debb9 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/screenshots/Mod_home.png b/assets/screenshots/Mod_home.png new file mode 100644 index 0000000..ced4991 Binary files /dev/null and b/assets/screenshots/Mod_home.png differ diff --git a/assets/screenshots/Mod_player.png b/assets/screenshots/Mod_player.png new file mode 100644 index 0000000..69fe856 Binary files /dev/null and b/assets/screenshots/Mod_player.png differ diff --git a/assets/screenshots/Mod_search.png b/assets/screenshots/Mod_search.png new file mode 100644 index 0000000..3e168a0 Binary files /dev/null and b/assets/screenshots/Mod_search.png differ diff --git a/custom_navigator b/custom_navigator deleted file mode 160000 index bef1bad..0000000 --- a/custom_navigator +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bef1badfa66f5d1aa265555ef675e65e453bfd5d diff --git a/equalizer_flutter b/equalizer_flutter deleted file mode 160000 index 2fd74d8..0000000 --- a/equalizer_flutter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2fd74d8a6a95d0e6a07a07210f051c4ead283597 diff --git a/external_path b/external_path deleted file mode 160000 index c753b1a..0000000 --- a/external_path +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c753b1a154bbdcac297682ead6c9cae16c6441fd diff --git a/lib/fonts/deezer_icons.dart b/lib/fonts/deezer_icons.dart new file mode 100644 index 0000000..c1d6742 --- /dev/null +++ b/lib/fonts/deezer_icons.dart @@ -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); +} diff --git a/lib/main.dart b/lib/main.dart index f1e9684..44913f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { @override Widget build(BuildContext context) { return MaterialApp( - title: 'ReFreezer', + title: 'Deezer', shortcuts: { ...WidgetsApp.defaultShortcuts, LogicalKeySet(LogicalKeyboardKey.select): @@ -202,8 +204,10 @@ class _MainScreenState extends State late final AppLifecycleListener _lifeCycleListener; final List _screens = [ const HomeScreen(), + const LibraryScreen(), + const LibraryPlaylists(), const SearchScreen(), - const LibraryScreen() + const SettingsScreen() ]; Future? _initialization; int _selected = 0; @@ -427,15 +431,26 @@ class _MainScreenState extends State 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: [ - 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 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( - 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 )); } else { // While audio_service is initializing - return const Scaffold( + return Scaffold( body: Center( - child: CircularProgressIndicator(), + child: CircularProgressIndicator( + color: Theme.of(context).primaryColor, + ), ), ); } diff --git a/lib/settings.dart b/lib/settings.dart index fb9f1a3..7caa425 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -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 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, diff --git a/lib/ui/elements.dart b/lib/ui/elements.dart index 03028b0..da84d06 100644 --- a/lib/ui/elements.dart +++ b/lib/ui/elements.dart @@ -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?, ), diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index c2b1434..13bdad9 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -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: [ @@ -32,7 +35,7 @@ class HomeScreen extends StatelessWidget { ) ], ), - )); + ))); } } @@ -49,7 +52,7 @@ class HomeAppBar extends StatelessWidget implements PreferredSizeWidget { actions: [ 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: [ Image.asset('assets/icon.png', width: 64, height: 64), const Text( - 'ReFreezer', + 'Deezer', style: TextStyle(fontSize: 56, fontWeight: FontWeight.w900), ) ], diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 4d5b7e1..37a01b9 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -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: [ 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 tracks = await deezerAPI.libraryShuffle(); GetIt.I().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 { ), 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 { 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 { 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), ], diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index 545b2f1..5afa75c 100644 --- a/lib/ui/menu.dart +++ b/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: [ - 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: [ - 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: [ - 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: [ + 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: [ + 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 options = const [], Function? onRemove}) { + {required BuildContext context, + List 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().insertQueueItem(-1, t.toMediaItem()); + await GetIt.I() + .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 options = const [], Function? onRemove}) { + {required BuildContext context, + List 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 options = const [], Function? onRemove}) { + {required BuildContext context, + List 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 options = const [], Function? onRemove, Function? onUpdate}) { + {required BuildContext context, + List 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 options = const []}) { - show(context, [shareTile('episode', e.id!), shareShow(s.id!), downloadExternalEpisode(e), ...options]); + defaultShowEpisodeMenu(Show s, ShowEpisode e, + {required BuildContext context, List 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 { 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 { 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 { 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().pause(); cache.sleepTimer?.cancel(); cache.sleepTimerTime = null; @@ -837,16 +918,20 @@ class _CreatePlaylistDialogState extends State { 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 tracks = []; tracks = widget.tracks?.map((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(); }, diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index 7e005e6..aeaa0ba 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -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 { - final double iconSize = 28; + AudioPlayerHandler audioHandler = GetIt.I(); + 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().playbackState.value.processingState == AudioProcessingState.idle) return 0.0; + if (GetIt.I().playbackState.value.processingState == + AudioProcessingState.idle) return 0.0; if (GetIt.I().mediaItem.value == null) return 0.0; - if (GetIt.I().mediaItem.value!.duration!.inSeconds == 0) return 0.0; //Division by 0 - return GetIt.I().playbackState.value.position.inSeconds / + if (GetIt.I().mediaItem.value!.duration!.inSeconds == 0) + return 0.0; //Division by 0 + return GetIt.I() + .playbackState + .value + .position + .inSeconds / GetIt.I().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().skipToPrevious(); - } else if ((details.primaryVelocity ?? 0) > 100) { - // Swiped right - await GetIt.I().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().skipToNext(); + updateColor(); + } else if ((details.primaryVelocity ?? 0) > 100) { + // Swiped right + await GetIt.I().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().mediaItem.value == null) { - return const SizedBox( - width: 0, - height: 0, - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - 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().mediaItem.value?.extras?['thumb'] ?? - GetIt.I().mediaItem.value?.artUri, - ), - title: Text( - GetIt.I().mediaItem.value?.displayTitle ?? '', - overflow: TextOverflow.clip, - maxLines: 1, - ), - subtitle: Text( - GetIt.I().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: [ - 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().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: [ + 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() + .mediaItem + .value + ?.extras?['thumb'] ?? + GetIt.I() + .mediaItem + .value + ?.artUri, + ), + title: Text( + GetIt.I() + .mediaItem + .value + ?.displayTitle ?? + '', + overflow: TextOverflow.clip, + maxLines: 1, + ), + subtitle: Text( + GetIt.I() + .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: [ + 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 with SingleTickerProviderStateMixin { +class _PlayPauseButtonState extends State + with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { - _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); - _animation = Tween(begin: 0, end: 1).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + _controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 200)); + _animation = Tween(begin: 0, end: 1) + .animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); super.initState(); } @@ -227,7 +312,9 @@ class _PlayPauseButtonState extends State 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 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(begin: 1, end: 0.75).animate(anim) + : Tween(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().pause() : () => GetIt.I().play()); + onPressed: playing + ? () => GetIt.I().pause() + : () => GetIt.I().play()); } switch (processingState) { @@ -256,7 +354,8 @@ class _PlayPauseButtonState extends State 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), ), ), ); diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index 806be6b..01d6f0c 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -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 { //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 { 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 { _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 { 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 { 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 { ), ), 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 { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - 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: [ SizedBox( height: ScreenUtil().setSp(50), - child: GetIt.I().mediaItem.value!.displayTitle!.length >= 22 + child: GetIt.I() + .mediaItem + .value! + .displayTitle! + .length >= + 22 ? Marquee( - text: GetIt.I().mediaItem.value!.displayTitle!, - style: TextStyle(fontSize: ScreenUtil().setSp(40), fontWeight: FontWeight.bold), + text: GetIt.I() + .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().mediaItem.value!.displayTitle!, + GetIt.I() + .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().mediaItem.value!.displaySubtitle ?? '', + GetIt.I() + .mediaItem + .value! + .displaySubtitle ?? + '', maxLines: 1, textAlign: TextAlign.center, overflow: TextOverflow.clip, @@ -248,7 +277,7 @@ class _PlayerScreenHorizontalState extends State { 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 { 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().mediaItem.value!); - if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) { + Track t = Track.fromMediaItem( + GetIt.I().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 { + final GlobalKey iconButtonKey = GlobalKey(); + @override Widget build(BuildContext context) { return Column( @@ -323,67 +357,130 @@ class _PlayerScreenVerticalState extends State { ), ), Container(height: 4.0), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: ScreenUtil().setSp(26), - child: (GetIt.I().mediaItem.value?.displayTitle ?? '').length >= 26 - ? Marquee( - text: GetIt.I().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().mediaItem.value?.displayTitle ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: ScreenUtil().setSp(22), fontWeight: FontWeight.bold), - )), - Container( - height: 4, - ), - Text( - GetIt.I().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: [ + SeekBar(8.0), + Container( + height: 8.0, ), - ), - ], + SizedBox( + height: ScreenUtil().setSp(18), + child: (GetIt.I() + .mediaItem + .value + ?.displayTitle ?? + '') + .length >= + 26 + ? Marquee( + text: GetIt.I() + .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() + .mediaItem + .value + ?.displayTitle ?? + '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: ScreenUtil().setSp(16), + fontWeight: FontWeight.bold), + )), + Container( + height: 4, + ), + Text( + GetIt.I() + .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: [ - 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().mediaItem.value!); - if (await downloadManager.addOfflineTrack(t, private: false, isSingleton: true) != false) { + Track t = Track.fromMediaItem( + GetIt.I().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 { //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 { 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().mediaItem.value!); + Track track = + Track.fromMediaItem(GetIt.I().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().mediaItem.value!); + Track t = + Track.fromMediaItem(GetIt.I().mediaItem.value!); MenuSheet m = MenuSheet(navigateCallback: () { Navigator.of(context).pop(); }); - if (GetIt.I().mediaItem.value!.extras?['show'] == null) { - m.defaultTrackMenu(t, context: context, options: [m.sleepTimer(context), m.wakelock(context)]); + if (GetIt.I().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().mediaItem.value!.extras?['show'])), - ShowEpisode.fromMediaItem(GetIt.I().mediaItem.value!), + Show.fromJson(jsonDecode(GetIt.I() + .mediaItem + .value! + .extras?['show'])), + ShowEpisode.fromMediaItem( + GetIt.I().mediaItem.value!), context: context, options: [m.sleepTimer(context), m.wakelock(context)]); } @@ -550,20 +661,20 @@ class _RepeatButtonState extends State { switch (GetIt.I().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 { } } +class ActionControls extends StatefulWidget { + final double iconSize; + const ActionControls(this.iconSize, {super.key}); + + @override + _ActionControls createState() => _ActionControls(); +} + +class _ActionControls extends State { + AudioPlayerHandler audioHandler = GetIt.I(); + 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().mediaItem.value!); + MenuSheet m = MenuSheet(navigateCallback: () { + Navigator.of(context).pop(); + }); + if (GetIt.I() + .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() + .mediaItem + .value! + .extras?['show'])), + ShowEpisode.fromMediaItem( + GetIt.I().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 { AudioPlayerHandler audioHandler = GetIt.I(); - 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 { 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 with WidgetsBindingObserver { _imageList = _getImageList(audioHandler.queue.value); - _currentItemAndQueueSub = Rx.combineLatest2, void>( + _currentItemAndQueueSub = + Rx.combineLatest2, void>( audioHandler.mediaItem, audioHandler.queue, (mediaItem, queue) { @@ -698,7 +901,9 @@ class _BigAlbumArtState extends State with WidgetsBindingObserver { } List _getImageList(List queue) { - return queue.map((item) => ZoomableImage(url: item.artUri?.toString() ?? '')).toList(); + return queue + .map((item) => ZoomableImage(url: item.artUri?.toString() ?? '')) + .toList(); } bool _didQueueChange(List newQueue) { @@ -718,7 +923,8 @@ class _BigAlbumArtState extends State 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().queueSource?.text ?? '') - : 'Playing from:'.i18n + ' ' + (GetIt.I().queueSource?.text ?? ''), + : 'Playing from:'.i18n + + ' ' + + (GetIt.I().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 { 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 { return StreamBuilder( stream: Stream.periodic(const Duration(milliseconds: 250)), builder: (BuildContext context, AsyncSnapshot snapshot) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _timeString(position), - style: TextStyle(fontSize: ScreenUtil().setSp(widget.relativeTextSize)), + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 0.0, horizontal: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 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 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 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 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); }, ), ) diff --git a/lib/ui/search.dart b/lib/ui/search.dart index bf57db1..a206e02 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -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 { actions: [ IconButton( icon: Icon( - Icons.file_download, + DeezerIcons.download, semanticLabel: 'Download'.i18n, ), onPressed: () { @@ -171,7 +173,7 @@ class _SearchScreenState extends State { ), IconButton( icon: Icon( - Icons.settings, + DeezerIcons.settings, semanticLabel: 'Settings'.i18n, ), onPressed: () { @@ -218,6 +220,7 @@ class _SearchScreenState extends State { 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 { _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); diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index b16297b..83a0692 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -52,70 +52,111 @@ class _SettingsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Settings'.i18n), - body: ListView( - children: [ - ListTile( - title: Text('General'.i18n), - leading: const LeadingIcon(Icons.settings, color: Color(0xffeca704)), - onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const GeneralSettings())), - ), - ListTile( - title: Text('Download Settings'.i18n), - leading: const LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)), - onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadsSettings())), - ), - ListTile( - title: Text('Appearance'.i18n), - leading: const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const AppearanceSettings())), - ), - ListTile( - title: Text('Quality'.i18n), - leading: const LeadingIcon(Icons.high_quality, color: Color(0xff384697)), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const QualitySettings())), - ), - ListTile( - title: Text('Deezer'.i18n), - leading: const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const DeezerSettings())), - ), - //Language select - ListTile( - title: Text('Language'.i18n), - leading: const LeadingIcon(Icons.language, color: Color(0xff009a85)), - onTap: () { - showDialog( - context: context, - builder: (context) => SimpleDialog( - title: Text('Select language'.i18n), - children: List.generate(languages.length, (int i) { - Language l = languages[i]; - return ListTile( - title: Text(l.name), - subtitle: Text('${l.locale}-${l.country}'), - onTap: () async { - I18n.of(customNavigatorKey.currentContext!).locale = Locale(l.locale, l.country); - setState(() => settings.language = '${l.locale}_${l.country}'); - await settings.save(); - // Close the SimpleDialog - if (context.mounted) Navigator.of(context).pop(); - }, - ); - }))); - }, - ), - ListTile( - title: Text('Updates'.i18n), - leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const UpdaterScreen())), - ), - ListTile( - title: Text('About'.i18n), - leading: const LeadingIcon(Icons.info, color: Colors.grey), - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CreditsScreen())), - ), - ], - ), + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView(children: [ + ListTile( + title: Text('General'.i18n), + leading: + const LeadingIcon(Icons.settings, color: Color(0xffeca704)), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const GeneralSettings())), + ), + ListTile( + title: Text('Download Settings'.i18n), + leading: const LeadingIcon(Icons.cloud_download, + color: Color(0xffbe3266)), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const DownloadsSettings())), + ), + ListTile( + title: Text('Appearance'.i18n), + leading: + const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AppearanceSettings())), + ), + ListTile( + title: Text('Quality'.i18n), + leading: const LeadingIcon(Icons.high_quality, + color: Color(0xff384697)), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const QualitySettings())), + ), + ListTile( + title: Text('Deezer'.i18n), + leading: + const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DeezerSettings())), + ), + //Language select + ListTile( + title: Text('Language'.i18n), + leading: + const LeadingIcon(Icons.language, color: Color(0xff009a85)), + onTap: () { + showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text('Select language'.i18n), + children: List.generate(languages.length, (int i) { + Language l = languages[i]; + return ListTile( + title: Text(l.name), + subtitle: Text('${l.locale}-${l.country}'), + onTap: () async { + I18n.of(customNavigatorKey.currentContext!) + .locale = Locale(l.locale, l.country); + setState(() => settings.language = + '${l.locale}_${l.country}'); + await settings.save(); + // Close the SimpleDialog + if (context.mounted) Navigator.of(context).pop(); + }, + ); + }))); + }, + ), + ListTile( + title: Text('Updates'.i18n), + leading: + const LeadingIcon(Icons.update, color: Color(0xff2ba766)), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const UpdaterScreen())), + ), + ListTile( + title: Text('About'.i18n), + leading: const LeadingIcon(Icons.info, color: Colors.grey), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CreditsScreen())), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.fromLTRB(0, 12, 0, 12), + child: Text( + 'App by @DjDoubleD, mod by @PetitPrince', + style: TextStyle(color: Settings.secondaryText), + textAlign: TextAlign.center, + ), + ) + ], + ) + ])), ); } } @@ -134,202 +175,202 @@ class _AppearanceSettingsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Appearance'.i18n), - body: ListView( - children: [ - ListTile( - title: Text('Theme'.i18n), - subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'), - leading: const Icon(Icons.color_lens), - onTap: () { - showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: Text('Select theme'.i18n), - children: [ - SimpleDialogOption( - child: Text('Light'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Light); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - SimpleDialogOption( - child: Text('Dark'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Dark); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - SimpleDialogOption( - child: Text('Black (AMOLED)'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Black); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - SimpleDialogOption( - child: Text('Deezer (Dark)'.i18n), - onPressed: () { - setState(() => settings.theme = Themes.Deezer); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - ], - ); - }); - }, - ), - ListTile( - title: Text('Use system theme'.i18n), + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView( + children: [ + ListTile( + title: Text('Theme'.i18n), + subtitle: Text('Currently'.i18n + + ': ${settings.theme.toString().split('.').last}'), + leading: const Icon(Icons.color_lens), + onTap: () { + showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: Text('Select theme'.i18n), + children: [ + SimpleDialogOption( + child: Text('Light'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Light); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + SimpleDialogOption( + child: Text('Dark'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Dark); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + SimpleDialogOption( + child: Text('Black (AMOLED)'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Black); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + SimpleDialogOption( + child: Text('Deezer (Dark)'.i18n), + onPressed: () { + setState(() => settings.theme = Themes.Deezer); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + ], + ); + }); + }, + ), + ListTile( + title: Text('Use system theme'.i18n), + trailing: Switch( + value: settings.useSystemTheme, + onChanged: (bool v) async { + setState(() { + settings.useSystemTheme = v; + }); + updateTheme(); + await settings.save(); + }, + ), + leading: const Icon(Icons.android)), + ListTile( + title: Text('Font'.i18n), + leading: const Icon(Icons.font_download), + subtitle: Text(settings.font), + onTap: () { + showDialog( + context: context, + builder: (context) => + FontSelector(() => Navigator.of(context).pop())); + }, + ), + ListTile( + title: Text('Player gradient background'.i18n), + leading: const Icon(Icons.colorize), trailing: Switch( - value: settings.useSystemTheme, + value: settings.colorGradientBackground, onChanged: (bool v) async { - setState(() { - settings.useSystemTheme = v; - }); - updateTheme(); + setState(() => settings.colorGradientBackground = v); await settings.save(); }, ), - leading: const Icon(Icons.android)), - ListTile( - title: Text('Font'.i18n), - leading: const Icon(Icons.font_download), - subtitle: Text(settings.font), - onTap: () { - showDialog(context: context, builder: (context) => FontSelector(() => Navigator.of(context).pop())); - }, - ), - ListTile( - title: Text('Player gradient background'.i18n), - leading: const Icon(Icons.colorize), - trailing: Switch( - value: settings.colorGradientBackground, - onChanged: (bool v) async { - setState(() => settings.colorGradientBackground = v); - await settings.save(); - }, ), - ), - ListTile( - title: Text('Blur player background'.i18n), - subtitle: Text('Might have impact on performance'.i18n), - leading: const Icon(Icons.blur_on), - trailing: Switch( - value: settings.blurPlayerBackground, - onChanged: (bool v) async { - setState(() => settings.blurPlayerBackground = v); - await settings.save(); - }, - ), - ), - ListTile( - title: Text('Visualizer'.i18n), - subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n), - leading: const Icon(Icons.equalizer), - trailing: Switch( - value: settings.lyricsVisualizer, - onChanged: (bool v) async { - if (await Permission.microphone.request().isGranted) { - setState(() => settings.lyricsVisualizer = v); + ListTile( + title: Text('Blur player background'.i18n), + subtitle: Text('Might have impact on performance'.i18n), + leading: const Icon(Icons.blur_on), + trailing: Switch( + value: settings.blurPlayerBackground, + onChanged: (bool v) async { + setState(() => settings.blurPlayerBackground = v); await settings.save(); - return; - } + }, + ), + ), + ListTile( + title: Text('Visualizer'.i18n), + subtitle: Text( + 'Show visualizers on lyrics page. WARNING: Requires microphone permission!' + .i18n), + leading: const Icon(Icons.equalizer), + trailing: Switch( + value: settings.lyricsVisualizer, + onChanged: (bool v) async { + if (await Permission.microphone.request().isGranted) { + setState(() => settings.lyricsVisualizer = v); + await settings.save(); + return; + } + }, + ), + enabled: false, + ), + ListTile( + title: Text('Primary color'.i18n), + leading: const Icon(Icons.format_paint), + subtitle: Text( + 'Selected color'.i18n, + style: TextStyle(color: settings.primaryColor), + ), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Primary colors'.i18n), + content: SizedBox( + height: 240, + child: MaterialColorPicker( + colors: [ + ...Colors.primaries, + //Logo colors + _swatch(0xFFFF3386), + _swatch(0xff4b2e7e), + _swatch(0xff384697), + _swatch(0xff0880b5), + _swatch(0xff009a85), + _swatch(0xFFA238FF), + ], + allowShades: false, + selectedColor: settings.primaryColor, + onMainColorChange: (ColorSwatch? color) { + setState(() { + settings.primaryColor = color!; + }); + settings.save(); + updateTheme(); + Navigator.of(context).pop(); + }, + ), + ), + ); + }); }, ), - enabled: false, - ), - ListTile( - title: Text('Primary color'.i18n), - leading: const Icon(Icons.format_paint), - subtitle: Text( - 'Selected color'.i18n, - style: TextStyle(color: settings.primaryColor), - ), - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Primary color'.i18n), - content: SizedBox( - height: 240, - child: MaterialColorPicker( - colors: [ - ...Colors.primaries, - //Logo colors - _swatch(0xffeca704), - _swatch(0xffbe3266), - _swatch(0xff4b2e7e), - _swatch(0xff384697), - _swatch(0xff0880b5), - _swatch(0xff009a85), - _swatch(0xff2ba766) - ], - allowShades: false, - selectedColor: settings.primaryColor, - onMainColorChange: (ColorSwatch? color) { - setState(() { - settings.primaryColor = color!; - }); - settings.save(); - updateTheme(); - Navigator.of(context).pop(); - }, - ), - ), - ); - }); - }, - ), - ListTile( - title: Text('Use album art primary color'.i18n), - subtitle: Text('Warning: might be buggy'.i18n), - leading: const Icon(Icons.invert_colors), - trailing: Switch( - value: settings.useArtColor, - onChanged: (v) => setState(() => settings.updateUseArtColor(v)), - ), - ), - //Display mode - ListTile( - leading: const Icon(Icons.screen_lock_portrait), - title: Text('Change display mode'.i18n), - subtitle: Text('Enable high refresh rates'.i18n), - onTap: () async { - List modes = await FlutterDisplayMode.supported; - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: Text('Display mode'.i18n), - children: List.generate( - modes.length, - (i) => SimpleDialogOption( - child: Text(modes[i].toString()), - onPressed: () async { - settings.displayMode = i; - await settings.save(); - await FlutterDisplayMode.setPreferredMode(modes[i]); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ))); - }); - }, - ) - ], + //Display mode + ListTile( + leading: const Icon(Icons.screen_lock_portrait), + title: Text('Change display mode'.i18n), + subtitle: Text('Enable high refresh rates'.i18n), + onTap: () async { + List modes = await FlutterDisplayMode.supported; + if (!context.mounted) return; + showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: Text('Display mode'.i18n), + children: List.generate( + modes.length, + (i) => SimpleDialogOption( + child: Text(modes[i].toString()), + onPressed: () async { + settings.displayMode = i; + await settings.save(); + await FlutterDisplayMode.setPreferredMode( + modes[i]); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ))); + }); + }, + ) + ], + ), ), ); } @@ -347,7 +388,9 @@ class FontSelector extends StatefulWidget { class _FontSelectorState extends State { String query = ''; List get fonts { - return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList(); + return settings.fonts + .where((f) => f.toLowerCase().contains(query)) + .toList(); } //Font selected @@ -415,34 +458,41 @@ class QualitySettings extends StatefulWidget { class _QualitySettingsState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: FreezerAppBar('Quality'.i18n), - body: ListView( - children: [ - ListTile( - title: Text('Mobile streaming'.i18n), - leading: const LeadingIcon(Icons.network_cell, color: Color(0xff384697)), - ), - const QualityPicker('mobile'), - const FreezerDivider(), - ListTile( - title: Text('Wifi streaming'.i18n), - leading: const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)), - ), - const QualityPicker('wifi'), - const FreezerDivider(), - ListTile( - title: Text('Offline'.i18n), - leading: const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)), - ), - const QualityPicker('offline'), - const FreezerDivider(), - ListTile( - title: Text('External downloads'.i18n), - leading: const LeadingIcon(Icons.file_download, color: Color(0xff2ba766)), - ), - const QualityPicker('download'), - ], + return Container( + padding: EdgeInsets.only(bottom: 80), + child: Scaffold( + appBar: FreezerAppBar('Quality'.i18n), + body: ListView( + children: [ + ListTile( + title: Text('Mobile streaming'.i18n), + leading: const LeadingIcon(Icons.network_cell, + color: Color(0xff384697)), + ), + const QualityPicker('mobile'), + const FreezerDivider(), + ListTile( + title: Text('Wifi streaming'.i18n), + leading: const LeadingIcon(Icons.network_wifi, + color: Color(0xff0880b5)), + ), + const QualityPicker('wifi'), + const FreezerDivider(), + ListTile( + title: Text('Offline'.i18n), + leading: const LeadingIcon(Icons.offline_pin, + color: Color(0xff009a85)), + ), + const QualityPicker('offline'), + const FreezerDivider(), + ListTile( + title: Text('External downloads'.i18n), + leading: const LeadingIcon(Icons.file_download, + color: Color(0xff2ba766)), + ), + const QualityPicker('download'), + ], + ), ), ); } @@ -604,76 +654,84 @@ class _DeezerSettingsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Deezer'.i18n), - body: ListView( - children: [ - ListTile( - title: Text('Content language'.i18n), - subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'), - leading: const Icon(Icons.language), - onTap: () { - showDialog( - context: context, - builder: (context) => SimpleDialog( - title: Text('Select language'.i18n), - children: List.generate( - ContentLanguage.all.length, - (i) => ListTile( - title: Text(ContentLanguage.all[i].name), - subtitle: Text(ContentLanguage.all[i].code), - onTap: () async { - setState(() => settings.deezerLanguage = ContentLanguage.all[i].code); - await settings.save(); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - )), - )); - }, - ), - ListTile( - title: Text('Content country'.i18n), - subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'), - leading: const Icon(Icons.vpn_lock), - onTap: () { - showDialog( - context: context, - builder: (context) => CountryPickerDialog( - title: Text('Select country'.i18n), - titlePadding: const EdgeInsets.all(8.0), - isSearchable: true, - itemBuilder: (country) => Row( - children: [ - CountryPickerUtils.getDefaultFlagImage(country), - const SizedBox( - width: 8.0, - ), - Expanded( - child: Text( - '${country.name} (${country.isoCode})', - )) - ], - ), - onValuePicked: (Country country) { - setState(() => settings.deezerCountry = country.isoCode ?? 'us'); - settings.save(); - }, - )); - }, - ), - ListTile( - title: Text('Log tracks'.i18n), - subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n), - trailing: Switch( - value: settings.logListen, - onChanged: (bool v) { - setState(() => settings.logListen = v); - settings.save(); + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView( + children: [ + ListTile( + title: Text('Content language'.i18n), + subtitle: Text('Not app language, used in headers. Now'.i18n + + ': ${settings.deezerLanguage}'), + leading: const Icon(Icons.language), + onTap: () { + showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text('Select language'.i18n), + children: List.generate( + ContentLanguage.all.length, + (i) => ListTile( + title: Text(ContentLanguage.all[i].name), + subtitle: Text(ContentLanguage.all[i].code), + onTap: () async { + setState(() => settings.deezerLanguage = + ContentLanguage.all[i].code); + await settings.save(); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + )), + )); }, ), - leading: const Icon(Icons.history_toggle_off), - ), - //TODO: Reimplement proxy + ListTile( + title: Text('Content country'.i18n), + subtitle: Text('Country used in headers. Now'.i18n + + ': ${settings.deezerCountry}'), + leading: const Icon(Icons.vpn_lock), + onTap: () { + showDialog( + context: context, + builder: (context) => CountryPickerDialog( + title: Text('Select country'.i18n), + titlePadding: const EdgeInsets.all(8.0), + isSearchable: true, + itemBuilder: (country) => Row( + children: [ + CountryPickerUtils.getDefaultFlagImage(country), + const SizedBox( + width: 8.0, + ), + Expanded( + child: Text( + '${country.name} (${country.isoCode})', + )) + ], + ), + onValuePicked: (Country country) { + setState(() => settings.deezerCountry = + country.isoCode ?? 'us'); + settings.save(); + }, + )); + }, + ), + ListTile( + title: Text('Log tracks'.i18n), + subtitle: Text( + 'Send track listen logs to Deezer, enable it for features like Flow to work properly' + .i18n), + trailing: Switch( + value: settings.logListen, + onChanged: (bool v) { + setState(() => settings.logListen = v); + settings.save(); + }, + ), + leading: const Icon(Icons.history_toggle_off), + ), + //TODO: Reimplement proxy // ListTile( // title: Text('Proxy'.i18n), // leading: Icon(Icons.vpn_key), @@ -722,7 +780,8 @@ class _DeezerSettingsState extends State { // ); // }, // ) - ], + ], + ), ), ); } @@ -764,7 +823,8 @@ class _FilenameTemplateDialogState extends State { Text( 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' + - "If you want to use custom directory naming - use '/' as directory separator.".i18n, + "If you want to use custom directory naming - use '/' as directory separator." + .i18n, style: const TextStyle( fontSize: 12.0, ), @@ -779,7 +839,8 @@ class _FilenameTemplateDialogState extends State { TextButton( child: Text('Reset'.i18n), onPressed: () { - _controller.value = _controller.value.copyWith(text: '%artist% - %title%'); + _controller.value = + _controller.value.copyWith(text: '%artist% - %title%'); _new = '%artist% - %title%'; }, ), @@ -808,259 +869,276 @@ class DownloadsSettings extends StatefulWidget { class _DownloadsSettingsState extends State { double _downloadThreads = settings.downloadThreads.toDouble(); - final TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator); + final TextEditingController _artistSeparatorController = + TextEditingController(text: settings.artistSeparator); @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Download Settings'.i18n), - body: ListView( - children: [ - ListTile( - title: Text('Download path'.i18n), - leading: const Icon(Icons.folder), - subtitle: Text(settings.downloadPath ?? 'Not set'.i18n), - onTap: () async { - //Check permissions - //if (!(await Permission.storage.request().isGranted)) return; - if (await FileUtils.checkStoragePermission()) { - //Navigate - if (context.mounted) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => DirectoryPicker( - settings.downloadPath ?? '', - onSelect: (String p) async { - setState(() => settings.downloadPath = p); - await settings.save(); - }, - ))); + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView( + children: [ + ListTile( + title: Text('Download path'.i18n), + leading: const Icon(Icons.folder), + subtitle: Text(settings.downloadPath ?? 'Not set'.i18n), + onTap: () async { + //Check permissions + //if (!(await Permission.storage.request().isGranted)) return; + if (await FileUtils.checkStoragePermission()) { + //Navigate + if (context.mounted) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => DirectoryPicker( + settings.downloadPath ?? '', + onSelect: (String p) async { + setState(() => settings.downloadPath = p); + await settings.save(); + }, + ))); + } + } else { + Fluttertoast.showToast( + msg: 'Storage permission denied!'.i18n, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM); + return; } - } else { - Fluttertoast.showToast( - msg: 'Storage permission denied!'.i18n, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM); - return; - } - }, - ), - ListTile( - title: Text('Downloads naming'.i18n), - subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'), - leading: const Icon(Icons.text_format), - onTap: () { - showDialog( - context: context, - builder: (context) { - return FilenameTemplateDialog(settings.downloadFilename, (f) async { - setState(() => settings.downloadFilename = f); - await settings.save(); - }); - }); - }, - ), - ListTile( - title: Text('Singleton naming'.i18n), - subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'), - leading: const Icon(Icons.text_format), - onTap: () { - showDialog( - context: context, - builder: (context) { - return FilenameTemplateDialog(settings.singletonFilename, (f) async { - setState(() => settings.singletonFilename = f); - await settings.save(); - }); - }); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - 'Download threads'.i18n + ': ${_downloadThreads.round().toString()}', - style: const TextStyle(fontSize: 16.0), - ), - ), - Slider( - min: 1, - max: 16, - divisions: 15, - value: _downloadThreads, - label: _downloadThreads.round().toString(), - onChanged: (double v) => setState(() => _downloadThreads = v), - onChangeEnd: (double val) async { - _downloadThreads = val; - setState(() { - settings.downloadThreads = _downloadThreads.round(); - _downloadThreads = settings.downloadThreads.toDouble(); - }); - await settings.save(); - - //Prevent null - if (val > 8 && cache.threadsWarning != true && context.mounted) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Warning'.i18n), - content: Text( - 'Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n), - actions: [ - TextButton( - child: Text('Dismiss'.i18n), - onPressed: () => Navigator.of(context).pop(), - ) - ], - ); - }); - - cache.threadsWarning = true; - await cache.save(); - } - }), - const FreezerDivider(), - ListTile( - title: Text('Tags'.i18n), - leading: const Icon(Icons.label), - onTap: () => - Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TagSelectionScreen())), - ), - ListTile( - title: Text('Create folders for artist'.i18n), - trailing: Switch( - value: settings.artistFolder, - onChanged: (v) { - setState(() => settings.artistFolder = v); - settings.save(); }, ), - leading: const Icon(Icons.folder), - ), - ListTile( - title: Text('Create folders for albums'.i18n), - trailing: Switch( - value: settings.albumFolder, - onChanged: (v) { - setState(() => settings.albumFolder = v); - settings.save(); - }, - ), - leading: const Icon(Icons.folder)), - ListTile( - title: Text('Create folder for playlist'.i18n), - trailing: Switch( - value: settings.playlistFolder, - onChanged: (v) { - setState(() => settings.playlistFolder = v); - settings.save(); - }, - ), - leading: const Icon(Icons.folder)), - const FreezerDivider(), - ListTile( - title: Text('Separate albums by discs'.i18n), - trailing: Switch( - value: settings.albumDiscFolder, - onChanged: (v) { - setState(() => settings.albumDiscFolder = v); - settings.save(); - }, - ), - leading: const Icon(Icons.album)), - ListTile( - title: Text('Overwrite already downloaded files'.i18n), - trailing: Switch( - value: settings.overwriteDownload, - onChanged: (v) { - setState(() => settings.overwriteDownload = v); - settings.save(); - }, - ), - leading: const Icon(Icons.delete)), - ListTile( - title: Text('Download .LRC lyrics'.i18n), - trailing: Switch( - value: settings.downloadLyrics, - onChanged: (v) { - setState(() => settings.downloadLyrics = v); - settings.save(); - }, - ), - leading: const Icon(Icons.subtitles)), - const FreezerDivider(), - ListTile( - title: Text('Save cover file for every track'.i18n), - trailing: Switch( - value: settings.trackCover, - onChanged: (v) { - setState(() => settings.trackCover = v); - settings.save(); - }, - ), - leading: const Icon(Icons.image)), - ListTile( - title: Text('Save album cover'.i18n), - trailing: Switch( - value: settings.albumCover, - onChanged: (v) { - setState(() => settings.albumCover = v); - settings.save(); - }, - ), - leading: const Icon(Icons.image)), - ListTile( - title: Text('Album cover resolution'.i18n), - subtitle: Text("WARNING: Resolutions above 1200 aren't officially supported".i18n), - leading: const Icon(Icons.image), - trailing: SizedBox( - width: 75.0, - child: DropdownButton( - value: settings.albumArtResolution, - items: [400, 800, 1000, 1200, 1400, 1600, 1800] - .map>((int i) => DropdownMenuItem( - value: i, - child: Text(i.toString()), - )) - .toList(), - onChanged: (int? n) async { - setState(() { - settings.albumArtResolution = n ?? 400; + ListTile( + title: Text('Downloads naming'.i18n), + subtitle: + Text('Currently'.i18n + ': ${settings.downloadFilename}'), + leading: const Icon(Icons.text_format), + onTap: () { + showDialog( + context: context, + builder: (context) { + return FilenameTemplateDialog(settings.downloadFilename, + (f) async { + setState(() => settings.downloadFilename = f); + await settings.save(); }); - await settings.save(); - }, - ))), - ListTile( - title: Text('Create .nomedia files'.i18n), - subtitle: Text('To prevent gallery being filled with album art'.i18n), - trailing: Switch( - value: settings.nomediaFiles, - onChanged: (v) { - setState(() => settings.nomediaFiles = v); - settings.save(); - }, - ), - leading: const Icon(Icons.insert_drive_file)), - ListTile( - title: Text('Artist separator'.i18n), - leading: const Icon(WebSymbols.tag), - trailing: SizedBox( - width: 75.0, - child: TextField( - controller: _artistSeparatorController, - onChanged: (s) async { - settings.artistSeparator = s; - await settings.save(); - }, + }); + }, + ), + ListTile( + title: Text('Singleton naming'.i18n), + subtitle: + Text('Currently'.i18n + ': ${settings.singletonFilename}'), + leading: const Icon(Icons.text_format), + onTap: () { + showDialog( + context: context, + builder: (context) { + return FilenameTemplateDialog(settings.singletonFilename, + (f) async { + setState(() => settings.singletonFilename = f); + await settings.save(); + }); + }); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'Download threads'.i18n + + ': ${_downloadThreads.round().toString()}', + style: const TextStyle(fontSize: 16.0), ), ), - ), - const FreezerDivider(), - ListTile( - title: Text('Download Log'.i18n), - leading: const Icon(Icons.sticky_note_2), - onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const DownloadLogViewer())), - ) - ], + Slider( + min: 1, + max: 16, + divisions: 15, + value: _downloadThreads, + label: _downloadThreads.round().toString(), + onChanged: (double v) => setState(() => _downloadThreads = v), + onChangeEnd: (double val) async { + _downloadThreads = val; + setState(() { + settings.downloadThreads = _downloadThreads.round(); + _downloadThreads = settings.downloadThreads.toDouble(); + }); + await settings.save(); + + //Prevent null + if (val > 8 && + cache.threadsWarning != true && + context.mounted) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Warning'.i18n), + content: Text( + 'Using too many concurrent downloads on older/weaker devices might cause crashes!' + .i18n), + actions: [ + TextButton( + child: Text('Dismiss'.i18n), + onPressed: () => Navigator.of(context).pop(), + ) + ], + ); + }); + + cache.threadsWarning = true; + await cache.save(); + } + }), + const FreezerDivider(), + ListTile( + title: Text('Tags'.i18n), + leading: const Icon(Icons.label), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const TagSelectionScreen())), + ), + ListTile( + title: Text('Create folders for artist'.i18n), + trailing: Switch( + value: settings.artistFolder, + onChanged: (v) { + setState(() => settings.artistFolder = v); + settings.save(); + }, + ), + leading: const Icon(Icons.folder), + ), + ListTile( + title: Text('Create folders for albums'.i18n), + trailing: Switch( + value: settings.albumFolder, + onChanged: (v) { + setState(() => settings.albumFolder = v); + settings.save(); + }, + ), + leading: const Icon(Icons.folder)), + ListTile( + title: Text('Create folder for playlist'.i18n), + trailing: Switch( + value: settings.playlistFolder, + onChanged: (v) { + setState(() => settings.playlistFolder = v); + settings.save(); + }, + ), + leading: const Icon(Icons.folder)), + const FreezerDivider(), + ListTile( + title: Text('Separate albums by discs'.i18n), + trailing: Switch( + value: settings.albumDiscFolder, + onChanged: (v) { + setState(() => settings.albumDiscFolder = v); + settings.save(); + }, + ), + leading: const Icon(Icons.album)), + ListTile( + title: Text('Overwrite already downloaded files'.i18n), + trailing: Switch( + value: settings.overwriteDownload, + onChanged: (v) { + setState(() => settings.overwriteDownload = v); + settings.save(); + }, + ), + leading: const Icon(Icons.delete)), + ListTile( + title: Text('Download .LRC lyrics'.i18n), + trailing: Switch( + value: settings.downloadLyrics, + onChanged: (v) { + setState(() => settings.downloadLyrics = v); + settings.save(); + }, + ), + leading: const Icon(Icons.subtitles)), + const FreezerDivider(), + ListTile( + title: Text('Save cover file for every track'.i18n), + trailing: Switch( + value: settings.trackCover, + onChanged: (v) { + setState(() => settings.trackCover = v); + settings.save(); + }, + ), + leading: const Icon(Icons.image)), + ListTile( + title: Text('Save album cover'.i18n), + trailing: Switch( + value: settings.albumCover, + onChanged: (v) { + setState(() => settings.albumCover = v); + settings.save(); + }, + ), + leading: const Icon(Icons.image)), + ListTile( + title: Text('Album cover resolution'.i18n), + subtitle: Text( + "WARNING: Resolutions above 1200 aren't officially supported" + .i18n), + leading: const Icon(Icons.image), + trailing: SizedBox( + width: 75.0, + child: DropdownButton( + value: settings.albumArtResolution, + items: [400, 800, 1000, 1200, 1400, 1600, 1800] + .map>( + (int i) => DropdownMenuItem( + value: i, + child: Text(i.toString()), + )) + .toList(), + onChanged: (int? n) async { + setState(() { + settings.albumArtResolution = n ?? 400; + }); + await settings.save(); + }, + ))), + ListTile( + title: Text('Create .nomedia files'.i18n), + subtitle: + Text('To prevent gallery being filled with album art'.i18n), + trailing: Switch( + value: settings.nomediaFiles, + onChanged: (v) { + setState(() => settings.nomediaFiles = v); + settings.save(); + }, + ), + leading: const Icon(Icons.insert_drive_file)), + ListTile( + title: Text('Artist separator'.i18n), + leading: const Icon(WebSymbols.tag), + trailing: SizedBox( + width: 75.0, + child: TextField( + controller: _artistSeparatorController, + onChanged: (s) async { + settings.artistSeparator = s; + await settings.save(); + }, + ), + ), + ), + const FreezerDivider(), + ListTile( + title: Text('Download Log'.i18n), + leading: const Icon(Icons.sticky_note_2), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const DownloadLogViewer())), + ) + ], + ), ), ); } @@ -1103,25 +1181,28 @@ class _TagSelectionScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Tags'.i18n), - body: ListView( - children: List.generate( - tags.length, - (i) => ListTile( - title: Text(tags[i].title), - leading: Switch( - value: settings.tags.contains(tags[i].value), - onChanged: (v) async { - //Update - if (v) { - settings.tags.add(tags[i].value); - } else { - settings.tags.remove(tags[i].value); - } - setState(() {}); - await settings.save(); - }, - ), - )), + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView( + children: List.generate( + tags.length, + (i) => ListTile( + title: Text(tags[i].title), + leading: Switch( + value: settings.tags.contains(tags[i].value), + onChanged: (v) async { + //Update + if (v) { + settings.tags.add(tags[i].value); + } else { + settings.tags.remove(tags[i].value); + } + setState(() {}); + await settings.save(); + }, + ), + )), + ), ), ); } @@ -1139,142 +1220,153 @@ class _GeneralSettingsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('General'.i18n), - body: ListView( - children: [ - ListTile( - title: Text('Offline mode'.i18n), - subtitle: Text('Will be overwritten on start.'.i18n), - trailing: Switch( - value: settings.offlineMode, - onChanged: (bool v) { - if (v) { - setState(() => settings.offlineMode = true); - return; - } - showDialog( - context: context, - builder: (context) { - deezerAPI.authorize().then((v) async { - if (v) { - setState(() => settings.offlineMode = false); - } else { - Fluttertoast.showToast( - msg: 'Error logging in, check your internet connections.'.i18n, - gravity: ToastGravity.BOTTOM, - toastLength: Toast.LENGTH_SHORT); - } - if (context.mounted) Navigator.of(context).pop(); + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView( + children: [ + ListTile( + title: Text('Offline mode'.i18n), + subtitle: Text('Will be overwritten on start.'.i18n), + trailing: Switch( + value: settings.offlineMode, + onChanged: (bool v) { + if (v) { + setState(() => settings.offlineMode = true); + return; + } + showDialog( + context: context, + builder: (context) { + deezerAPI.authorize().then((v) async { + if (v) { + setState(() => settings.offlineMode = false); + } else { + Fluttertoast.showToast( + msg: + 'Error logging in, check your internet connections.' + .i18n, + gravity: ToastGravity.BOTTOM, + toastLength: Toast.LENGTH_SHORT); + } + if (context.mounted) Navigator.of(context).pop(); + }); + return AlertDialog( + title: Text('Logging in...'.i18n), + content: const Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + )); }); - return AlertDialog( - title: Text('Logging in...'.i18n), - content: const Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator()], - )); - }); - }, - ), - leading: const Icon(Icons.lock), - ), - ListTile( - title: Text('Copy ARL'.i18n), - subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n), - leading: const Icon(Icons.lock), - onTap: () async { - await FlutterClipboard.copy(settings.arl ?? ''); - await Fluttertoast.showToast( - msg: 'Copied'.i18n, - ); - }, - ), - ListTile( - title: Text('Enable equalizer'.i18n), - subtitle: Text('Might enable some equalizer apps to work. Requires restart of ReFreezer'.i18n), - leading: const Icon(Icons.equalizer), - trailing: Switch( - value: settings.enableEqualizer, - onChanged: (v) async { - setState(() => settings.enableEqualizer = v); - settings.save(); - }, - ), - ), - ListTile( - title: Text('LastFM'.i18n), - subtitle: Text((settings.lastFMUsername != null) ? 'Log out'.i18n : 'Login to enable scrobbling.'.i18n), - leading: const Icon(FontAwesome5.lastfm), - onTap: () async { - if (settings.lastFMUsername != null) { - //Log out - settings.lastFMUsername = null; - settings.lastFMPassword = null; - await settings.save(); - await GetIt.I().disableLastFM(); - //await GetIt.I().customAction('disableLastFM', Map()); - setState(() {}); - Fluttertoast.showToast(msg: 'Logged out!'.i18n); - return; - } else { - showDialog( - context: context, - builder: (context) => const LastFMLogin(), - ).then((_) { - setState(() {}); - }); - } - }, - //enabled: false, - ), - ListTile( - title: Text('Ignore interruptions'.i18n), - subtitle: Text('Requires app restart to apply!'.i18n), - leading: const Icon(Icons.not_interested), - trailing: Switch( - value: settings.ignoreInterruptions, - onChanged: (bool v) async { - setState(() => settings.ignoreInterruptions = v); - await settings.save(); - }, - ), - ), - ListTile( - title: Text('Application Log'.i18n), - leading: const Icon(Icons.sticky_note_2), - onTap: () => - Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ApplicationLogViewer())), - ), - const FreezerDivider(), - ListTile( - title: Text( - 'Log out'.i18n, - style: const TextStyle(color: Colors.red), + }, ), - leading: const Icon(Icons.exit_to_app), - onTap: () { - showDialog( + leading: const Icon(Icons.lock), + ), + ListTile( + title: Text('Copy ARL'.i18n), + subtitle: + Text('Copy userToken/ARL Cookie for use in other apps.'.i18n), + leading: const Icon(Icons.lock), + onTap: () async { + await FlutterClipboard.copy(settings.arl ?? ''); + await Fluttertoast.showToast( + msg: 'Copied'.i18n, + ); + }, + ), + ListTile( + title: Text('Enable equalizer'.i18n), + subtitle: Text( + 'Might enable some equalizer apps to work. Requires restart of ReFreezer' + .i18n), + leading: const Icon(Icons.equalizer), + trailing: Switch( + value: settings.enableEqualizer, + onChanged: (v) async { + setState(() => settings.enableEqualizer = v); + settings.save(); + }, + ), + ), + ListTile( + title: Text('LastFM'.i18n), + subtitle: Text((settings.lastFMUsername != null) + ? 'Log out'.i18n + : 'Login to enable scrobbling.'.i18n), + leading: const Icon(FontAwesome5.lastfm), + onTap: () async { + if (settings.lastFMUsername != null) { + //Log out + settings.lastFMUsername = null; + settings.lastFMPassword = null; + await settings.save(); + await GetIt.I().disableLastFM(); + //await GetIt.I().customAction('disableLastFM', Map()); + setState(() {}); + Fluttertoast.showToast(msg: 'Logged out!'.i18n); + return; + } else { + showDialog( context: context, - builder: (context) { - return AlertDialog( - title: Text('Log out'.i18n), - // There was no Incompatability, cookies just needed to be cleared... - // content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n), - // content: Text('Restart of app is required to properly log out!'.i18n), - content: Text('Are you sure you want to log out?'.i18n), - actions: [ - TextButton( - child: Text('Cancel'.i18n), - onPressed: () => Navigator.of(context).pop(), - ), - TextButton( - //child: Text('(ARL ONLY) Continue'.i18n), - child: Text('Continue'.i18n), - onPressed: () async { - await logOut(); - if (context.mounted) Navigator.of(context).pop(); - }, - ), - /* TextButton( + builder: (context) => const LastFMLogin(), + ).then((_) { + setState(() {}); + }); + } + }, + //enabled: false, + ), + ListTile( + title: Text('Ignore interruptions'.i18n), + subtitle: Text('Requires app restart to apply!'.i18n), + leading: const Icon(Icons.not_interested), + trailing: Switch( + value: settings.ignoreInterruptions, + onChanged: (bool v) async { + setState(() => settings.ignoreInterruptions = v); + await settings.save(); + }, + ), + ), + ListTile( + title: Text('Application Log'.i18n), + leading: const Icon(Icons.sticky_note_2), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const ApplicationLogViewer())), + ), + const FreezerDivider(), + ListTile( + title: Text( + 'Log out'.i18n, + style: const TextStyle(color: Colors.red), + ), + leading: const Icon(Icons.exit_to_app), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Log out'.i18n), + // There was no Incompatability, cookies just needed to be cleared... + // content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n), + // content: Text('Restart of app is required to properly log out!'.i18n), + content: + Text('Are you sure you want to log out?'.i18n), + actions: [ + TextButton( + child: Text('Cancel'.i18n), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + //child: Text('(ARL ONLY) Continue'.i18n), + child: Text('Continue'.i18n), + onPressed: () async { + await logOut(); + if (context.mounted) + Navigator.of(context).pop(); + }, + ), + /* TextButton( child: Text('Log out & Exit'.i18n), onPressed: () async { try { @@ -1290,11 +1382,12 @@ class _GeneralSettingsState extends State { Restart.restartApp(); }, )*/ - ], - ); - }); - }), - ], + ], + ); + }); + }), + ], + ), ), ); } @@ -1341,7 +1434,10 @@ class _LastFMLoginState extends State { LastFM last; try { last = await LastFM.authenticate( - apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: _username, password: _password); + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: _username, + password: _password); } catch (e) { Logger.root.severe('Error authorizing LastFM: $e'); Fluttertoast.showToast(msg: 'Authorization error!'.i18n); @@ -1365,23 +1461,30 @@ class StorageInfo { final String appFilesDir; final int availableBytes; - StorageInfo({required this.rootDir, required this.appFilesDir, required this.availableBytes}); + StorageInfo( + {required this.rootDir, + required this.appFilesDir, + required this.availableBytes}); } Future> getStorageInfo() async { - final externalDirectories = await ExternalPath.getExternalStorageDirectories(); + final externalDirectories = + await ExternalPath.getExternalStorageDirectories(); List storageInfoList = []; if (externalDirectories.isNotEmpty) { for (var dir in externalDirectories) { - var availableMegaBytes = (await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0; + var availableMegaBytes = + (await DiskSpacePlus.getFreeDiskSpaceForPath(dir)) ?? 0.0; storageInfoList.add( StorageInfo( rootDir: dir, appFilesDir: dir, - availableBytes: availableMegaBytes > 0 ? (availableMegaBytes * 1000000).floor() : 0, + availableBytes: availableMegaBytes > 0 + ? (availableMegaBytes * 1000000).floor() + : 0, ), ); } @@ -1453,14 +1556,17 @@ class _DirectoryPickerState extends State { padding: EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator()], + children: [ + CircularProgressIndicator() + ], ), ); } return Column( mainAxisSize: MainAxisSize.min, children: [ - ...List.generate(snapshot.data?.length ?? 0, (i) { + ...List.generate(snapshot.data?.length ?? 0, + (i) { StorageInfo si = snapshot.data![i]; return ListTile( title: Text(si.rootDir), @@ -1524,7 +1630,9 @@ class _DirectoryPickerState extends State { onTap: () { setState(() { if (_root == _path) { - Fluttertoast.showToast(msg: 'Permission denied'.i18n, gravity: ToastGravity.BOTTOM); + Fluttertoast.showToast( + msg: 'Permission denied'.i18n, + gravity: ToastGravity.BOTTOM); return; } _previous = _path; @@ -1608,21 +1716,41 @@ class _CreditsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('About'.i18n), - body: ListView( - children: [ - const FreezerTitle(), - Text( - _version, - textAlign: TextAlign.center, - style: const TextStyle(fontStyle: FontStyle.italic), - ), - const FreezerDivider(), - const ListTile( - title: Text('DJDoubleD', style: TextStyle(fontWeight: FontWeight.bold)), - subtitle: Text('Developer, tester, new icon & logo, some translations, ...'), - ), - const FreezerDivider(), - /*ListTile( + body: Container( + padding: EdgeInsets.only(bottom: 80), + child: ListView( + children: [ + const FreezerTitle(), + Text( + _version + '-MOD', + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + const FreezerDivider(), + ListTile( + title: Text('DJDoubleD'.i18n), + subtitle: Text( + 'Developer, tester, new icon & logo, some translations, ...' + .i18n), + leading: + Image.asset('assets/DJDoubleD.jpg', width: 36, height: 36), + onTap: () { + launchUrlString('https://github.com/DJDoubleD'); + }, + ), + const FreezerDivider(), + ListTile( + title: Text('PetitPrince'.i18n), + subtitle: Text( + 'Developer, tester, new icon & logo, style mod, ...'.i18n), + leading: + Image.asset('assets/PetitPrince.png', width: 36, height: 36), + onTap: () { + launchUrlString('https://github.com/PetitPrinc3'); + }, + ), + const FreezerDivider(), + /*ListTile( title: Text('Telegram Channel'.i18n), subtitle: Text('To get latest releases'.i18n), leading: const Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0), @@ -1646,148 +1774,159 @@ class _CreditsScreenState extends State { launchUrlString('https://discord.gg/qwJpa3r4dQ'); }, ),*/ - ListTile( - title: Text('Repository'.i18n), - subtitle: Text('Source code, report issues there.'.i18n), - leading: const Icon(Icons.code, color: Colors.green, size: 36.0), - onTap: () { - launchUrlString('https://github.com/DJDoubleD/ReFreezer'); - }, - ), - ListTile( - title: Text('Crowdin'.i18n), - subtitle: Text('Help translating this app on Crowdin!'.i18n), - leading: const Icon(ReFreezerIcons.crowdin, color: Color(0xffbdc1c6), size: 36.0), - onTap: () { - launchUrlString('https://crowdin.com/project/refreezer'); - }, - ), - ListTile( - isThreeLine: true, - title: Text('Donate'.i18n), - subtitle: Text('You should rather support your favorite artists, instead of this app!'.i18n), - leading: const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0), - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Donate'.i18n), - content: Text('No really, go support your favorite artists instead ;)'.i18n), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () { - if (context.mounted) Navigator.of(context).pop(); - }, - ) - ], - ); - }); - // launchUrlString('https://paypal.me/exttex'); - }, - ), - const FreezerDivider(), - ...List.generate( - translators.length, - (i) => ListTile( - title: Text(translators[i][0]), - subtitle: Text(translators[i][1]), - )), - const Padding(padding: EdgeInsets.all(8.0)), - const FreezerDivider(), - ExpansionTile( - title: LayoutBuilder( - builder: (context, constraints) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset('assets/icon_legacy.png', width: 24, height: 24), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - 'The original freezer development team'.i18n, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w600, - ), - softWrap: true, - overflow: TextOverflow.visible, - ), - ), - ), - Image.asset('assets/icon_legacy.png', width: 24, height: 24), - ], - ), - ], - ); + ListTile( + title: Text('Repository'.i18n), + subtitle: Text('Source code, report issues there.'.i18n), + leading: const Icon(Icons.code, color: Colors.green, size: 36.0), + onTap: () { + launchUrlString('https://github.com/DJDoubleD/ReFreezer'); }, ), - textColor: Theme.of(context).primaryColor, - iconColor: Theme.of(context).primaryColor, - tilePadding: const EdgeInsets.symmetric(horizontal: 16.0), - shape: const Border(), - children: [ - const FreezerDivider(), - const ListTile( - title: Text('exttex'), - subtitle: Text('Developer'), - ), - const ListTile( - title: Text('Bas Curtiz'), - subtitle: Text('Icon, logo, banner, design suggestions, tester'), - ), - const ListTile( - title: Text('Tobs'), - subtitle: Text('Alpha testers'), - ), - const ListTile( - title: Text('Deemix'), - subtitle: Text('Better app <3'), - ), - const ListTile( - title: Text('Xandar Null'), - subtitle: Text('Tester, translations help'), - ), - ListTile( - title: const Text('Francesco'), - subtitle: const Text('Tester'), - onTap: () { - setState(() { - settings.primaryColor = const Color(0xff333333); - }); - updateTheme(); - settings.save(); + ListTile( + title: Text('Crowdin'.i18n), + subtitle: Text('Help translating this app on Crowdin!'.i18n), + leading: const Icon(ReFreezerIcons.crowdin, + color: Color(0xffbdc1c6), size: 36.0), + onTap: () { + launchUrlString('https://crowdin.com/project/refreezer'); + }, + ), + ListTile( + isThreeLine: true, + title: Text('Donate'.i18n), + subtitle: Text( + 'You should rather support your favorite artists, instead of this app!' + .i18n), + leading: const Icon(FontAwesome5.paypal, + color: Colors.blue, size: 36.0), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Donate'.i18n), + content: Text( + 'No really, go support your favorite artists instead ;)' + .i18n), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + if (context.mounted) Navigator.of(context).pop(); + }, + ) + ], + ); + }); + // launchUrlString('https://paypal.me/exttex'); + }, + ), + const FreezerDivider(), + ...List.generate( + translators.length, + (i) => ListTile( + title: Text(translators[i][0]), + subtitle: Text(translators[i][1]), + )), + const Padding(padding: EdgeInsets.all(8.0)), + const FreezerDivider(), + ExpansionTile( + title: LayoutBuilder( + builder: (context, constraints) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/icon_legacy.png', + width: 24, height: 24), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + 'The original freezer development team'.i18n, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + ), + softWrap: true, + overflow: TextOverflow.visible, + ), + ), + ), + Image.asset('assets/icon_legacy.png', + width: 24, height: 24), + ], + ), + ], + ); }, ), - const ListTile( - title: Text('Annexhack'), - subtitle: Text('Android Auto help'), - ), - const FreezerDivider(), - ...List.generate( - freezerTranslators.length, - (i) => ListTile( - title: Text(freezerTranslators[i][0]), - subtitle: Text(freezerTranslators[i][1]), - )), - ], - ), - const FreezerDivider(), - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 0, 8), - child: Text( - 'Huge thanks to all the contributors! <3'.i18n, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16.0), + textColor: Theme.of(context).primaryColor, + iconColor: Theme.of(context).primaryColor, + tilePadding: const EdgeInsets.symmetric(horizontal: 16.0), + shape: const Border(), + children: [ + const FreezerDivider(), + const ListTile( + title: Text('exttex'), + subtitle: Text('Developer'), + ), + const ListTile( + title: Text('Bas Curtiz'), + subtitle: + Text('Icon, logo, banner, design suggestions, tester'), + ), + const ListTile( + title: Text('Tobs'), + subtitle: Text('Alpha testers'), + ), + const ListTile( + title: Text('Deemix'), + subtitle: Text('Better app <3'), + ), + const ListTile( + title: Text('Xandar Null'), + subtitle: Text('Tester, translations help'), + ), + ListTile( + title: const Text('Francesco'), + subtitle: const Text('Tester'), + onTap: () { + setState(() { + settings.primaryColor = const Color(0xff333333); + }); + updateTheme(); + settings.save(); + }, + ), + const ListTile( + title: Text('Annexhack'), + subtitle: Text('Android Auto help'), + ), + const FreezerDivider(), + ...List.generate( + freezerTranslators.length, + (i) => ListTile( + title: Text(freezerTranslators[i][0]), + subtitle: Text(freezerTranslators[i][1]), + )), + ], ), - ), - const FreezerDivider(), - ], + const FreezerDivider(), + Padding( + padding: const EdgeInsets.fromLTRB(0, 4, 0, 8), + child: Text( + 'Huge thanks to all the contributors! <3'.i18n, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16.0), + ), + ), + const FreezerDivider(), + ], + ), ), ); } diff --git a/marquee b/marquee deleted file mode 160000 index 4241bdf..0000000 --- a/marquee +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4241bdf46dd1346b939b0f4f02bde26b258f9cea diff --git a/move_to_background b/move_to_background deleted file mode 160000 index 600f91b..0000000 --- a/move_to_background +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 600f91b77430341fdd2ce100e42e8b6acf8b677b diff --git a/pubspec.yaml b/pubspec.yaml index 2cb4ce9..6cb04f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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. diff --git a/scrobblenaut b/scrobblenaut deleted file mode 160000 index 6966422..0000000 --- a/scrobblenaut +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6966422e6ae49494b2cb332177f793abc265450a