diff --git a/.circleci/config.yml b/.circleci/config.yml
index a5c71033..a55310ce 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -82,8 +82,6 @@ jobs:
name: Generate .env file
command: |
echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env
- echo "SUPABASE_URL=${SUPABASE_URL}" >> .env
- echo "SUPABASE_API_KEY=${SUPABASE_API_KEY}" >> .env
- run:
name: Replace Version in files
diff --git a/.env.example b/.env.example
index 67d1be8e..22abd24b 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,3 @@
-SUPABASE_URL=
-SUPABASE_API_KEY=
-
# The format:
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
SPOTIFY_SECRETS=
diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json
index ba129cfd..f1f9ceed 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 d461e296..d57cc0e8 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 3710d812..1544f055 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,45 @@
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
+
+* "Add () to Playlist" option not showing in favorited playlists [#904](https://github.com/KRTirtho/spotube/issues/904) ([96021e1](https://github.com/KRTirtho/spotube/commit/96021e1a49d22bd25fd052c122f49f439c2bea43))
+* 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 11206e6d..b2823e62 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 71589794..498c45de 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😉
@@ -108,7 +108,7 @@ This handy table lists all methods you can use to install Spotube:
-
Then run: sudo apt install Spotube-linux-x86_64.deb
+ Then run: sudo apt install ./Spotube-linux-x86_64.deb
@@ -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 43e1e53d..f8975335 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 1b9de3de..50fe1e6a 100644
--- a/lib/collections/env.dart
+++ b/lib/collections/env.dart
@@ -1,15 +1,10 @@
import 'package:envied/envied.dart';
+import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
part 'env.g.dart';
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
abstract class Env {
- @EnviedField(varName: 'SUPABASE_URL')
- static final String? supabaseUrl = _Env.supabaseUrl;
-
- @EnviedField(varName: 'SUPABASE_API_KEY')
- static final String? supabaseAnonKey = _Env.supabaseAnonKey;
-
@EnviedField(varName: 'SPOTIFY_SECRETS')
static final String rawSpotifySecrets = _Env.rawSpotifySecrets;
@@ -30,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 8c7ea73b..abccb3ad 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 2a742f46..d89e1a2a 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 81ebb3e6..82597ddb 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 5c769498..d00775c7 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 d8f8d85b..c7ae2f9a 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(),
- );
+ final fetchedTracks = await fetchAllTrack();
- if (fetchedTracks == null || fetchedTracks.isEmpty) return;
+ 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 8fa9be87..5114170c 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 993e9f6a..434b90ad 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 b9783f87..f2b183f4 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 42654ed9..7f580157 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 ae8a2513..10dec410 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 50ae64be..cc8b10cf 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 8ed3e73d..f7736ca7 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 811d24c5..889b7c5c 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 b3a1e340..7a248aa5 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 07a6b7ba..1000af18 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 354d1a36..4869a0fa 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 725af22b..8142740c 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 6587b8b3..cf1429b9 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,
- ),
- style: theme.textTheme.headlineSmall,
- ),
- ),
- automaticallyImplyLeading: false,
+ 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,
- 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;
+ 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,
+ ),
+ ),
+ 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(),
+ ),
+ 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 0438e559..f429a0ab 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