diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json
index ba129cfda..f1f9ceed7 100644
--- a/.fvm/fvm_config.json
+++ b/.fvm/fvm_config.json
@@ -1,4 +1,4 @@
{
- "flutterSdkVersion": "3.10.0",
+ "flutterSdkVersion": "3.16.0",
"flavors": {}
}
\ No newline at end of file
diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index d461e2962..d57cc0e83 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -4,7 +4,7 @@ on:
inputs:
version:
description: Version to release (x.x.x)
- default: 3.2.0
+ default: 3.3.0
required: true
channel:
type: choice
@@ -26,13 +26,18 @@ on:
default: true
env:
- FLUTTER_VERSION: '3.13.2'
+ FLUTTER_VERSION: '3.16.0'
jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
+ - uses: actions/checkout@v4
+ with:
+ repository: KRTirtho/flutter_distributor
+ path: flutter_distributor
+ ref: fix-windows-build
- uses: subosito/flutter-action@v2.10.0
with:
cache: true
@@ -74,9 +79,10 @@ jobs:
- name: Build Windows Executable
run: |
- dart pub global activate flutter_distributor
+ dart pub global activate melos
+ cd flutter_distributor && melos bs && cd ..
make innoinstall
- flutter_distributor package --platform=windows --targets=exe --skip-clean
+ dart run ./flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=windows --targets=exe --skip-clean
mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
- name: Create Chocolatey Package and set hash
@@ -319,6 +325,7 @@ jobs:
- name: Package Macos App
run: |
+ python3 -m pip install setuptools
npm install -g appdmg
mkdir -p build/${{ env.BUILD_VERSION }}
appdmg appdmg.json build/Spotube-macos-universal.dmg
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3710d8129..dbdf1326a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,44 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+## [3.3.0](https://github.com/KRTirtho/spotube/compare/v3.2.0...v3.3.0) (2023-11-27)
+
+
+### Features
+
+* Add JioSaavn as audio source ([#881](https://github.com/KRTirtho/spotube/issues/881)) ([14069cd](https://github.com/KRTirtho/spotube/commit/14069cd4fe08597c8d9aa0810270fb4c386c1d55))
+* **android:** better quick scroll/drag to scroll implementation ([2e2c44f](https://github.com/KRTirtho/spotube/commit/2e2c44f0afef69bf9bc485db97d45127a0847c8e))
+* **artist:** modularize page and add wikipedia section ([2a69886](https://github.com/KRTirtho/spotube/commit/2a698865567883271471ace9a44123bbfd8fcd2f))
+* discord RPC integration [#98](https://github.com/KRTirtho/spotube/issues/98) ([88b8785](https://github.com/KRTirtho/spotube/commit/88b8785cb86a19900f3a867b044c1ccb2fe400bb))
+* **mini_player:** show/hide lyrics [#851](https://github.com/KRTirtho/spotube/issues/851) ([dcbb156](https://github.com/KRTirtho/spotube/commit/dcbb1568337969841acc0abe0e7185ee5e4c3590))
+* paginated playlist and album page ([28a5d6b](https://github.com/KRTirtho/spotube/commit/28a5d6bb3820ab0bd4007664f73d685f6e1d2c90))
+* **translations:** add Turkish translations ([0c22469](https://github.com/KRTirtho/spotube/commit/0c22469503f32dbbf1a5d31419c1b76c699fa966))
+
+
+### Bug Fixes
+
+* 0:00 media duration in queue after application restart [#782](https://github.com/KRTirtho/spotube/issues/782) ([83c0b49](https://github.com/KRTirtho/spotube/commit/83c0b49da962d9f3d40de9525f90f0b320e8f7b8))
+* Add to Playlist Dialog memory leak [#817](https://github.com/KRTirtho/spotube/issues/817) ([fed36ec](https://github.com/KRTirtho/spotube/commit/fed36ecdd81e8a0f8358693eff0a6233dea32e5d))
+* **album_card:** show loading state during adding track to queue/play ([5633367](https://github.com/KRTirtho/spotube/commit/5633367397812148f6d712d06e97a4f84033f968))
+* alternative track source safearea overflow [#876](https://github.com/KRTirtho/spotube/issues/876) ([7b72a90](https://github.com/KRTirtho/spotube/commit/7b72a90bc65b541cbe2e24ef2234524b522ad71d))
+* android invalid download location Download not starting or not explaining error [#720](https://github.com/KRTirtho/spotube/issues/720) ([d056dbf](https://github.com/KRTirtho/spotube/commit/d056dbf9eeef7033dbc012d0c05800063e820042))
+* changed settings are not persisting after force stop [#821](https://github.com/KRTirtho/spotube/issues/821) ([e29a38d](https://github.com/KRTirtho/spotube/commit/e29a38dfa43ddf7a38046d1d40424f01dbe62261))
+* check for unsynced lyrics and error handling for timed lyrics query ([1d77556](https://github.com/KRTirtho/spotube/commit/1d77556157d158600f29cf2ea5f26c567607dec7))
+* **genres:** lag while scrolling ([dc980b0](https://github.com/KRTirtho/spotube/commit/dc980b024edad3132e72cbb2f0087297a4b76469))
+* infinite list disappearing for a moment everytime new page is fetched ([1334a62](https://github.com/KRTirtho/spotube/commit/1334a62aaea31f97031b3ebf455e94c583f37314))
+* last track of queue keeps repeating [#718](https://github.com/KRTirtho/spotube/issues/718) ([58e5698](https://github.com/KRTirtho/spotube/commit/58e569864dddd74c3064624998dfc184046e97eb))
+* Navigating to settings, redirects to home page [#812](https://github.com/KRTirtho/spotube/issues/812) ([da04f06](https://github.com/KRTirtho/spotube/commit/da04f068f9b7effff8d50cb5714d93ea80c22b7f))
+* new releases section flickering on scroll glitch ([ee94b7c](https://github.com/KRTirtho/spotube/commit/ee94b7cbb24e0f0bc22a6d49c830d4055aa02895))
+* **playbutton_card:** annoying animation ([574406d](https://github.com/KRTirtho/spotube/commit/574406dd5fc410914b27e7fce374323696845012))
+* scrobbling not working for first track or single track ([0a6b54d](https://github.com/KRTirtho/spotube/commit/0a6b54da367345b73fe6e954f1d9368d9f9ead71))
+* settings page scrollbar position ([ee82290](https://github.com/KRTirtho/spotube/commit/ee8229020b3b03fc074b316db4b322af13b807bd))
+* shuffle doesn't move active track to top ([4956bf3](https://github.com/KRTirtho/spotube/commit/4956bf367baae39c88b5de7c6c136513a14f8ad2))
+* spotube doesn't exit properly, hangs in infinite loop [#768](https://github.com/KRTirtho/spotube/issues/768) ([353ca79](https://github.com/KRTirtho/spotube/commit/353ca79be334077c3ac27b4f64e8b4b15eca7175))
+* trim login field padding ([286ef83](https://github.com/KRTirtho/spotube/commit/286ef83e8ec516db70019398d9e3e724437a4172))
+* use CustomScrollView for personalized page ([7d05c40](https://github.com/KRTirtho/spotube/commit/7d05c40dc0d04208b059f2483c1e4de199c8b51d))
+* user_playlists layout, track tile index, ([487c2ed](https://github.com/KRTirtho/spotube/commit/487c2ed6bdc4af33006ba52532eb4eaaa261dceb))
+* **windows:** media control not working [#641](https://github.com/KRTirtho/spotube/issues/641) ([7818574](https://github.com/KRTirtho/spotube/commit/7818574356d0fb8ff567e1f6a83fd0b6f2ee7c8a))
+
## [3.2.0](https://github.com/KRTirtho/spotube/compare/v3.1.2...v3.2.0) (2023-10-16)
diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
index 11206e6d9..b2823e623 100644
--- a/CONTRIBUTION.md
+++ b/CONTRIBUTION.md
@@ -119,7 +119,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirt
Do the following:
-- Download the latest Flutter SDK (>=3.10.0) & enable desktop support
+- Download the latest Flutter SDK (>=3.16.0) & enable desktop support
- Install Development dependencies in linux
- Debian (>=12/Bookworm)/Ubuntu
```bash
diff --git a/README.md b/README.md
index d82af7834..498c45ded 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
An open source, cross-platform Spotify client compatible across multiple platforms
-utilizing Spotify's data API and YouTube (or Piped.video) as an audio source,
+utilizing Spotify's data API and YouTube (or Piped.video or JioSaavn) as an audio source,
eliminating the need for Spotify Premium
Btw it's not another Electron app😉
@@ -184,19 +184,23 @@ If you are concerned, you can [read the reason of choosing this license](https:/
- [Click to show]
🙏 Library/Plugin/Framework Credits
+ [Click to show]
🙏 Services/Package/Plugin Credits
+### Services
1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
+1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux
1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos.
1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan
1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device
+
+### Dependencies
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off.
@@ -216,7 +220,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
-1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
+1. [fl_query](https://fl-query.krtirtho.dev) - Asynchronous data caching, refetching & invalidation library for Flutter
+1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
+1. [fl_query_devtools](https://fl-query.krtirtho.dev) - Devtools support for Fl-Query
+1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
@@ -227,7 +234,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
-1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
+1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
@@ -244,10 +251,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files
1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
-1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
+1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image.
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
-1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
+1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
@@ -258,7 +265,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
-1. [supabase](https://supabase.com) - A dart client for Supabase. This client makes it simple for developers to build secure and scalable products.
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.
@@ -269,6 +275,13 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
1. [simple_icons](https://jlnrrg.github.io/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
+1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
+1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
+1. [very_good_infinite_list](https://github.com/VeryGoodOpenSource/very_good_infinite_list) - A library for easily displaying paginated data, created by Very Good Ventures. Great for activity feeds, news feeds, and more.
+1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
+1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
+1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
+1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps.
@@ -279,12 +292,11 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
-1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter
-1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
-1. [fl_query_devtools](https://fl-query.vercel.app) - Devtools support for Fl-Query
1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter.
+1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
+1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
© Copyright Spotube 2023
diff --git a/bin/gen-credits.dart b/bin/gen-credits.dart
index 43e1e53d2..f8975335f 100644
--- a/bin/gen-credits.dart
+++ b/bin/gen-credits.dart
@@ -1,7 +1,7 @@
+import 'dart:developer';
import 'dart:io';
import 'package:collection/collection.dart';
-import 'package:path/path.dart';
import 'package:http/http.dart';
import 'package:html/parser.dart';
import 'package:pub_api_client/pub_api_client.dart';
@@ -33,15 +33,20 @@ void main() async {
final gitDeps = gitDepsList.map(
(d) {
+ final uri = Uri.parse(
+ d.value.url.toString().replaceAll('.git', ''),
+ );
return MapEntry(
d.key,
- join(
- d.value.url.toString().replaceAll('.git', ''),
- 'raw',
- d.value.ref ?? 'main',
- d.value.path ?? '',
- 'pubspec.yaml',
- ),
+ uri.replace(
+ pathSegments: [
+ ...uri.pathSegments,
+ 'raw',
+ d.value.ref ?? 'main',
+ d.value.path ?? '',
+ 'pubspec.yaml',
+ ],
+ ).toString(),
);
},
).toList();
@@ -55,7 +60,10 @@ void main() async {
} catch (e) {
final document = parse(res.body);
final pre = document.querySelector('pre');
- if (pre == null) rethrow;
+ if (pre == null) {
+ log(d.toString());
+ rethrow;
+ }
return Pubspec.parse(pre.text);
}
}
diff --git a/lib/collections/env.dart b/lib/collections/env.dart
index 3923435b0..50fe1e6a7 100644
--- a/lib/collections/env.dart
+++ b/lib/collections/env.dart
@@ -1,4 +1,5 @@
import 'package:envied/envied.dart';
+import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
part 'env.g.dart';
@@ -24,5 +25,8 @@ abstract class Env {
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
- static bool get enableUpdateChecker => _enableUpdateChecker == "1";
+ static bool get enableUpdateChecker =>
+ DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1";
+
+ static String discordAppId = "1176718791388975124";
}
diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart
index 8c7ea73b7..abccb3ad9 100644
--- a/lib/collections/intents.dart
+++ b/lib/collections/intents.dart
@@ -1,6 +1,7 @@
+import 'dart:io';
+
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/components/player/player_controls.dart';
@@ -115,7 +116,7 @@ class CloseAppAction extends Action {
@override
invoke(intent) {
if (kIsDesktop) {
- DesktopTools.window.close();
+ exit(0);
} else {
SystemNavigator.pop();
}
diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart
index 58328b75d..0518363e6 100644
--- a/lib/collections/language_codes.dart
+++ b/lib/collections/language_codes.dart
@@ -660,10 +660,10 @@ abstract class LanguageLocals {
// name: "Tonga (Tonga Islands)",
// nativeName: "faka Tonga",
// ),
- // "tr": const ISOLanguageName(
- // name: "Turkish",
- // nativeName: "Türkçe",
- // ),
+ "tr": const ISOLanguageName(
+ name: "Turkish",
+ nativeName: "Türkçe",
+ ),
// "ts": const ISOLanguageName(
// name: "Tsonga",
// nativeName: "Xitsonga",
diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart
index 81ebb3e66..82597ddb8 100644
--- a/lib/collections/routes.dart
+++ b/lib/collections/routes.dart
@@ -3,29 +3,29 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search;
+import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
+import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
+import 'package:spotube/pages/playlist/liked_playlist.dart';
+import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/shared/spotube_page_route.dart';
-import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/library/library.dart';
import 'package:spotube/pages/desktop_login/login_tutorial.dart';
import 'package:spotube/pages/desktop_login/desktop_login.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
-import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
-import '../pages/library/playlist_generate/playlist_generate_result.dart';
-
final rootNavigatorKey = Catcher2.navigatorKey;
final shellRouteNavigatorKey = GlobalKey();
final router = GoRouter(
@@ -104,7 +104,9 @@ final router = GoRouter(
path: "/album/:id",
pageBuilder: (context, state) {
assert(state.extra is AlbumSimple);
- return SpotubePage(child: AlbumPage(state.extra as AlbumSimple));
+ return SpotubePage(
+ child: AlbumPage(album: state.extra as AlbumSimple),
+ );
},
),
GoRoute(
@@ -119,7 +121,9 @@ final router = GoRouter(
pageBuilder: (context, state) {
assert(state.extra is PlaylistSimple);
return SpotubePage(
- child: PlaylistView(state.extra as PlaylistSimple),
+ child: state.pathParameters["id"] == "user-liked-tracks"
+ ? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
+ : PlaylistPage(playlist: state.extra as PlaylistSimple),
);
},
),
diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart
index 5c769498c..d00775c75 100644
--- a/lib/collections/spotube_icons.dart
+++ b/lib/collections/spotube_icons.dart
@@ -40,6 +40,7 @@ abstract class SpotubeIcons {
static const trash = FeatherIcons.trash2;
static const clock = FeatherIcons.clock;
static const lyrics = Icons.lyrics_rounded;
+ static const lyricsOff = Icons.lyrics_outlined;
static const logout = FeatherIcons.logOut;
static const login = FeatherIcons.logIn;
static const dashboard = FeatherIcons.grid;
@@ -106,4 +107,5 @@ abstract class SpotubeIcons {
static const eye = FeatherIcons.eye;
static const noEye = FeatherIcons.eyeOff;
static const normalize = FeatherIcons.barChart2;
+ static const wikipedia = SimpleIcons.wikipedia;
}
diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart
index d8f8d85bd..c7ae2f9a0 100644
--- a/lib/components/album/album_card.dart
+++ b/lib/components/album/album_card.dart
@@ -4,10 +4,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
-import 'package:spotube/hooks/use_breakpoint_value.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/extensions/infinite_query.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
+import 'package:spotube/services/queries/album.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
@@ -16,7 +18,7 @@ extension FormattedAlbumType on AlbumType {
}
class AlbumCard extends HookConsumerWidget {
- final Album album;
+ final AlbumSimple album;
const AlbumCard(
this.album, {
Key? key,
@@ -28,30 +30,54 @@ class AlbumCard extends HookConsumerWidget {
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
+
final queryClient = useQueryClient();
+
bool isPlaylistPlaying = useMemoized(
() => playlist.containsCollection(album.id!),
[playlist, album.id],
);
- final marginH = useBreakpointValue(
- xs: 10,
- sm: 10,
- md: 15,
- others: 20,
- );
-
final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
+ final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
+
+ Future> fetchAllTrack() async {
+ if (album.tracks != null && album.tracks!.isNotEmpty) {
+ return album.tracks!
+ .map((track) =>
+ TypeConversionUtils.simpleTrack_X_Track(track, album))
+ .toList();
+ }
+ final job = AlbumQueries.tracksOfJob(album.id!);
+
+ final query = queryClient.createInfiniteQuery(
+ job.queryKey,
+ (page) => job.task(page, (spotify: spotify, album: album)),
+ initialPage: 0,
+ nextPage: job.nextPage,
+ );
+
+ return await query.fetchAllTracks(
+ getAllTracks: () async {
+ final res = await spotify.albums.tracks(album.id!).all();
+ return res
+ .map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
+ .toList();
+ },
+ );
+ }
+
return PlaybuttonCard(
imageUrl: TypeConversionUtils.image_X_UrlString(
album.images,
placeholder: ImagePlaceholder.collection,
),
- margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
+ margin: const EdgeInsets.symmetric(horizontal: 10),
isPlaying: isPlaylistPlaying,
- isLoading: isPlaylistPlaying && playlist.isFetching == true,
+ isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
+ updating.value,
title: album.name!,
description:
"${album.albumType?.formatted} • ${TypeConversionUtils.artists_X_String(album.artists ?? [])}",
@@ -61,20 +87,15 @@ class AlbumCard extends HookConsumerWidget {
onPlaybuttonPressed: () async {
updating.value = true;
try {
- if (isPlaylistPlaying && playing) {
- return audioPlayer.pause();
- } else if (isPlaylistPlaying && !playing) {
- return audioPlayer.resume();
+ if (isPlaylistPlaying) {
+ return playing ? audioPlayer.pause() : audioPlayer.resume();
}
- await playlistNotifier.load(
- album.tracks
- ?.map((e) =>
- TypeConversionUtils.simpleTrack_X_Track(e, album))
- .toList() ??
- [],
- autoPlay: true,
- );
+ final fetchedTracks = await fetchAllTrack();
+
+ if (fetchedTracks.isEmpty) return;
+
+ await playlistNotifier.load(fetchedTracks, autoPlay: true);
playlistNotifier.addCollection(album.id!);
} finally {
updating.value = false;
@@ -87,28 +108,16 @@ class AlbumCard extends HookConsumerWidget {
updating.value = true;
try {
- final fetchedTracks =
- await queryClient.fetchQuery, SpotifyApi>(
- "album-tracks/${album.id}",
- () {
- return spotify.albums
- .getTracks(album.id!)
- .all()
- .then((value) => value.toList());
- },
- ).then(
- (tracks) => tracks
- ?.map(
- (e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
- .toList(),
- );
-
- if (fetchedTracks == null || fetchedTracks.isEmpty) return;
+ final fetchedTracks = await fetchAllTrack();
+
+ if (fetchedTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedTracks);
playlistNotifier.addCollection(album.id!);
if (context.mounted) {
final snackbar = SnackBar(
- content: Text("Added ${album.tracks?.length} tracks to queue"),
+ content: Text(
+ context.l10n.added_to_queue(fetchedTracks.length),
+ ),
action: SnackBarAction(
label: "Undo",
onPressed: () {
@@ -117,7 +126,8 @@ class AlbumCard extends HookConsumerWidget {
},
),
);
- ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar);
+
+ scaffoldMessenger?.showSnackBar(snackbar);
}
} finally {
updating.value = false;
diff --git a/lib/components/artist/artist_album_list.dart b/lib/components/artist/artist_album_list.dart
index 8fa9be87f..5114170cd 100644
--- a/lib/components/artist/artist_album_list.dart
+++ b/lib/components/artist/artist_album_list.dart
@@ -1,11 +1,9 @@
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/album/album_card.dart';
-import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
-import 'package:spotube/components/shared/waypoint.dart';
+import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
+import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@@ -20,7 +18,6 @@ class ArtistAlbumList extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final scrollController = useScrollController();
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
final albums = useMemoized(() {
@@ -29,40 +26,17 @@ class ArtistAlbumList extends HookConsumerWidget {
.toList();
}, [albumsQuery.pages]);
- final hasNextPage = albumsQuery.pages.isEmpty
- ? false
- : (albumsQuery.pages.last.items?.length ?? 0) == 5;
+ final theme = Theme.of(context);
- return Column(
- children: [
- ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: Scrollbar(
- interactive: false,
- controller: scrollController,
- child: Waypoint(
- controller: scrollController,
- onTouchEdge: albumsQuery.fetchNext,
- child: SingleChildScrollView(
- controller: scrollController,
- scrollDirection: Axis.horizontal,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- ...albums.map((album) => AlbumCard(album)),
- if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
- ],
- ),
- ),
- ),
- ),
- ),
- ],
+ return HorizontalPlaybuttonCardView(
+ isLoadingNextPage: albumsQuery.isLoadingNextPage,
+ hasNextPage: albumsQuery.hasNextPage,
+ items: albums,
+ onFetchMore: albumsQuery.fetchNext,
+ title: Text(
+ context.l10n.albums,
+ style: theme.textTheme.headlineSmall,
+ ),
);
}
}
diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart
index 993e9f6a4..434b90ad3 100644
--- a/lib/components/artist/artist_card.dart
+++ b/lib/components/artist/artist_card.dart
@@ -5,8 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/hooks/use_breakpoint_value.dart';
-import 'package:spotube/hooks/use_brightness_value.dart';
+import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
+import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart
index b9783f876..f2b183f46 100644
--- a/lib/components/desktop_login/login_form.dart
+++ b/lib/components/desktop_login/login_form.dart
@@ -63,7 +63,7 @@ class TokenLoginForm extends HookConsumerWidget {
return;
}
final cookieHeader =
- "sp_dc=${directCodeController.text}; sp_key=${keyCodeController.text}";
+ "sp_dc=${directCodeController.text.trim()}; sp_key=${keyCodeController.text.trim()}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(
diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart
index 42654ed9c..7f5801576 100644
--- a/lib/components/genre/category_card.dart
+++ b/lib/components/genre/category_card.dart
@@ -1,13 +1,10 @@
-import 'dart:ui';
-
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/playlist/playlist_card.dart';
-import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
-import 'package:spotube/components/shared/waypoint.dart';
+import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@@ -22,57 +19,33 @@ class CategoryCard extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final scrollController = useScrollController();
final playlistQuery = useQueries.category.playlistsOf(
ref,
category.id!,
);
- if (playlistQuery.hasErrors && !playlistQuery.hasPageData) {
+ final playlists = useMemoized(
+ () => playlistQuery.pages.expand(
+ (page) {
+ return page.items?.whereNotNull() ??
+ const Iterable.empty();
+ },
+ ).toList(),
+ [playlistQuery.pages],
+ );
+
+ if (playlistQuery.hasErrors &&
+ !playlistQuery.hasPageData &&
+ !playlistQuery.isLoadingNextPage) {
return const SizedBox.shrink();
}
- final playlists = playlistQuery.pages.expand(
- (page) {
- return page.items?.where((i) => i != null) ?? const Iterable.empty();
- },
- ).toList();
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- category.name!,
- style: Theme.of(context).textTheme.titleMedium,
- ),
- ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: Waypoint(
- controller: scrollController,
- onTouchEdge: playlistQuery.fetchNext,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- controller: scrollController,
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- ...playlists.map((playlist) => PlaylistCard(playlist)),
- if (playlistQuery.hasNextPage)
- const ShimmerPlaybuttonCard(count: 1),
- ],
- ),
- ),
- ),
- ),
- ],
- ),
+
+ return HorizontalPlaybuttonCardView(
+ title: Text(category.name!),
+ isLoadingNextPage: playlistQuery.isLoadingNextPage,
+ hasNextPage: playlistQuery.hasNextPage,
+ items: playlists,
+ onFetchMore: playlistQuery.fetchNext,
);
}
}
diff --git a/lib/components/library/user_downloads/download_item.dart b/lib/components/library/user_downloads/download_item.dart
index ae8a2513a..10dec4104 100644
--- a/lib/components/library/user_downloads/download_item.dart
+++ b/lib/components/library/user_downloads/download_item.dart
@@ -5,9 +5,9 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/services/download_manager/download_status.dart';
+import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class DownloadItem extends HookConsumerWidget {
@@ -24,25 +24,25 @@ class DownloadItem extends HookConsumerWidget {
final taskStatus = useState(null);
useEffect(() {
- if (track is! SpotubeTrack) return null;
- final notifier = downloadManager.getStatusNotifier(track as SpotubeTrack);
+ if (track is! SourcedTrack) return null;
+ final notifier = downloadManager.getStatusNotifier(track as SourcedTrack);
taskStatus.value = notifier?.value;
- listener() {
+
+ void listener() {
taskStatus.value = notifier?.value;
}
- downloadManager
- .getStatusNotifier(track as SpotubeTrack)
- ?.addListener(listener);
+ notifier?.addListener(listener);
return () {
- downloadManager
- .getStatusNotifier(track as SpotubeTrack)
- ?.removeListener(listener);
+ notifier?.removeListener(listener);
};
}, [track]);
+ final isQueryingSourceInfo =
+ taskStatus.value == null || track is! SourcedTrack;
+
return ListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
@@ -63,7 +63,7 @@ class DownloadItem extends HookConsumerWidget {
track.artists ?? [],
mainAxisAlignment: WrapAlignment.start,
),
- trailing: taskStatus.value == null || track is! SpotubeTrack
+ trailing: isQueryingSourceInfo
? Text(
context.l10n.querying_info,
style: Theme.of(context).textTheme.labelMedium,
@@ -72,7 +72,7 @@ class DownloadItem extends HookConsumerWidget {
DownloadStatus.downloading => HookBuilder(builder: (context) {
final taskProgress = useListenable(useMemoized(
() => downloadManager
- .getProgressNotifier(track as SpotubeTrack),
+ .getProgressNotifier(track as SourcedTrack),
[track],
));
return SizedBox(
@@ -86,13 +86,13 @@ class DownloadItem extends HookConsumerWidget {
IconButton(
icon: const Icon(SpotubeIcons.pause),
onPressed: () {
- downloadManager.pause(track as SpotubeTrack);
+ downloadManager.pause(track as SourcedTrack);
}),
const SizedBox(width: 10),
IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
- downloadManager.cancel(track as SpotubeTrack);
+ downloadManager.cancel(track as SourcedTrack);
}),
],
),
@@ -104,13 +104,13 @@ class DownloadItem extends HookConsumerWidget {
IconButton(
icon: const Icon(SpotubeIcons.play),
onPressed: () {
- downloadManager.resume(track as SpotubeTrack);
+ downloadManager.resume(track as SourcedTrack);
}),
const SizedBox(width: 10),
IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
- downloadManager.cancel(track as SpotubeTrack);
+ downloadManager.cancel(track as SourcedTrack);
})
],
),
@@ -126,7 +126,7 @@ class DownloadItem extends HookConsumerWidget {
IconButton(
icon: const Icon(SpotubeIcons.refresh),
onPressed: () {
- downloadManager.retry(track as SpotubeTrack);
+ downloadManager.retry(track as SourcedTrack);
},
),
],
@@ -137,7 +137,7 @@ class DownloadItem extends HookConsumerWidget {
DownloadStatus.queued => IconButton(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
- downloadManager.removeFromQueue(track as SpotubeTrack);
+ downloadManager.removeFromQueue(track as SourcedTrack);
}),
},
);
diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart
index 50ae64be7..cc8b10cf3 100644
--- a/lib/components/library/user_local_tracks.dart
+++ b/lib/components/library/user_local_tracks.dart
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:catcher_2/catcher_2.dart';
-import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -12,7 +11,6 @@ import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
-import 'package:permission_handler/permission_handler.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
@@ -20,17 +18,14 @@ import 'package:spotube/components/shared/expandable_search/expandable_search.da
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
-import 'package:spotube/components/shared/track_table/track_tile.dart';
+import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/hooks/use_async_effect.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
-import 'package:spotube/provider/user_preferences_provider.dart';
-import 'package:spotube/utils/platform.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
-import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'
- show FfiException;
+import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
const supportedAudioTypes = [
"audio/webm",
@@ -162,39 +157,13 @@ class UserLocalTracks extends HookConsumerWidget {
final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying =
playlist.containsTracks(trackSnapshot.value ?? []);
- final isMounted = useIsMounted();
final searchController = useTextEditingController();
useValueListenable(searchController);
final searchFocus = useFocusNode();
final isFiltering = useState(false);
- useAsyncEffect(
- () async {
- if (!kIsMobile) return;
-
- final androidInfo = await DeviceInfoPlugin().androidInfo;
-
- final hasNoStoragePerm = androidInfo.version.sdkInt < 33 &&
- !await Permission.storage.isGranted &&
- !await Permission.storage.isLimited;
-
- final hasNoAudioPerm = androidInfo.version.sdkInt >= 33 &&
- !await Permission.audio.isGranted &&
- !await Permission.audio.isLimited;
-
- if (hasNoStoragePerm) {
- await Permission.storage.request();
- if (isMounted()) ref.refresh(localTracksProvider);
- }
- if (hasNoAudioPerm) {
- await Permission.audio.request();
- if (isMounted()) ref.refresh(localTracksProvider);
- }
- },
- null,
- [],
- );
+ final controller = useScrollController();
return Column(
children: [
@@ -230,7 +199,8 @@ class UserLocalTracks extends HookConsumerWidget {
),
const Spacer(),
ExpandableSearchButton(
- isFiltering: isFiltering,
+ isFiltering: isFiltering.value,
+ onPressed: (value) => isFiltering.value = value,
searchFocus: searchFocus,
),
const SizedBox(width: 10),
@@ -253,7 +223,8 @@ class UserLocalTracks extends HookConsumerWidget {
ExpandableSearchField(
searchController: searchController,
searchFocus: searchFocus,
- isFiltering: isFiltering,
+ isFiltering: isFiltering.value,
+ onChangeFiltering: (value) => isFiltering.value = value,
),
trackSnapshot.when(
data: (tracks) {
@@ -289,7 +260,9 @@ class UserLocalTracks extends HookConsumerWidget {
ref.refresh(localTracksProvider);
},
child: InterScrollbar(
+ controller: controller,
child: ListView.builder(
+ controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length,
itemBuilder: (context, index) {
@@ -313,7 +286,7 @@ class UserLocalTracks extends HookConsumerWidget {
);
},
loading: () =>
- const Expanded(child: ShimmerTrackTile(noSliver: true)),
+ const Expanded(child: ShimmerTrackTileGroup(noSliver: true)),
error: (error, stackTrace) =>
Text(error.toString() + stackTrace.toString()),
)
diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart
index 8ed3e73d6..f7736ca7e 100644
--- a/lib/components/library/user_playlists.dart
+++ b/lib/components/library/user_playlists.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart' hide Image;
+import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:collection/collection.dart';
@@ -13,6 +14,7 @@ import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/waypoint.dart';
+import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart';
@@ -80,64 +82,73 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator(
onRefresh: playlistsQuery.refresh,
- child: InterScrollbar(
- controller: controller,
- child: SingleChildScrollView(
+ child: SafeArea(
+ child: InterScrollbar(
controller: controller,
- physics: const AlwaysScrollableScrollPhysics(),
- child: Waypoint(
+ child: CustomScrollView(
controller: controller,
- onTouchEdge: () {
- if (playlistsQuery.hasNextPage) {
- playlistsQuery.fetchNext();
- }
- },
- child: SafeArea(
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.all(10),
- child: SearchBar(
- onChanged: (value) => searchText.value = value,
- hintText: context.l10n.filter_playlists,
- leading: const Icon(SpotubeIcons.filter),
+ slivers: [
+ SliverToBoxAdapter(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(10),
+ child: SearchBar(
+ onChanged: (value) => searchText.value = value,
+ hintText: context.l10n.filter_playlists,
+ leading: const Icon(SpotubeIcons.filter),
+ ),
),
- ),
- AnimatedCrossFade(
- duration: const Duration(milliseconds: 300),
- crossFadeState: !playlistsQuery.hasPageData &&
- !playlistsQuery.hasPageError &&
- !playlistsQuery.isLoadingNextPage
- ? CrossFadeState.showFirst
- : CrossFadeState.showSecond,
- firstChild:
- const Center(child: ShimmerPlaybuttonCard(count: 7)),
- secondChild: Wrap(
- runSpacing: 10,
- alignment: WrapAlignment.center,
+ Row(
children: [
- Row(
- children: [
- const SizedBox(width: 10),
- const PlaylistCreateDialogButton(),
- const SizedBox(width: 10),
- ElevatedButton.icon(
- icon: const Icon(SpotubeIcons.magic),
- label: Text(context.l10n.generate_playlist),
- onPressed: () {
- GoRouter.of(context).push("/library/generate");
- },
- ),
- const SizedBox(width: 10),
- ],
+ const SizedBox(width: 10),
+ const PlaylistCreateDialogButton(),
+ const SizedBox(width: 10),
+ ElevatedButton.icon(
+ icon: const Icon(SpotubeIcons.magic),
+ label: Text(context.l10n.generate_playlist),
+ onPressed: () {
+ GoRouter.of(context).push("/library/generate");
+ },
),
- ...playlists.map((playlist) => PlaylistCard(playlist))
+ const SizedBox(width: 10),
],
),
- ),
- ],
+ ],
+ ),
+ ),
+ const SliverToBoxAdapter(
+ child: SizedBox(height: 10),
),
- ),
+ SliverLayoutBuilder(builder: (context, constrains) {
+ return SliverGrid.builder(
+ itemCount: playlists.length + 1,
+ gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
+ maxCrossAxisExtent: 200,
+ mainAxisExtent: constrains.smAndDown ? 225 : 250,
+ crossAxisSpacing: 8,
+ mainAxisSpacing: 8,
+ ),
+ itemBuilder: (context, index) {
+ if (index == playlists.length) {
+ if (!playlistsQuery.hasNextPage) {
+ return const SizedBox.shrink();
+ }
+
+ return Waypoint(
+ controller: controller,
+ isGrid: true,
+ onTouchEdge: playlistsQuery.fetchNext,
+ child: const ShimmerPlaybuttonCard(count: 1),
+ );
+ }
+
+ return PlaylistCard(playlists[index]);
+ },
+ );
+ })
+ ],
),
),
),
diff --git a/lib/hooks/use_synced_lyrics.dart b/lib/components/lyrics/use_synced_lyrics.dart
similarity index 100%
rename from lib/hooks/use_synced_lyrics.dart
rename to lib/components/lyrics/use_synced_lyrics.dart
diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart
index 811d24c51..889b7c5cb 100644
--- a/lib/components/player/player.dart
+++ b/lib/components/player/player.dart
@@ -18,8 +18,8 @@ import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/panels/sliding_up_panel.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/hooks/use_custom_status_bar_color.dart';
-import 'package:spotube/hooks/use_palette_color.dart';
+import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
+import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart
index b3a1e3408..7a248aa5d 100644
--- a/lib/components/player/player_actions.dart
+++ b/lib/components/player/player_actions.dart
@@ -5,10 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/heart_button.dart';
+import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart';
@@ -35,6 +35,7 @@ class PlayerActions extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
+ final mediaQuery = MediaQuery.of(context);
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider);
@@ -86,23 +87,7 @@ class PlayerActions extends HookConsumerWidget {
tooltip: context.l10n.queue,
onPressed: playlist.activeTrack != null
? () {
- showModalBottomSheet(
- context: context,
- isDismissible: true,
- enableDrag: true,
- isScrollControlled: true,
- backgroundColor: Colors.black12,
- barrierColor: Colors.black12,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(10),
- ),
- constraints: BoxConstraints(
- maxHeight: MediaQuery.of(context).size.height * .7,
- ),
- builder: (context) {
- return PlayerQueue(floating: floatingQueue);
- },
- );
+ Scaffold.of(context).openEndDrawer();
}
: null,
),
@@ -119,6 +104,7 @@ class PlayerActions extends HookConsumerWidget {
isScrollControlled: true,
backgroundColor: Colors.black12,
barrierColor: Colors.black12,
+ elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart
index 07a6b7ba3..1000af18b 100644
--- a/lib/components/player/player_controls.dart
+++ b/lib/components/player/player_controls.dart
@@ -8,7 +8,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
-import 'package:spotube/hooks/use_progress.dart';
+import 'package:spotube/components/player/use_progress.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart
index 354d1a364..4869a0fa2 100644
--- a/lib/components/player/player_overlay.dart
+++ b/lib/components/player/player_overlay.dart
@@ -9,7 +9,7 @@ import 'package:spotube/components/root/spotube_navigation_bar.dart';
import 'package:spotube/components/shared/panels/sliding_up_panel.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart';
-import 'package:spotube/hooks/use_progress.dart';
+import 'package:spotube/components/player/use_progress.dart';
import 'package:spotube/components/player/player.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart
index 725af22ba..8142740c9 100644
--- a/lib/components/player/player_queue.dart
+++ b/lib/components/player/player_queue.dart
@@ -11,10 +11,10 @@ import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
-import 'package:spotube/components/shared/track_table/track_tile.dart';
+import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/hooks/use_auto_scroll_controller.dart';
+import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
@@ -36,12 +36,15 @@ class PlayerQueue extends HookConsumerWidget {
final tracks = playlist.tracks;
final borderRadius = floating
- ? BorderRadius.circular(10)
+ ? const BorderRadius.only(
+ topLeft: Radius.circular(10),
+ )
: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
);
final theme = Theme.of(context);
+ final mediaQuery = MediaQuery.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color;
final filteredTracks = useMemoized(
@@ -80,47 +83,49 @@ class PlayerQueue extends HookConsumerWidget {
return const NotFound(vertical: true);
}
- return BackdropFilter(
- filter: ImageFilter.blur(
- sigmaX: 12.0,
- sigmaY: 12.0,
- ),
- child: Container(
- margin: EdgeInsets.all(floating ? 8.0 : 0),
- padding: const EdgeInsets.only(
- top: 5.0,
+ return ClipRRect(
+ borderRadius: borderRadius,
+ clipBehavior: Clip.hardEdge,
+ child: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 15,
+ sigmaY: 15,
),
- decoration: BoxDecoration(
- color: theme.scaffoldBackgroundColor.withOpacity(0.5),
- borderRadius: borderRadius,
- ),
- child: CallbackShortcuts(
- bindings: {
- LogicalKeySet(LogicalKeyboardKey.escape): () {
- if (!isSearching.value) {
- Navigator.of(context).pop();
+ child: Container(
+ padding: const EdgeInsets.only(
+ top: 5.0,
+ ),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
+ borderRadius: borderRadius,
+ ),
+ child: CallbackShortcuts(
+ bindings: {
+ LogicalKeySet(LogicalKeyboardKey.escape): () {
+ if (!isSearching.value) {
+ Navigator.of(context).pop();
+ }
+ isSearching.value = false;
+ searchText.value = '';
}
- isSearching.value = false;
- searchText.value = '';
- }
- },
- child: LayoutBuilder(builder: (context, constraints) {
- return Column(
+ },
+ child: Column(
children: [
- Container(
- height: 5,
- width: 100,
- margin: const EdgeInsets.only(bottom: 5, top: 2),
- decoration: BoxDecoration(
- color: headlineColor,
- borderRadius: BorderRadius.circular(20),
+ if (!floating)
+ Container(
+ height: 5,
+ width: 100,
+ margin: const EdgeInsets.only(bottom: 5, top: 2),
+ decoration: BoxDecoration(
+ color: headlineColor,
+ borderRadius: BorderRadius.circular(20),
+ ),
),
- ),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- if (constraints.mdAndUp || !isSearching.value) ...[
+ if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
Text(
context.l10n.tracks_in_queue(tracks.length),
@@ -132,7 +137,7 @@ class PlayerQueue extends HookConsumerWidget {
),
const Spacer(),
],
- if (constraints.mdAndUp || isSearching.value)
+ if (mediaQuery.mdAndUp || isSearching.value)
TextField(
onChanged: (value) {
searchText.value = value;
@@ -140,7 +145,7 @@ class PlayerQueue extends HookConsumerWidget {
decoration: InputDecoration(
hintText: context.l10n.search,
isDense: true,
- prefixIcon: constraints.smAndDown
+ prefixIcon: mediaQuery.smAndDown
? IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_outlined,
@@ -157,8 +162,8 @@ class PlayerQueue extends HookConsumerWidget {
: const Icon(SpotubeIcons.filter),
constraints: BoxConstraints(
maxHeight: 40,
- maxWidth: constraints.smAndDown
- ? constraints.maxWidth - 20
+ maxWidth: mediaQuery.smAndDown
+ ? mediaQuery.size.width - 40
: 300,
),
),
@@ -170,7 +175,7 @@ class PlayerQueue extends HookConsumerWidget {
isSearching.value = !isSearching.value;
},
),
- if (constraints.mdAndUp || !isSearching.value) ...[
+ if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
FilledButton(
style: FilledButton.styleFrom(
@@ -197,51 +202,50 @@ class PlayerQueue extends HookConsumerWidget {
const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty)
Flexible(
- child: InterScrollbar(
- controller: controller,
- child: ReorderableListView.builder(
- onReorder: (oldIndex, newIndex) {
- playlistNotifier.moveTrack(oldIndex, newIndex);
- },
- scrollController: controller,
- itemCount: tracks.length,
- shrinkWrap: true,
- buildDefaultDragHandles: false,
- itemBuilder: (context, i) {
- final track = tracks.elementAt(i);
- return AutoScrollTag(
- key: ValueKey(i),
- controller: controller,
- index: i,
- child: Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 8.0),
- child: TrackTile(
- index: i,
- track: track,
- onTap: () async {
- if (playlist.activeTrack?.id == track.id) {
- return;
- }
- await playlistNotifier.jumpToTrack(track);
- },
- leadingActions: [
- ReorderableDragStartListener(
- index: i,
- child: const Icon(SpotubeIcons.dragHandle),
- ),
- ],
- ),
+ child: ReorderableListView.builder(
+ onReorder: (oldIndex, newIndex) {
+ playlistNotifier.moveTrack(oldIndex, newIndex);
+ },
+ scrollController: controller,
+ itemCount: tracks.length,
+ shrinkWrap: true,
+ buildDefaultDragHandles: false,
+ itemBuilder: (context, i) {
+ final track = tracks.elementAt(i);
+ return AutoScrollTag(
+ key: ValueKey(i),
+ controller: controller,
+ index: i,
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 8.0),
+ child: TrackTile(
+ index: i,
+ track: track,
+ onTap: () async {
+ if (playlist.activeTrack?.id == track.id) {
+ return;
+ }
+ await playlistNotifier.jumpToTrack(track);
+ },
+ leadingActions: [
+ ReorderableDragStartListener(
+ index: i,
+ child: const Icon(SpotubeIcons.dragHandle),
+ ),
+ ],
),
- );
- },
- ),
+ ),
+ );
+ },
),
)
else
Flexible(
child: InterScrollbar(
+ controller: controller,
child: ListView.builder(
+ controller: controller,
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
@@ -264,8 +268,8 @@ class PlayerQueue extends HookConsumerWidget {
),
),
],
- );
- }),
+ ),
+ ),
),
),
);
diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart
index 6587b8b39..cf1429b9f 100644
--- a/lib/components/player/sibling_tracks_sheet.dart
+++ b/lib/components/player/sibling_tracks_sheet.dart
@@ -1,5 +1,6 @@
import 'dart:ui';
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -11,13 +12,14 @@ import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
-import 'package:spotube/hooks/use_debounce.dart';
-import 'package:spotube/models/matched_track.dart';
-import 'package:spotube/models/spotube_track.dart';
+import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
-import 'package:spotube/provider/user_preferences_provider.dart';
-import 'package:spotube/provider/youtube_provider.dart';
-import 'package:spotube/services/youtube/youtube.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
+import 'package:spotube/services/sourced_track/models/source_info.dart';
+import 'package:spotube/services/sourced_track/models/video_info.dart';
+import 'package:spotube/services/sourced_track/sourced_track.dart';
+import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
@@ -34,7 +36,6 @@ class SiblingTracksSheet extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final preferences = ref.watch(userPreferencesProvider);
- final youtube = ref.watch(youtubeProvider);
final isSearching = useState(false);
final searchMode = useState(preferences.searchMode);
@@ -56,20 +57,35 @@ class SiblingTracksSheet extends HookConsumerWidget {
useValueListenable(searchController).text,
);
+ final controller = useScrollController();
+
final searchRequest = useMemoized(() async {
if (searchTerm.trim().isEmpty) {
- return [];
+ return [];
}
- return youtube.search(searchTerm.trim());
+ final results = await youtubeClient.search.search(searchTerm.trim());
+
+ return await Future.wait(
+ results.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async {
+ final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video);
+ return siblingType.info;
+ }),
+ );
}, [
searchTerm,
searchMode.value,
]);
- final siblings = playlist.isFetching == false
- ? (playlist.activeTrack as SpotubeTrack).siblings
- : [];
+ final siblings = useMemoized(
+ () => playlist.isFetching == false
+ ? [
+ (playlist.activeTrack as SourcedTrack).sourceInfo,
+ ...(playlist.activeTrack as SourcedTrack).siblings,
+ ]
+ : [],
+ [playlist.isFetching, playlist.activeTrack],
+ );
final borderRadius = floating
? BorderRadius.circular(10)
@@ -79,158 +95,166 @@ class SiblingTracksSheet extends HookConsumerWidget {
);
useEffect(() {
- if (playlist.activeTrack is SpotubeTrack &&
- (playlist.activeTrack as SpotubeTrack).siblings.isEmpty) {
+ if (playlist.activeTrack is SourcedTrack &&
+ (playlist.activeTrack as SourcedTrack).siblings.isEmpty) {
playlistNotifier.populateSibling();
}
return null;
}, [playlist.activeTrack]);
- final itemBuilder = useCallback((YoutubeVideoInfo video) {
- return ListTile(
- title: Text(video.title),
- leading: Padding(
- padding: const EdgeInsets.all(8.0),
- child: UniversalImage(
- path: video.thumbnailUrl,
- height: 60,
- width: 60,
+ final itemBuilder = useCallback(
+ (SourceInfo sourceInfo) {
+ return ListTile(
+ title: Text(sourceInfo.title),
+ leading: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: UniversalImage(
+ path: sourceInfo.thumbnail,
+ height: 60,
+ width: 60,
+ ),
),
- ),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(5),
- ),
- trailing: Text(video.duration.toHumanReadableString()),
- subtitle: Text(video.channelName),
- enabled: playlist.isFetching != true,
- selected: playlist.isFetching != true &&
- video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id,
- selectedTileColor: theme.popupMenuTheme.color,
- onTap: () {
- if (playlist.isFetching == false &&
- video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) {
- playlistNotifier.swapSibling(video);
- Navigator.of(context).pop();
- }
- },
- );
- }, [
- playlist.isFetching,
- playlist.activeTrack,
- siblings,
- ]);
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(5),
+ ),
+ trailing: Text(sourceInfo.duration.toHumanReadableString()),
+ subtitle: Text(sourceInfo.artist),
+ enabled: playlist.isFetching != true,
+ selected: playlist.isFetching != true &&
+ sourceInfo.id ==
+ (playlist.activeTrack as SourcedTrack).sourceInfo.id,
+ selectedTileColor: theme.popupMenuTheme.color,
+ onTap: () {
+ if (playlist.isFetching == false &&
+ sourceInfo.id !=
+ (playlist.activeTrack as SourcedTrack).sourceInfo.id) {
+ playlistNotifier.swapSibling(sourceInfo);
+ Navigator.of(context).pop();
+ }
+ },
+ );
+ },
+ [playlist.isFetching, playlist.activeTrack, siblings],
+ );
var mediaQuery = MediaQuery.of(context);
return SafeArea(
- child: BackdropFilter(
- filter: ImageFilter.blur(
- sigmaX: 12.0,
- sigmaY: 12.0,
- ),
- child: AnimatedSize(
- duration: const Duration(milliseconds: 300),
- child: Container(
- height: isSearching.value && mediaQuery.smAndDown
- ? mediaQuery.size.height
- : mediaQuery.size.height * .6,
- margin: const EdgeInsets.all(8.0),
- decoration: BoxDecoration(
- borderRadius: borderRadius,
- color: theme.scaffoldBackgroundColor.withOpacity(.3),
- ),
- child: Scaffold(
- backgroundColor: Colors.transparent,
- appBar: AppBar(
- centerTitle: true,
- title: AnimatedSwitcher(
- duration: const Duration(milliseconds: 300),
- child: !isSearching.value
- ? Text(
- context.l10n.alternative_track_sources,
- style: theme.textTheme.headlineSmall,
- )
- : TextField(
- autofocus: true,
- controller: searchController,
- decoration: InputDecoration(
- hintText: context.l10n.search,
- hintStyle: theme.textTheme.headlineSmall,
- border: InputBorder.none,
+ child: ClipRRect(
+ borderRadius: borderRadius,
+ clipBehavior: Clip.hardEdge,
+ child: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 12.0,
+ sigmaY: 12.0,
+ ),
+ child: AnimatedSize(
+ duration: const Duration(milliseconds: 300),
+ child: Container(
+ height: isSearching.value && mediaQuery.smAndDown
+ ? mediaQuery.size.height - 50
+ : mediaQuery.size.height * .6,
+ decoration: BoxDecoration(
+ borderRadius: borderRadius,
+ color: theme.colorScheme.surfaceVariant.withOpacity(.5),
+ ),
+ child: Scaffold(
+ backgroundColor: Colors.transparent,
+ appBar: AppBar(
+ centerTitle: true,
+ title: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: !isSearching.value
+ ? Text(
+ context.l10n.alternative_track_sources,
+ style: theme.textTheme.headlineSmall,
+ )
+ : TextField(
+ autofocus: true,
+ controller: searchController,
+ decoration: InputDecoration(
+ hintText: context.l10n.search,
+ hintStyle: theme.textTheme.headlineSmall,
+ border: InputBorder.none,
+ ),
+ style: theme.textTheme.headlineSmall,
),
- style: theme.textTheme.headlineSmall,
+ ),
+ automaticallyImplyLeading: false,
+ backgroundColor: Colors.transparent,
+ actions: [
+ if (!isSearching.value)
+ IconButton(
+ icon: const Icon(SpotubeIcons.search, size: 18),
+ onPressed: () {
+ isSearching.value = true;
+ },
+ )
+ else ...[
+ if (preferences.audioSource == AudioSource.piped)
+ PopupMenuButton(
+ icon: const Icon(SpotubeIcons.filter, size: 18),
+ onSelected: (SearchMode mode) {
+ searchMode.value = mode;
+ },
+ initialValue: searchMode.value,
+ itemBuilder: (context) => SearchMode.values
+ .map(
+ (e) => PopupMenuItem(
+ value: e,
+ child: Text(e.label),
+ ),
+ )
+ .toList(),
),
- ),
- automaticallyImplyLeading: false,
- backgroundColor: Colors.transparent,
- actions: [
- if (!isSearching.value)
- IconButton(
- icon: const Icon(SpotubeIcons.search, size: 18),
- onPressed: () {
- isSearching.value = true;
- },
- )
- else ...[
- if (preferences.youtubeApiType == YoutubeApiType.piped)
- PopupMenuButton(
- icon: const Icon(SpotubeIcons.filter, size: 18),
- onSelected: (SearchMode mode) {
- searchMode.value = mode;
+ IconButton(
+ icon: const Icon(SpotubeIcons.close, size: 18),
+ onPressed: () {
+ isSearching.value = false;
},
- initialValue: searchMode.value,
- itemBuilder: (context) => SearchMode.values
- .map(
- (e) => PopupMenuItem(
- value: e,
- child: Text(e.label),
- ),
- )
- .toList(),
),
- IconButton(
- icon: const Icon(SpotubeIcons.close, size: 18),
- onPressed: () {
- isSearching.value = false;
+ ]
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ transitionBuilder: (child, animation) =>
+ FadeTransition(opacity: animation, child: child),
+ child: InterScrollbar(
+ controller: controller,
+ child: switch (isSearching.value) {
+ false => ListView.builder(
+ controller: controller,
+ itemCount: siblings.length,
+ itemBuilder: (context, index) =>
+ itemBuilder(siblings[index]),
+ ),
+ true => FutureBuilder(
+ future: searchRequest,
+ builder: (context, snapshot) {
+ if (snapshot.hasError) {
+ return Center(
+ child: Text(snapshot.error.toString()),
+ );
+ } else if (!snapshot.hasData) {
+ return const Center(
+ child: CircularProgressIndicator());
+ }
+
+ return InterScrollbar(
+ controller: controller,
+ child: ListView.builder(
+ controller: controller,
+ itemCount: snapshot.data!.length,
+ itemBuilder: (context, index) =>
+ itemBuilder(snapshot.data![index]),
+ ),
+ );
+ },
+ ),
},
),
- ]
- ],
- ),
- body: Padding(
- padding: const EdgeInsets.all(8.0),
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 300),
- transitionBuilder: (child, animation) =>
- FadeTransition(opacity: animation, child: child),
- child: InterScrollbar(
- child: switch (isSearching.value) {
- false => ListView.builder(
- itemCount: siblings.length,
- itemBuilder: (context, index) =>
- itemBuilder(siblings[index]),
- ),
- true => FutureBuilder(
- future: searchRequest,
- builder: (context, snapshot) {
- if (snapshot.hasError) {
- return Center(
- child: Text(snapshot.error.toString()),
- );
- } else if (!snapshot.hasData) {
- return const Center(
- child: CircularProgressIndicator());
- }
-
- return InterScrollbar(
- child: ListView.builder(
- itemCount: snapshot.data!.length,
- itemBuilder: (context, index) =>
- itemBuilder(snapshot.data![index]),
- ),
- );
- },
- ),
- },
),
),
),
diff --git a/lib/hooks/use_progress.dart b/lib/components/player/use_progress.dart
similarity index 100%
rename from lib/hooks/use_progress.dart
rename to lib/components/player/use_progress.dart
diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart
index 0438e559d..f429a0ab9 100644
--- a/lib/components/playlist/playlist_card.dart
+++ b/lib/components/playlist/playlist_card.dart
@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
+import 'package:spotube/extensions/infinite_query.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@@ -23,7 +24,7 @@ class PlaylistCard extends HookConsumerWidget {
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
- final queryBowl = QueryClient.of(context);
+ final queryClient = QueryClient.of(context);
final tracks = useState?>(null);
bool isPlaylistPlaying = useMemoized(
() => playlistQueue.containsCollection(playlist.id!),
@@ -34,6 +35,31 @@ class PlaylistCard extends HookConsumerWidget {
final spotify = ref.watch(spotifyProvider);
final me = useQueries.user.me(ref);
+ Future> fetchAllTracks() async {
+ if (playlist.id == 'user-liked-tracks') {
+ return await queryClient.fetchQuery(
+ "user-liked-tracks",
+ () => useQueries.playlist.likedTracks(spotify),
+ ) ??
+ [];
+ }
+
+ final query = queryClient.createInfiniteQuery, dynamic, int>(
+ "playlist-tracks/${playlist.id}",
+ (page) => useQueries.playlist.tracksOf(page, spotify, playlist.id!),
+ initialPage: 0,
+ nextPage: useQueries.playlist.tracksOfQueryNextPage,
+ );
+
+ return await query.fetchAllTracks(
+ getAllTracks: () async {
+ final res =
+ await spotify.playlists.getTracksByPlaylistId(playlist.id!).all();
+ return res.toList();
+ },
+ );
+ }
+
return PlaybuttonCard(
margin: const EdgeInsets.symmetric(horizontal: 10),
title: playlist.name!,
@@ -62,18 +88,7 @@ class PlaylistCard extends HookConsumerWidget {
return audioPlayer.resume();
}
- List