diff --git a/.env.example b/.env.example
index 35c5d563..bbab9bb8 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,3 @@
-# The format:
-# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
-SPOTIFY_SECRETS=$SPOTIFY_SECRETS
-
# 0 or 1
# 0 = disable
# 1 = enable
@@ -13,5 +9,4 @@ LASTFM_API_SECRET=$LASTFM_API_SECRET
# Release channel. Can be: nightly, stable
RELEASE_CHANNEL=$RELEASE_CHANNEL
-HIDE_DONATIONS=$HIDE_DONATIONS
-DISABLE_SPOTIFY_IMAGES=$DISABLE_SPOTIFY_IMAGES
+HIDE_DONATIONS=$HIDE_DONATIONS
\ No newline at end of file
diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json
index b4cb4720..58b893ee 100644
--- a/.fvm/fvm_config.json
+++ b/.fvm/fvm_config.json
@@ -1,3 +1,3 @@
{
- "flutterSdkVersion": "3.29.2"
+ "flutterSdkVersion": "3.35.2"
}
\ No newline at end of file
diff --git a/.fvmrc b/.fvmrc
index 5b0ad692..cf986e39 100644
--- a/.fvmrc
+++ b/.fvmrc
@@ -1,4 +1,4 @@
{
- "flutter": "3.29.2",
+ "flutter": "3.35.2",
"flavors": {}
}
\ No newline at end of file
diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml
index 245101d8..3e73be4d 100644
--- a/.github/workflows/pr-lint.yml
+++ b/.github/workflows/pr-lint.yml
@@ -4,7 +4,7 @@ on:
pull_request:
env:
- FLUTTER_VERSION: 3.29.2
+ FLUTTER_VERSION: 3.35.2
jobs:
lint:
@@ -21,7 +21,6 @@ jobs:
run: |
envsubst < .env.example > .env
env:
- SPOTIFY_SECRETS: xxx:xxx
ENABLE_UPDATE_CHECK: true
LASTFM_API_KEY: xxx
LASTFM_API_SECRET: xxx
diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index 05a672d4..4f2cff34 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -20,7 +20,7 @@ on:
description: Dry run without uploading to release
env:
- FLUTTER_VERSION: 3.29.2
+ FLUTTER_VERSION: 3.35.2
FLUTTER_CHANNEL: master
permissions:
@@ -56,7 +56,7 @@ jobs:
files: |
dist/Spotube-windows-x86_64.nupkg
dist/Spotube-windows-x86_64-setup.exe
- - os: macos-latest
+ - os: macos-14
platform: ios
arch: all
files: |
@@ -95,7 +95,7 @@ jobs:
if: ${{matrix.platform == 'ios'}}
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: "16.1"
+ xcode-version: "16.2"
- name: Install ${{matrix.platform}} dependencies
run: |
diff --git a/.metadata b/.metadata
index 828f2c0a..e8b36fde 100644
--- a/.metadata
+++ b/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: "300451adae589accbece3490f4396f10bdf15e6e"
+ revision: "d7b523b356d15fb81e7d340bbe52b47f93937323"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: 300451adae589accbece3490f4396f10bdf15e6e
- base_revision: 300451adae589accbece3490f4396f10bdf15e6e
+ create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
+ base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
- platform: windows
- create_revision: 300451adae589accbece3490f4396f10bdf15e6e
- base_revision: 300451adae589accbece3490f4396f10bdf15e6e
+ create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
+ base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323
# User provided section
diff --git a/.vscode/launch.json b/.vscode/launch.json
index deabf1d3..b81e2eee 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -17,6 +17,17 @@
"dev"
]
},
+ {
+ "name": "spotube (mobile-skia)",
+ "type": "dart",
+ "request": "launch",
+ "program": "lib/main.dart",
+ "args": [
+ "--flavor",
+ "dev",
+ "--no-enable-impeller"
+ ]
+ },
{
"name": "spotube (profile)",
"type": "dart",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8ae9c74f..69c80bb3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,6 +5,7 @@
"ambiguate",
"Amoled",
"Buildless",
+ "configurators",
"danceability",
"fuzzywuzzy",
"gapless",
@@ -29,5 +30,5 @@
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
"*.dart": "${capture}.g.dart,${capture}.freezed.dart"
},
- "dart.flutterSdkPath": ".fvm/versions/3.29.2"
+ "dart.flutterSdkPath": ".fvm/versions/3.35.2"
}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 48626312..49ae034a 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ tar:
mkdir -p $(TEMP_DIR)\
&& cp -r $(BUNDLE_DIR)/* $(TEMP_DIR)\
&& cp linux/spotube.desktop $(TEMP_DIR)\
- && cp assets/spotube-logo.png $(TEMP_DIR)\
+ && cp assets/branding/spotube-logo.png $(TEMP_DIR)\
&& cp linux/com.github.KRTirtho.Spotube.appdata.xml $(TEMP_DIR)\
&& tar -cJf build/spotube-linux-${VERSION}-${PKG_ARCH}.tar.xz -C $(TEMP_DIR) .\
&& rm -rf $(TEMP_DIR)
@@ -52,4 +52,7 @@ dmg:
if [ -f dist/Spotube-macos-universal.dmg ];\
then rm dist/Spotube-macos-universal.dmg;\
fi &&\
- appdmg appdmg.json dist/Spotube-macos-universal.dmg
\ No newline at end of file
+ appdmg appdmg.json dist/Spotube-macos-universal.dmg
+
+changelog:
+ git-cliff --unreleased
\ No newline at end of file
diff --git a/README.md b/README.md
index 26bb1d71..39f17945 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,8 @@
-# 🚨 Spotube is banned from using "Spotify™ API" 🚨
-
-### The developer of Spotube has received a cease and desist letter from Spotify USA Inc. and Spotify AB, asserting a legal threat concerning the distribution and development of any application that utilizes Spotify’s data API in conjunction with content from YouTube® to facilitate ad-free playback of music tracks. The letter contends that this specific use of the Spotify™ APIs contravenes the Spotify™ Agreements and may also infringe upon the rights of music rights holders.
-
-### Consequently, as the official maintainer of Spotube, I will immediately cease all forms of official distribution and development of Spotube that continue to employ the aforementioned 'Spotify™ APIs'
-
-### Their exact reasoning : (any) "uses of Spotify’s data API in connection with content from YouTube to provide ad-free playback of music tracks. The use of the Spotify APIs in this manner violates the Spotify Agreements and may also violate the rights of music rights holders."
-
-## So what's now?
-
-> In short, we are cooked (legally)
-
-For now, I've to:
-
-1. Stop distributing/developing Spotube/any app that uses "Spotify™ APIs"
-
- That means, I can no longer distribute Spotube through the website, GitHub, any app store and immediately have to take down the versions that uses Spotify™ APIs.
-
-1. Stop using their logo/image/name/intellectual property in a manner that "seems infringement"
-1. Forever desist from aiding or assisting any other person or entity in the activities described above
-
----
-
-**For the users of Spotube:**
-
-Don't worry, Spotube is banned only from (or assisting other) using those APIs. As long as the app isn't using them or no way helps anyone else to use them, it's ok.
-
-In future, I'll try to rewrite Spotube to ensure it operates within the bounds of copyright law and platform policies. And give ways for the users to extend the app to their use cases. Work is already in progress to implement this! So expect some big updates soon!
-
-But for eternity, you can't download versions of Spotube that still uses "Spotify™ APIs" from official means (website/Github/app stores). Those will be taken down.
-
-**But newer version of Spotube that _doesn't_ use "Spotify™ APIs" will be available to replace those.**
-
-That means, in the upcoming new versions, you will no longer be able to login with your "Spotify™ Account", access your saved playlists, albums, tracks, followed artists or perform any action on that account or anything that is from "Spotify™" or owned by "Spotify™" (yes the API public data (e.g. track metadata) as well) through Spotube.
-
-**Conclusion:** I'm extremely sorry for this disruption to your day to day music listening experience. Spotube existed and it used by a large number of users because they find it better. And we'll continue to be better than others but legally\* from now on
-
-> Spotube has no affiliation with Spotify™ or any of its subsidiaries.
-
-
+
-An open source, cross-platform music client
-utilizing selected music provider API and YouTube®, Piped.video or JioSaavn as an audio source
+A cross-platform extensible open-source music streaming platform.
+Bring your own music metadata/playlist with plugins created by community or by yourself. A small step towards the decentralized music streaming era!
Btw it's not just another Electron app 😉
@@ -61,30 +22,316 @@ Btw it's not just another Electron app 😉
## 🌃 Features
-- 🚫 No ads, thanks to the use of public & free music metadata providers and YT Music APIs¹
-- ⬇️ Freely downloadable tracks
-- 🖥️ 📱 Cross-platform support
-- 🪶 Small size & less data usage
-- 🕵️ Anonymous/guest login
-- 🕒 Time synced lyrics
-- ✋ No telemetry, diagnostics or user data collection
-- 🚀 Native performance
-- 📖 Open source/libre software
-- 🔉 Playback control is done locally, not on the server
+- 🧩 Plugin powered, supports any platform or custom music service through plugins.
+- 🗺️ Community driven plugins for popular platforms or create your own.
+- ⬇️ Freely downloadable tracks with tagged metadata.
+- 🖥️ 📱 Cross-platform support.
+- 🪶 Small size & less data usage.
+- 🕒 Time synced lyrics regardless of the plugin support.
+- ✋ No telemetry, diagnostics or user data collection.
+- 🚀 Native performance.
+- 📖 Open source/libre software.
+- 🔉 Playback control is done locally, not on the server.
-**¹** It is still **recommended** to support creators by engaging with their YouTube channels/tracks in music platforms (or preferably by buying their merch/concert tickets/physical media).
+## 📜 ⬇️ Installation guide
-### ❌ Unsupported features
+New versions usually release every 3-4 months.
+This handy table lists all the methods you can use to install Spotube:
-- 🗣️ **Shows & Podcasts:** Shows and Podcasts will
**never be supported** because the audio tracks are
_only_ available on music providers and accessing them would require premium.
-- 🎧 **Listen Along:** [Coming soon!](https://github.com/KRTirtho/spotube/issues/8)
+
+
+ Platform
+ Package/Installation Method
+
+
+ Windows
+
+
+
+
+
+
+ MacOS
+
+
+
+
+
+
+
+ Android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ iOS
+
+
+
+
+
+
+ *iPA file only. Requires sideloading with AltStore or similar tools.
+
+
+
+
+ Flatpak
+
+ flatpak install com.github.KRTirtho.Spotube
+
+
+
+
+
+
+ AppImage
+ AppImage's lacking stability led to it's temporary removal. More information at https://github.com/KRTirtho/spotube/issues/1082
+
+
+ Debian/Ubuntu
+
+
+
+
+ Then run: sudo apt install ./Spotube-linux-x86_64.deb
+
+
+
+ Arch/Manjaro
+
+ With pamac: sudo pamac install spotube-bin
+ With yay: yay -Sy spotube-bin
+
+
+
+ Fedora/OpenSuse
+
+
+
+
+ For Fedora: sudo dnf install ./Spotube-linux-x86_64.rpm
+ For OpenSuse: sudo zypper in ./Spotube-linux-x86_64.rpm
+
+
+
+ Linux (tarball)
+
+
+
+
+
+
+
+ Macos - Homebrew
+
+
+brew tap krtirtho/apps
+brew install --cask spotube
+
+
+
+
+ Windows - Chocolatey
+
+ choco install spotube
+
+
+
+ Windows - Scoop
+
+ scoop bucket add extras
+ scoop install spotube
+
+
+
+ Windows - WinGet
+
+ winget install --id KRTirtho.Spotube
+
+
+
+
+### 🔄 Nightly Builds
+
+Grab the latest nightly builds of Spotube [from the GitHub Releases](https://github.com/KRTirtho/spotube/releases/tag/nightly).
+
+## 🕳️ Building from source
+
+
+
+You can compile Spotube's source code by [following these instructions](CONTRIBUTION.md#your-first-code-contribution).
## 👥 The Spotube team
- [Kingkor Roy Tirtho](https://github.com/KRTirtho) - The Founder, Maintainer and Lead Developer
+- [Owen Connor](https://github.com/owencz1998) - The Cool Discord Moderator
+- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
+- [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy
## 💼 License
Spotube is open source and licensed under the [BSD-4-Clause](/LICENSE) License.
-If you are concerned, you can [read the reason of choosing this license](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p).
+If you are curious, you can [read the reason of choosing this license](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p).
+
+
+
+ [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. [MPV](https://mpv.io) - mpv is a free (as in freedom) media player for the command line. It supports a wide variety of media file formats, audio and video codecs, and subtitle types.
+1. [Musicbrainz](https://musicbrainz.org) - MusicBrainz is a MetaBrainz project that aims to create a collaborative music database that is similar to the freedb project.
+1. [Listenbrainz](https://listenbrainz.org) - ListenBrainz is a open-source project by the MetaBrainz Foundation that allows users to crowdsource and publicly store their digital music listening data.
+1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
+1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube.
+1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader.
+1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites.
+1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
+1. [LRCLib](https://lrclib.net/) - A public synced lyric API.
+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
+1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
+
+### Dependencies
+
+1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
+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.
+1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
+1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
+1. [auto_route](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you.
+1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
+1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
+1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
+1. [connectivity_plus](https://github.com/fluttercommunity/plus_plugins) - Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS.
+1. [device_info_plus](https://github.com/fluttercommunity/plus_plugins) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on.
+1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.
+1. [drift](https://drift.simonbinder.eu/) - Drift is a reactive library to store relational data in Dart and Flutter applications.
+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. [encrypt](https://pub.dev/packages/encrypt) - A set of high-level APIs over PointyCastle for two-way cryptography.
+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_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. [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. [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_discord_rpc](https://pub.dev/packages/flutter_discord_rpc) - Discord RPC support for Flutter desktop platforms
+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.
+1. [flutter_form_builder](https://github.com/flutter-form-builder-ecosystem) - This package helps in creation of forms in Flutter by removing the boilerplate code, reusing validation, react to changes, and collect final user input.
+1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
+1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
+1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
+1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
+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_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
+1. [flutter_undraw](https://github.com/KRTirtho/flutter_undraw) - Undraw.co Illustrations for Flutter with customization options
+1. [form_builder_validators](https://github.com/flutter-form-builder-ecosystem) - Form Builder Validators set of validators for FlutterFormBuilder. Provides common validators and a way to make your own.
+1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
+1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
+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. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
+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. [home_widget](https://pub.dev/packages/home_widget) - A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS.
+1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
+1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
+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. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
+1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
+1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
+1. [invidious](https://pub.dev/packages/invidious) - Invidious API client for Dart and Flutter.
+1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
+1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
+1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications.
+1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
+1. [logging](https://pub.dev/packages/logging) - Provides APIs for debugging and error logging, similar to loggers in other languages, such as the Closure JS Logger and java.util.logging.Logger.
+1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
+1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
+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://pub.dev/packages/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. [open_file](https://pub.dev/packages/open_file) - A plug-in that can call native APP to open files with string result in flutter, support iOS(UTI) / android(intent) / PC(ffi) / web(dart:html)
+1. [package_info_plus](https://github.com/fluttercommunity/plus_plugins) - 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. [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. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
+1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
+1. [shadcn_flutter](https://github.com/sunarya-thito/shadcn_flutter) - Beautifully designed components from Shadcn/UI is now available for Flutter
+1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
+1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
+1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
+1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
+1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
+1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
+1. [sliding_up_panel](https://github.com/akshathjain/sliding_up_panel) - A draggable Flutter widget that makes implementing a SlidingUpPanel much easier!
+1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
+1. [smtc_windows](https://pub.dev/packages/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
+1. [sqlite3](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3) - Provides lightweight yet convenient bindings to SQLite by using dart:ffi
+1. [sqlite3_flutter_libs](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs) - Flutter plugin to include native sqlite3 libraries with your app
+1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
+1. [system_theme](https://github.com/bdlukaa/system_theme/tree/master/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
+1. [test](https://pub.dev/packages/test) - A full featured library for writing and running Dart tests across platforms.
+1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime.
+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. [tray_manager](https://github.com/leanflutter/tray_manager) - This plugin allows Flutter desktop apps to defines system tray.
+1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.
+1. [uuid](https://pub.dev/packages/uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart
+1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/
+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. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
+1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel.
+1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
+1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
+1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
+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. [http_parser](https://pub.dev/packages/http_parser) - A platform-independent package for parsing and serializing HTTP formats.
+1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
+1. [otp_util](https://github.com/dushiling) - otp_util is a dart package to generate and verify one-time passwords,it It provides two methods TOPT and HOTP.They are Time-based OTPs and Counter-based OTPs.
+1. [dio_http2_adapter](https://github.com/cfug/dio) - An adapter that combines HTTP/2 and dio. Supports reusing connections, header compression, etc.
+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_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
+1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
+1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
+1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
+1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
+1. [process_run](https://github.com/tekartik/process_run.dart/blob/master/packages/process_run) - Process run helpers for Linux/Win/Mac and which like feature for finding executables.
+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. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
+1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents.
+1. [io](https://pub.dev/packages/io) - Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values.
+1. [drift_dev](https://drift.simonbinder.eu/) - Dev-dependency for users of drift. Contains the generator and development tools.
+1. [auto_route_generator](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you.
+1. [desktop_webview_window](https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window) - Show a webview window on your flutter desktop application.
+1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc
+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. [flutter_broadcasts](https://github.com/KRTirtho/flutter_broadcasts.git) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
+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. [yt_dlp_dart](https://github.com/KRTirtho/yt_dlp_dart.git) - yt-dlp binding in Dart
+1. [flutter_new_pipe_extractor](https://github.com/KRTirtho/flutter_new_pipe_extractor) - NewPipeExtractor binding for Flutter (Android only)
+
+
+
© Copyright Spotube 2025
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5051f5a3..ee481eca 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -33,7 +33,7 @@ def composeVersion = "1.4.8"
android {
namespace "oss.krtirtho.spotube"
- compileSdkVersion 35
+ compileSdkVersion 36
ndkVersion = "27.0.12077973"
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index 17a13e9d..1f5a556c 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -1,3 +1,12 @@
+#Flutter Wrapper
+# -keep class io.flutter.app.** { *; }
+-keep class io.flutter.plugin.** { *; }
+-keep class io.flutter.util.** { *; }
+-keep class io.flutter.view.** { *; }
+# -keep class io.flutter.** { *; }
+-keep class io.flutter.plugins.** { *; }
+-keep class de.prosiebensat1digital.** { *; }
+
-keep class androidx.lifecycle.DefaultLifecycleObserver
-keepnames class kotlinx.serialization.** { *; }
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0effefe2..a005257e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -57,10 +57,6 @@
-
-
@@ -74,8 +70,6 @@
-
-
diff --git a/appdmg.json b/appdmg.json
index eb9b5236..6e365f23 100644
--- a/appdmg.json
+++ b/appdmg.json
@@ -1,6 +1,6 @@
{
"title": "Spotube",
- "icon": "assets/spotube-logo-macos.png",
+ "icon": "assets/branding/spotube-logo-macos.png",
"contents": [
{
"x": 448,
@@ -15,4 +15,4 @@
"path": "build/macos/Build/Products/Release/Spotube.app"
}
]
-}
\ No newline at end of file
+}
diff --git a/assets/backgrounds/xmas-effect.png b/assets/backgrounds/xmas-effect.png
deleted file mode 100644
index e7c8eeef..00000000
Binary files a/assets/backgrounds/xmas-effect.png and /dev/null differ
diff --git a/assets/bengali-patterns-bg.jpg b/assets/bengali-patterns-bg.jpg
deleted file mode 100644
index 513557a3..00000000
Binary files a/assets/bengali-patterns-bg.jpg and /dev/null differ
diff --git a/assets/branding.png b/assets/branding/branding.png
similarity index 100%
rename from assets/branding.png
rename to assets/branding/branding.png
diff --git a/assets/mobile-screenshots/android-1.jpg b/assets/branding/mobile-screenshots/android-1.jpg
similarity index 100%
rename from assets/mobile-screenshots/android-1.jpg
rename to assets/branding/mobile-screenshots/android-1.jpg
diff --git a/assets/mobile-screenshots/android-2.jpg b/assets/branding/mobile-screenshots/android-2.jpg
similarity index 100%
rename from assets/mobile-screenshots/android-2.jpg
rename to assets/branding/mobile-screenshots/android-2.jpg
diff --git a/assets/mobile-screenshots/android-3.jpg b/assets/branding/mobile-screenshots/android-3.jpg
similarity index 100%
rename from assets/mobile-screenshots/android-3.jpg
rename to assets/branding/mobile-screenshots/android-3.jpg
diff --git a/assets/mobile-screenshots/android-4.jpg b/assets/branding/mobile-screenshots/android-4.jpg
similarity index 100%
rename from assets/mobile-screenshots/android-4.jpg
rename to assets/branding/mobile-screenshots/android-4.jpg
diff --git a/assets/mobile-screenshots/android-5.jpg b/assets/branding/mobile-screenshots/android-5.jpg
similarity index 100%
rename from assets/mobile-screenshots/android-5.jpg
rename to assets/branding/mobile-screenshots/android-5.jpg
diff --git a/assets/mobile-screenshots/android-6.jpg b/assets/branding/mobile-screenshots/android-6.jpg
similarity index 100%
rename from assets/mobile-screenshots/android-6.jpg
rename to assets/branding/mobile-screenshots/android-6.jpg
diff --git a/assets/mobile-screenshots/combined.png b/assets/branding/mobile-screenshots/combined.png
similarity index 100%
rename from assets/mobile-screenshots/combined.png
rename to assets/branding/mobile-screenshots/combined.png
diff --git a/assets/spotube-hero-banner.png b/assets/branding/spotube-hero-banner.png
similarity index 100%
rename from assets/spotube-hero-banner.png
rename to assets/branding/spotube-hero-banner.png
diff --git a/assets/spotube-logo-foreground.png b/assets/branding/spotube-logo-foreground.png
similarity index 100%
rename from assets/spotube-logo-foreground.png
rename to assets/branding/spotube-logo-foreground.png
diff --git a/assets/spotube-logo-item.png b/assets/branding/spotube-logo-item.png
similarity index 100%
rename from assets/spotube-logo-item.png
rename to assets/branding/spotube-logo-item.png
diff --git a/assets/spotube-logo-light.png b/assets/branding/spotube-logo-light.png
similarity index 100%
rename from assets/spotube-logo-light.png
rename to assets/branding/spotube-logo-light.png
diff --git a/assets/spotube-logo-macos.png b/assets/branding/spotube-logo-macos.png
similarity index 100%
rename from assets/spotube-logo-macos.png
rename to assets/branding/spotube-logo-macos.png
diff --git a/assets/spotube-logo.bmp b/assets/branding/spotube-logo.bmp
similarity index 100%
rename from assets/spotube-logo.bmp
rename to assets/branding/spotube-logo.bmp
diff --git a/assets/spotube-logo.ico b/assets/branding/spotube-logo.ico
similarity index 100%
rename from assets/spotube-logo.ico
rename to assets/branding/spotube-logo.ico
diff --git a/assets/spotube-logo.png b/assets/branding/spotube-logo.png
similarity index 100%
rename from assets/spotube-logo.png
rename to assets/branding/spotube-logo.png
diff --git a/assets/spotube-logo_android12.png b/assets/branding/spotube-logo_android12.png
similarity index 100%
rename from assets/spotube-logo_android12.png
rename to assets/branding/spotube-logo_android12.png
diff --git a/assets/spotube-nightly-item.png b/assets/branding/spotube-nightly-item.png
similarity index 100%
rename from assets/spotube-nightly-item.png
rename to assets/branding/spotube-nightly-item.png
diff --git a/assets/spotube-nightly-logo-foreground.png b/assets/branding/spotube-nightly-logo-foreground.png
similarity index 100%
rename from assets/spotube-nightly-logo-foreground.png
rename to assets/branding/spotube-nightly-logo-foreground.png
diff --git a/assets/spotube-nightly-logo-foreground.svg b/assets/branding/spotube-nightly-logo-foreground.svg
similarity index 100%
rename from assets/spotube-nightly-logo-foreground.svg
rename to assets/branding/spotube-nightly-logo-foreground.svg
diff --git a/assets/spotube-nightly-logo.png b/assets/branding/spotube-nightly-logo.png
similarity index 100%
rename from assets/spotube-nightly-logo.png
rename to assets/branding/spotube-nightly-logo.png
diff --git a/assets/spotube-nightly-logo_android12.png b/assets/branding/spotube-nightly-logo_android12.png
similarity index 100%
rename from assets/spotube-nightly-logo_android12.png
rename to assets/branding/spotube-nightly-logo_android12.png
diff --git a/assets/spotube-screenshot.png b/assets/branding/spotube-screenshot.png
similarity index 100%
rename from assets/spotube-screenshot.png
rename to assets/branding/spotube-screenshot.png
diff --git a/assets/spotube-tall-capsule.png b/assets/branding/spotube-tall-capsule.png
similarity index 100%
rename from assets/spotube-tall-capsule.png
rename to assets/branding/spotube-tall-capsule.png
diff --git a/assets/spotube-wide-capsule-large.png b/assets/branding/spotube-wide-capsule-large.png
similarity index 100%
rename from assets/spotube-wide-capsule-large.png
rename to assets/branding/spotube-wide-capsule-large.png
diff --git a/assets/spotube-wide-capsule-small.png b/assets/branding/spotube-wide-capsule-small.png
similarity index 100%
rename from assets/spotube-wide-capsule-small.png
rename to assets/branding/spotube-wide-capsule-small.png
diff --git a/assets/spotube_banner.png b/assets/branding/spotube_banner.png
similarity index 100%
rename from assets/spotube_banner.png
rename to assets/branding/spotube_banner.png
diff --git a/assets/empty_box.png b/assets/empty_box.png
deleted file mode 100644
index 24e95b23..00000000
Binary files a/assets/empty_box.png and /dev/null differ
diff --git a/assets/fonts/Ubuntu_Mono/UFL.txt b/assets/fonts/Ubuntu_Mono/UFL.txt
new file mode 100644
index 00000000..6e722c88
--- /dev/null
+++ b/assets/fonts/Ubuntu_Mono/UFL.txt
@@ -0,0 +1,96 @@
+-------------------------------
+UBUNTU FONT LICENCE Version 1.0
+-------------------------------
+
+PREAMBLE
+This licence allows the licensed fonts to be used, studied, modified and
+redistributed freely. The fonts, including any derivative works, can be
+bundled, embedded, and redistributed provided the terms of this licence
+are met. The fonts and derivatives, however, cannot be released under
+any other licence. The requirement for fonts to remain under this
+licence does not require any document created using the fonts or their
+derivatives to be published under this licence, as long as the primary
+purpose of the document is not to be a vehicle for the distribution of
+the fonts.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this licence and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Original Version" refers to the collection of Font Software components
+as received under this licence.
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to
+a new environment.
+
+"Copyright Holder(s)" refers to all individuals and companies who have a
+copyright ownership of the Font Software.
+
+"Substantially Changed" refers to Modified Versions which can be easily
+identified as dissimilar to the Font Software by users of the Font
+Software comparing the Original Version with the Modified Version.
+
+To "Propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification and with or without charging
+a redistribution fee), making available to the public, and in some
+countries other activities as well.
+
+PERMISSION & CONDITIONS
+This licence does not grant any rights under trademark law and all such
+rights are reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to propagate the Font Software, subject to
+the below conditions:
+
+1) Each copy of the Font Software must contain the above copyright
+notice and this licence. These can be included either as stand-alone
+text files, human-readable headers or in the appropriate machine-
+readable metadata fields within text or binary files as long as those
+fields can be easily viewed by the user.
+
+2) The font name complies with the following:
+(a) The Original Version must retain its name, unmodified.
+(b) Modified Versions which are Substantially Changed must be renamed to
+avoid use of the name of the Original Version or similar names entirely.
+(c) Modified Versions which are not Substantially Changed must be
+renamed to both (i) retain the name of the Original Version and (ii) add
+additional naming elements to distinguish the Modified Version from the
+Original Version. The name of such Modified Versions must be the name of
+the Original Version, with "derivative X" where X represents the name of
+the new work, appended to that name.
+
+3) The name(s) of the Copyright Holder(s) and any contributor to the
+Font Software shall not be used to promote, endorse or advertise any
+Modified Version, except (i) as required by this licence, (ii) to
+acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
+their explicit written permission.
+
+4) The Font Software, modified or unmodified, in part or in whole, must
+be distributed entirely under this licence, and must not be distributed
+under any other licence. The requirement for fonts to remain under this
+licence does not affect any document created using the Font Software,
+except any version of the Font Software extracted from a document
+created using the Font Software may only be distributed under this
+licence.
+
+TERMINATION
+This licence becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
diff --git a/assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf b/assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
new file mode 100644
index 00000000..01ad81bf
Binary files /dev/null and b/assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf differ
diff --git a/assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf b/assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
new file mode 100644
index 00000000..731884eb
Binary files /dev/null and b/assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf differ
diff --git a/assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf b/assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
new file mode 100644
index 00000000..b89338d4
Binary files /dev/null and b/assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf differ
diff --git a/assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf b/assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
new file mode 100644
index 00000000..4977028d
Binary files /dev/null and b/assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf differ
diff --git a/assets/album-placeholder.png b/assets/images/album-placeholder.png
similarity index 100%
rename from assets/album-placeholder.png
rename to assets/images/album-placeholder.png
diff --git a/assets/images/bengali-patterns-bg.jpg b/assets/images/bengali-patterns-bg.jpg
new file mode 100644
index 00000000..a4090a01
Binary files /dev/null and b/assets/images/bengali-patterns-bg.jpg differ
diff --git a/assets/images/liked-tracks.jpg b/assets/images/liked-tracks.jpg
new file mode 100644
index 00000000..71e010dc
Binary files /dev/null and b/assets/images/liked-tracks.jpg differ
diff --git a/assets/images/logos/invidious.jpg b/assets/images/logos/invidious.jpg
new file mode 100644
index 00000000..3a54ace1
Binary files /dev/null and b/assets/images/logos/invidious.jpg differ
diff --git a/assets/jiosaavn.png b/assets/images/logos/jiosaavn.png
similarity index 100%
rename from assets/jiosaavn.png
rename to assets/images/logos/jiosaavn.png
diff --git a/assets/images/logos/songlink-transparent.png b/assets/images/logos/songlink-transparent.png
new file mode 100644
index 00000000..fc4ae541
Binary files /dev/null and b/assets/images/logos/songlink-transparent.png differ
diff --git a/assets/placeholder.png b/assets/images/placeholder.png
similarity index 100%
rename from assets/placeholder.png
rename to assets/images/placeholder.png
diff --git a/assets/user-placeholder.png b/assets/images/user-placeholder.png
similarity index 100%
rename from assets/user-placeholder.png
rename to assets/images/user-placeholder.png
diff --git a/assets/invidious.jpg b/assets/invidious.jpg
deleted file mode 100644
index 12c5f107..00000000
Binary files a/assets/invidious.jpg and /dev/null differ
diff --git a/assets/liked-tracks.jpg b/assets/liked-tracks.jpg
deleted file mode 100644
index 62dad65e..00000000
Binary files a/assets/liked-tracks.jpg and /dev/null differ
diff --git a/assets/logos/songlink-transparent.png b/assets/logos/songlink-transparent.png
deleted file mode 100644
index 6b7064c9..00000000
Binary files a/assets/logos/songlink-transparent.png and /dev/null differ
diff --git a/assets/logos/songlink.png b/assets/logos/songlink.png
deleted file mode 100644
index 43d823a5..00000000
Binary files a/assets/logos/songlink.png and /dev/null differ
diff --git a/assets/patterns/black_white_visualized.jpg b/assets/patterns/black_white_visualized.jpg
deleted file mode 100644
index e56a2780..00000000
Binary files a/assets/patterns/black_white_visualized.jpg and /dev/null differ
diff --git a/assets/patterns/brazil_carnival.jpg b/assets/patterns/brazil_carnival.jpg
deleted file mode 100644
index a7cdb3a1..00000000
Binary files a/assets/patterns/brazil_carnival.jpg and /dev/null differ
diff --git a/assets/patterns/cotton_balls.jpg b/assets/patterns/cotton_balls.jpg
deleted file mode 100644
index db6f02a8..00000000
Binary files a/assets/patterns/cotton_balls.jpg and /dev/null differ
diff --git a/assets/patterns/cute_worms.jpg b/assets/patterns/cute_worms.jpg
deleted file mode 100644
index 0c9f4fbb..00000000
Binary files a/assets/patterns/cute_worms.jpg and /dev/null differ
diff --git a/assets/patterns/flash_cross_axis.jpg b/assets/patterns/flash_cross_axis.jpg
deleted file mode 100644
index c6e52283..00000000
Binary files a/assets/patterns/flash_cross_axis.jpg and /dev/null differ
diff --git a/assets/patterns/memphis_shapes.jpg b/assets/patterns/memphis_shapes.jpg
deleted file mode 100644
index 2db8e775..00000000
Binary files a/assets/patterns/memphis_shapes.jpg and /dev/null differ
diff --git a/assets/patterns/oval_gloomy.jpg b/assets/patterns/oval_gloomy.jpg
deleted file mode 100644
index b44bf945..00000000
Binary files a/assets/patterns/oval_gloomy.jpg and /dev/null differ
diff --git a/assets/patterns/oval_sunny.jpg b/assets/patterns/oval_sunny.jpg
deleted file mode 100644
index bc07ae83..00000000
Binary files a/assets/patterns/oval_sunny.jpg and /dev/null differ
diff --git a/assets/patterns/red_nimbuses.jpg b/assets/patterns/red_nimbuses.jpg
deleted file mode 100644
index 6527999c..00000000
Binary files a/assets/patterns/red_nimbuses.jpg and /dev/null differ
diff --git a/assets/patterns/tree_bark.jpg b/assets/patterns/tree_bark.jpg
deleted file mode 100644
index 0dac37d7..00000000
Binary files a/assets/patterns/tree_bark.jpg and /dev/null differ
diff --git a/assets/patterns/vibrant_pentagons.jpg b/assets/patterns/vibrant_pentagons.jpg
deleted file mode 100644
index d9e8d537..00000000
Binary files a/assets/patterns/vibrant_pentagons.jpg and /dev/null differ
diff --git a/assets/patterns/wiring_pattern.jpg b/assets/patterns/wiring_pattern.jpg
deleted file mode 100644
index 9fc3b781..00000000
Binary files a/assets/patterns/wiring_pattern.jpg and /dev/null differ
diff --git a/assets/patterns/zigzags_gloomy.jpg b/assets/patterns/zigzags_gloomy.jpg
deleted file mode 100644
index c6ccd2a3..00000000
Binary files a/assets/patterns/zigzags_gloomy.jpg and /dev/null differ
diff --git a/assets/patterns/zigzags_sunny.jpg b/assets/patterns/zigzags_sunny.jpg
deleted file mode 100644
index 7470d5ef..00000000
Binary files a/assets/patterns/zigzags_sunny.jpg and /dev/null differ
diff --git a/assets/success.png b/assets/success.png
deleted file mode 100644
index 65cdba35..00000000
Binary files a/assets/success.png and /dev/null differ
diff --git a/assets/tutorial/step-1.png b/assets/tutorial/step-1.png
deleted file mode 100644
index 1182f054..00000000
Binary files a/assets/tutorial/step-1.png and /dev/null differ
diff --git a/assets/tutorial/step-2.png b/assets/tutorial/step-2.png
deleted file mode 100644
index af4616b0..00000000
Binary files a/assets/tutorial/step-2.png and /dev/null differ
diff --git a/assets/tutorial/step-3.png b/assets/tutorial/step-3.png
deleted file mode 100644
index ddbea140..00000000
Binary files a/assets/tutorial/step-3.png and /dev/null differ
diff --git a/choco-struct/spotube.nuspec b/choco-struct/spotube.nuspec
index e3588d98..40941c08 100644
--- a/choco-struct/spotube.nuspec
+++ b/choco-struct/spotube.nuspec
@@ -15,7 +15,7 @@ enclosed in quotation marks, you should use an editor that supports UTF-8, not t
Kingkor Roy Tirtho
https://spotube.krtirtho.dev
- https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png
+ https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/branding/spotube-logo.png
2022 Spotube
https://github.com/KRTirtho/spotube/blob/master/LICENSE
diff --git a/cli/commands/build/linux.dart b/cli/commands/build/linux.dart
index 3fd8a0b9..378f5a72 100644
--- a/cli/commands/build/linux.dart
+++ b/cli/commands/build/linux.dart
@@ -74,7 +74,7 @@ class LinuxBuildCommand extends Command with BuildCommandCommonSteps {
).copy(
join(tempDir, "com.github.KRTirtho.Spotube.appdata.xml"),
);
- await File(join(cwd.path, "assets", "spotube-logo.png")).copy(
+ await File(join(cwd.path, "assets", "branding", "spotube-logo.png")).copy(
join(tempDir, "spotube-logo.png"),
);
diff --git a/cliff.toml b/cliff.toml
new file mode 100644
index 00000000..688ecfbb
--- /dev/null
+++ b/cliff.toml
@@ -0,0 +1,92 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+ ## [{{ version | trim_start_matches(pat="v") }}](
/compare/v{{ previous.version | trim_start_matches(pat="v") }}...v{{ version | trim_start_matches(pat="v") }}) ({{ timestamp | date(format="%Y-%m-%d") }})
+{% else %}\
+ ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | striptags | trim | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\
+ {% if commit.breaking %}[**breaking**] {% endif %}\
+ {{ commit.message | upper_first }}\
+ {% endfor %}
+{% endfor %}
+"""
+# Remove leading and trailing whitespaces from the changelog's body.
+trim = true
+# Render body even when there are no releases to process.
+render_always = true
+# An array of regex based postprocessors to modify the changelog.
+postprocessors = [
+ # Replace the placeholder with a URL.
+ { pattern = '', replace = "https://github.com/KRTirtho/spotube" },
+]
+# render body even when there are no releases to process
+# render_always = true
+# output file path
+# output = "test.md"
+
+[git]
+# Parse commits according to the conventional commits specification.
+# See https://www.conventionalcommits.org
+conventional_commits = true
+# Exclude commits that do not match the conventional commits specification.
+filter_unconventional = true
+# Require all commits to be conventional.
+# Takes precedence over filter_unconventional.
+require_conventional = false
+# Split commits on newlines, treating each line as an individual commit.
+split_commits = false
+# An array of regex based parsers to modify commit messages prior to further processing.
+commit_preprocessors = [
+ # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
+ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" },
+ # Check spelling of the commit message using https://github.com/crate-ci/typos.
+ # If the spelling is incorrect, it will be fixed automatically.
+ #{ pattern = '.*', replace_command = 'typos --write-changes -' },
+]
+# Prevent commits that are breaking from being excluded by commit parsers.
+protect_breaking_commits = false
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's scope and can decide to exclude commits from further processing.
+commit_parsers = [
+ { message = "^feat", group = "Features" },
+ { message = "^fix", group = "Bug Fixes" },
+ # { message = "^doc", group = "📚 Documentation" },
+ # { message = "^perf", group = "⚡ Performance" },
+ # { message = "^refactor", group = "🚜 Refactor" },
+ # { message = "^style", group = "🎨 Styling" },
+ # { message = "^test", group = "🧪 Testing" },
+ # { message = "^chore\\(release\\): prepare for", skip = true },
+ # { message = "^chore\\(deps.*\\)", skip = true },
+ # { message = "^chore\\(pr\\)", skip = true },
+ # { message = "^chore\\(pull\\)", skip = true },
+ # { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" },
+ # { body = ".*security", group = "🛡️ Security" },
+ # { message = "^revert", group = "◀️ Revert" },
+ # { message = ".*", group = "💼 Other" },
+]
+# Exclude commits that are not matched by any commit parser.
+filter_commits = true
+# An array of link parsers for extracting external references, and turning them into URLs, using regex.
+link_parsers = []
+# Include only the tags that belong to the current branch.
+use_branch_tags = false
+# Order releases topologically instead of chronologically.
+topo_order = false
+# Order releases topologically instead of chronologically.
+topo_order_commits = true
+# Order of commits in each group/release within the changelog.
+# Allowed values: newest, oldest
+sort_commits = "oldest"
+# Process submodules commits
+recurse_submodules = false
diff --git a/drift_schemas/app_db/drift_schema_v7.json b/drift_schemas/app_db/drift_schema_v7.json
new file mode 100644
index 00000000..d6644857
--- /dev/null
+++ b/drift_schemas/app_db/drift_schema_v7.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"audio_quality","getter_name":"audioQuality","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceQualities.high.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceQualities.values)","dart_type_name":"SourceQualities"}},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Orange:0xFFf97315\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"piped_instance","getter_name":"pipedInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://pipedapi.kavin.rocks\")","default_client_dart":null,"dsl_features":[]},{"name":"invidious_instance","getter_name":"invidiousInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://inv.nadeko.net\")","default_client_dart":null,"dsl_features":[]},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source","getter_name":"audioSource","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(AudioSource.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(AudioSource.values)","dart_type_name":"AudioSource"}},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"stream_music_codec","getter_name":"streamMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.weba.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"download_music_codec","getter_name":"downloadMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.m4a.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"connect_port","getter_name":"connectPort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(-1)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_id","getter_name":"sourceId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceType.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceType.values)","dart_type_name":"SourceType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"tracks","getter_name":"tracks","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"[]\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeTrackObjectListConverter()","dart_type_name":"List"}},{"name":"current_index","getter_name":"currentIndex","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"metadata_plugins_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":50}}]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"author","getter_name":"author","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entry_point","getter_name":"entryPoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"apis","getter_name":"apis","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"abilities","getter_name":"abilities","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"selected","getter_name":"selected","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"repository","getter_name":"repository","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"plugin_api_version","getter_name":"pluginApiVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":11,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_id","source_type"]}}]}
\ No newline at end of file
diff --git a/drift_schemas/app_db/drift_schema_v8.json b/drift_schemas/app_db/drift_schema_v8.json
new file mode 100644
index 00000000..b2cc7428
--- /dev/null
+++ b/drift_schemas/app_db/drift_schema_v8.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"audio_quality","getter_name":"audioQuality","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceQualities.high.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceQualities.values)","dart_type_name":"SourceQualities"}},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Orange:0xFFf97315\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"piped_instance","getter_name":"pipedInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://pipedapi.kavin.rocks\")","default_client_dart":null,"dsl_features":[]},{"name":"invidious_instance","getter_name":"invidiousInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://inv.nadeko.net\")","default_client_dart":null,"dsl_features":[]},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source","getter_name":"audioSource","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(AudioSource.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(AudioSource.values)","dart_type_name":"AudioSource"}},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"stream_music_codec","getter_name":"streamMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.weba.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"download_music_codec","getter_name":"downloadMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.m4a.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"connect_port","getter_name":"connectPort","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(-1)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_id","getter_name":"sourceId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceType.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceType.values)","dart_type_name":"SourceType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"tracks","getter_name":"tracks","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"[]\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeTrackObjectListConverter()","dart_type_name":"List"}},{"name":"current_index","getter_name":"currentIndex","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"metadata_plugins_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":50}}]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"version","getter_name":"version","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"author","getter_name":"author","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"entry_point","getter_name":"entryPoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"apis","getter_name":"apis","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"abilities","getter_name":"abilities","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"selected","getter_name":"selected","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"selected\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"selected\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"repository","getter_name":"repository","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"plugin_api_version","getter_name":"pluginApiVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('1.0.0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":11,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_id","source_type"]}}]}
\ No newline at end of file
diff --git a/flutter_launcher_icons-nightly.yaml b/flutter_launcher_icons-nightly.yaml
index 770033bd..9e4e805c 100644
--- a/flutter_launcher_icons-nightly.yaml
+++ b/flutter_launcher_icons-nightly.yaml
@@ -1,6 +1,6 @@
flutter_launcher_icons:
android: true
ios: true
- image_path: "assets/spotube-nightly-logo.png"
- adaptive_icon_foreground: "assets/spotube-nightly-logo-foreground.png"
+ image_path: "assets/branding/spotube-nightly-logo.png"
+ adaptive_icon_foreground: "assets/branding/spotube-nightly-logo-foreground.png"
adaptive_icon_background: "#242832"
diff --git a/flutter_launcher_icons.yaml b/flutter_launcher_icons.yaml
index 2c558583..e5b26882 100644
--- a/flutter_launcher_icons.yaml
+++ b/flutter_launcher_icons.yaml
@@ -1,19 +1,19 @@
# flutter pub run flutter_launcher_icons
flutter_launcher_icons:
- image_path: "assets/spotube-logo.png"
+ image_path: "assets/branding/spotube-logo.png"
android: true
- # image_path_android: "assets/icon/icon.png"
+ # image_path_android: "assets/branding/icon/icon.png"
min_sdk_android: 21 # android min sdk min:16, default 21
adaptive_icon_background: "#242832"
- adaptive_icon_foreground: "assets/spotube-logo-foreground.png"
- # adaptive_icon_monochrome: "assets/icon/monochrome.png"
+ adaptive_icon_foreground: "assets/branding/spotube-logo-foreground.png"
+ # adaptive_icon_monochrome: "assets/branding/icon/monochrome.png"
ios: true
- # image_path_ios: "assets/icon/icon.png"
+ # image_path_ios: "assets/branding/icon/icon.png"
remove_alpha_channel_ios: true
- # image_path_ios_dark_transparent: "assets/icon/icon_dark.png"
- # image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png"
+ # image_path_ios_dark_transparent: "assets/branding/icon/icon_dark.png"
+ # image_path_ios_tinted_grayscale: "assets/branding/icon/icon_tinted.png"
# desaturate_tinted_to_grayscale_ios: true
web:
@@ -21,9 +21,9 @@ flutter_launcher_icons:
windows:
generate: true
- image_path: "assets/spotube-logo.png"
+ image_path: "assets/branding/spotube-logo.png"
icon_size: 48 # min:48, max:256, default: 48
macos:
generate: true
- image_path: "assets/spotube-logo-macos.png"
+ image_path: "assets/branding/spotube-logo-macos.png"
diff --git a/flutter_native_splash-nightly.yaml b/flutter_native_splash-nightly.yaml
index 37da37d9..3b7daeec 100644
--- a/flutter_native_splash-nightly.yaml
+++ b/flutter_native_splash-nightly.yaml
@@ -1,9 +1,9 @@
flutter_native_splash:
- background_image: assets/bengali-patterns-bg.jpg
- image: assets/spotube-nightly-logo.png
- branding: assets/branding.png
+ background_image: assets/images/bengali-patterns-bg.jpg
+ image: assets/branding/spotube-nightly-logo.png
+ branding: assets/branding/branding.png
android_12:
- image: assets/spotube-nightly-logo_android12.png
- branding: assets/branding.png
+ image: assets/branding/spotube-nightly-logo_android12.png
+ branding: assets/branding/branding.png
color: "#000000"
icon_background_color: "#000000"
diff --git a/l10n.yaml b/l10n.yaml
index ffab1c86..d5911fe1 100644
--- a/l10n.yaml
+++ b/l10n.yaml
@@ -2,4 +2,3 @@ arb-dir: lib/l10n
template-arb-file: app_en.arb
output-dir: lib/l10n/generated
untranslated-messages-file: untranslated_messages.json
-synthetic-package: false
diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart
index 09b6cdd2..31fb54b8 100644
--- a/lib/collections/assets.gen.dart
+++ b/lib/collections/assets.gen.dart
@@ -9,220 +9,89 @@
import 'package:flutter/widgets.dart';
-class $AssetsBackgroundsGen {
- const $AssetsBackgroundsGen();
+class $AssetsBrandingGen {
+ const $AssetsBrandingGen();
- /// File path: assets/backgrounds/xmas-effect.png
- AssetGenImage get xmasEffect =>
- const AssetGenImage('assets/backgrounds/xmas-effect.png');
+ /// File path: assets/branding/spotube-logo-light.png
+ AssetGenImage get spotubeLogoLight =>
+ const AssetGenImage('assets/branding/spotube-logo-light.png');
+
+ /// File path: assets/branding/spotube-logo.ico
+ String get spotubeLogoIco => 'assets/branding/spotube-logo.ico';
+
+ /// File path: assets/branding/spotube-logo.png
+ AssetGenImage get spotubeLogoPng =>
+ const AssetGenImage('assets/branding/spotube-logo.png');
/// List of all assets
- List get values => [xmasEffect];
+ List get values =>
+ [spotubeLogoLight, spotubeLogoIco, spotubeLogoPng];
}
-class $AssetsLogosGen {
- const $AssetsLogosGen();
+class $AssetsImagesGen {
+ const $AssetsImagesGen();
- /// File path: assets/logos/songlink-transparent.png
- AssetGenImage get songlinkTransparent =>
- const AssetGenImage('assets/logos/songlink-transparent.png');
+ /// File path: assets/images/album-placeholder.png
+ AssetGenImage get albumPlaceholder =>
+ const AssetGenImage('assets/images/album-placeholder.png');
- /// File path: assets/logos/songlink.png
- AssetGenImage get songlink =>
- const AssetGenImage('assets/logos/songlink.png');
+ /// File path: assets/images/bengali-patterns-bg.jpg
+ AssetGenImage get bengaliPatternsBg =>
+ const AssetGenImage('assets/images/bengali-patterns-bg.jpg');
- /// List of all assets
- List get values => [songlinkTransparent, songlink];
-}
+ /// File path: assets/images/liked-tracks.jpg
+ AssetGenImage get likedTracks =>
+ const AssetGenImage('assets/images/liked-tracks.jpg');
-class $AssetsPatternsGen {
- const $AssetsPatternsGen();
+ /// Directory path: assets/images/logos
+ $AssetsImagesLogosGen get logos => const $AssetsImagesLogosGen();
- /// File path: assets/patterns/black_white_visualized.jpg
- AssetGenImage get blackWhiteVisualized =>
- const AssetGenImage('assets/patterns/black_white_visualized.jpg');
+ /// File path: assets/images/placeholder.png
+ AssetGenImage get placeholder =>
+ const AssetGenImage('assets/images/placeholder.png');
- /// File path: assets/patterns/brazil_carnival.jpg
- AssetGenImage get brazilCarnival =>
- const AssetGenImage('assets/patterns/brazil_carnival.jpg');
-
- /// File path: assets/patterns/cotton_balls.jpg
- AssetGenImage get cottonBalls =>
- const AssetGenImage('assets/patterns/cotton_balls.jpg');
-
- /// File path: assets/patterns/cute_worms.jpg
- AssetGenImage get cuteWorms =>
- const AssetGenImage('assets/patterns/cute_worms.jpg');
-
- /// File path: assets/patterns/flash_cross_axis.jpg
- AssetGenImage get flashCrossAxis =>
- const AssetGenImage('assets/patterns/flash_cross_axis.jpg');
-
- /// File path: assets/patterns/memphis_shapes.jpg
- AssetGenImage get memphisShapes =>
- const AssetGenImage('assets/patterns/memphis_shapes.jpg');
-
- /// File path: assets/patterns/oval_gloomy.jpg
- AssetGenImage get ovalGloomy =>
- const AssetGenImage('assets/patterns/oval_gloomy.jpg');
-
- /// File path: assets/patterns/oval_sunny.jpg
- AssetGenImage get ovalSunny =>
- const AssetGenImage('assets/patterns/oval_sunny.jpg');
-
- /// File path: assets/patterns/red_nimbuses.jpg
- AssetGenImage get redNimbuses =>
- const AssetGenImage('assets/patterns/red_nimbuses.jpg');
-
- /// File path: assets/patterns/tree_bark.jpg
- AssetGenImage get treeBark =>
- const AssetGenImage('assets/patterns/tree_bark.jpg');
-
- /// File path: assets/patterns/vibrant_pentagons.jpg
- AssetGenImage get vibrantPentagons =>
- const AssetGenImage('assets/patterns/vibrant_pentagons.jpg');
-
- /// File path: assets/patterns/wiring_pattern.jpg
- AssetGenImage get wiringPattern =>
- const AssetGenImage('assets/patterns/wiring_pattern.jpg');
-
- /// File path: assets/patterns/zigzags_gloomy.jpg
- AssetGenImage get zigzagsGloomy =>
- const AssetGenImage('assets/patterns/zigzags_gloomy.jpg');
-
- /// File path: assets/patterns/zigzags_sunny.jpg
- AssetGenImage get zigzagsSunny =>
- const AssetGenImage('assets/patterns/zigzags_sunny.jpg');
+ /// File path: assets/images/user-placeholder.png
+ AssetGenImage get userPlaceholder =>
+ const AssetGenImage('assets/images/user-placeholder.png');
/// List of all assets
List get values => [
- blackWhiteVisualized,
- brazilCarnival,
- cottonBalls,
- cuteWorms,
- flashCrossAxis,
- memphisShapes,
- ovalGloomy,
- ovalSunny,
- redNimbuses,
- treeBark,
- vibrantPentagons,
- wiringPattern,
- zigzagsGloomy,
- zigzagsSunny
+ albumPlaceholder,
+ bengaliPatternsBg,
+ likedTracks,
+ placeholder,
+ userPlaceholder
];
}
-class $AssetsTutorialGen {
- const $AssetsTutorialGen();
+class $AssetsImagesLogosGen {
+ const $AssetsImagesLogosGen();
- /// File path: assets/tutorial/step-1.png
- AssetGenImage get step1 => const AssetGenImage('assets/tutorial/step-1.png');
+ /// File path: assets/images/logos/invidious.jpg
+ AssetGenImage get invidious =>
+ const AssetGenImage('assets/images/logos/invidious.jpg');
- /// File path: assets/tutorial/step-2.png
- AssetGenImage get step2 => const AssetGenImage('assets/tutorial/step-2.png');
+ /// File path: assets/images/logos/jiosaavn.png
+ AssetGenImage get jiosaavn =>
+ const AssetGenImage('assets/images/logos/jiosaavn.png');
- /// File path: assets/tutorial/step-3.png
- AssetGenImage get step3 => const AssetGenImage('assets/tutorial/step-3.png');
+ /// File path: assets/images/logos/songlink-transparent.png
+ AssetGenImage get songlinkTransparent =>
+ const AssetGenImage('assets/images/logos/songlink-transparent.png');
/// List of all assets
- List get values => [step1, step2, step3];
+ List get values => [invidious, jiosaavn, songlinkTransparent];
}
class Assets {
Assets._();
static const String license = 'LICENSE';
- static const AssetGenImage albumPlaceholder =
- AssetGenImage('assets/album-placeholder.png');
- static const $AssetsBackgroundsGen backgrounds = $AssetsBackgroundsGen();
- static const AssetGenImage bengaliPatternsBg =
- AssetGenImage('assets/bengali-patterns-bg.jpg');
- static const AssetGenImage branding = AssetGenImage('assets/branding.png');
- static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png');
- static const AssetGenImage invidious = AssetGenImage('assets/invidious.jpg');
- static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
- static const AssetGenImage likedTracks =
- AssetGenImage('assets/liked-tracks.jpg');
- static const $AssetsLogosGen logos = $AssetsLogosGen();
- static const $AssetsPatternsGen patterns = $AssetsPatternsGen();
- static const AssetGenImage placeholder =
- AssetGenImage('assets/placeholder.png');
- static const AssetGenImage spotubeHeroBanner =
- AssetGenImage('assets/spotube-hero-banner.png');
- static const AssetGenImage spotubeLogoForeground =
- AssetGenImage('assets/spotube-logo-foreground.png');
- static const AssetGenImage spotubeLogoItem =
- AssetGenImage('assets/spotube-logo-item.png');
- static const AssetGenImage spotubeLogoLight =
- AssetGenImage('assets/spotube-logo-light.png');
- static const AssetGenImage spotubeLogoMacos =
- AssetGenImage('assets/spotube-logo-macos.png');
- static const AssetGenImage spotubeLogoBmp =
- AssetGenImage('assets/spotube-logo.bmp');
- static const String spotubeLogoIco = 'assets/spotube-logo.ico';
- static const AssetGenImage spotubeLogoPng =
- AssetGenImage('assets/spotube-logo.png');
- static const AssetGenImage spotubeLogoAndroid12 =
- AssetGenImage('assets/spotube-logo_android12.png');
- static const AssetGenImage spotubeNightlyItem =
- AssetGenImage('assets/spotube-nightly-item.png');
- static const AssetGenImage spotubeNightlyLogoForegroundPng =
- AssetGenImage('assets/spotube-nightly-logo-foreground.png');
- static const String spotubeNightlyLogoForegroundSvg =
- 'assets/spotube-nightly-logo-foreground.svg';
- static const AssetGenImage spotubeNightlyLogo =
- AssetGenImage('assets/spotube-nightly-logo.png');
- static const AssetGenImage spotubeNightlyLogoAndroid12 =
- AssetGenImage('assets/spotube-nightly-logo_android12.png');
- static const AssetGenImage spotubeScreenshot =
- AssetGenImage('assets/spotube-screenshot.png');
- static const AssetGenImage spotubeTallCapsule =
- AssetGenImage('assets/spotube-tall-capsule.png');
- static const AssetGenImage spotubeWideCapsuleLarge =
- AssetGenImage('assets/spotube-wide-capsule-large.png');
- static const AssetGenImage spotubeWideCapsuleSmall =
- AssetGenImage('assets/spotube-wide-capsule-small.png');
- static const AssetGenImage spotubeBanner =
- AssetGenImage('assets/spotube_banner.png');
- static const AssetGenImage success = AssetGenImage('assets/success.png');
- static const $AssetsTutorialGen tutorial = $AssetsTutorialGen();
- static const AssetGenImage userPlaceholder =
- AssetGenImage('assets/user-placeholder.png');
+ static const $AssetsBrandingGen branding = $AssetsBrandingGen();
+ static const $AssetsImagesGen images = $AssetsImagesGen();
/// List of all assets
- static List get values => [
- license,
- albumPlaceholder,
- bengaliPatternsBg,
- branding,
- emptyBox,
- invidious,
- jiosaavn,
- likedTracks,
- placeholder,
- spotubeHeroBanner,
- spotubeLogoForeground,
- spotubeLogoItem,
- spotubeLogoLight,
- spotubeLogoMacos,
- spotubeLogoBmp,
- spotubeLogoIco,
- spotubeLogoPng,
- spotubeLogoAndroid12,
- spotubeNightlyItem,
- spotubeNightlyLogoForegroundPng,
- spotubeNightlyLogoForegroundSvg,
- spotubeNightlyLogo,
- spotubeNightlyLogoAndroid12,
- spotubeScreenshot,
- spotubeTallCapsule,
- spotubeWideCapsuleLarge,
- spotubeWideCapsuleSmall,
- spotubeBanner,
- success,
- userPlaceholder
- ];
+ static List get values => [license];
}
class AssetGenImage {
diff --git a/lib/collections/env.dart b/lib/collections/env.dart
index feb2a2db..52ef2bbf 100644
--- a/lib/collections/env.dart
+++ b/lib/collections/env.dart
@@ -10,9 +10,6 @@ enum ReleaseChannel {
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
abstract class Env {
- @EnviedField(varName: 'SPOTIFY_SECRETS')
- static final String rawSpotifySecrets = _Env.rawSpotifySecrets;
-
@EnviedField(varName: 'LASTFM_API_KEY')
static final String lastFmApiKey = _Env.lastFmApiKey;
@@ -24,25 +21,12 @@ abstract class Env {
static bool get hideDonations => _hideDonations == 1;
- static final spotifySecrets = rawSpotifySecrets.split(',').map((e) {
- final secrets = e.trim().split(":").map((e) => e.trim());
- return {
- "clientId": secrets.first,
- "clientSecret": secrets.last,
- };
- }).toList();
-
@EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1")
static final String _enableUpdateChecker = _Env._enableUpdateChecker;
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
static final String _releaseChannel = _Env._releaseChannel;
- @EnviedField(varName: "DISABLE_SPOTIFY_IMAGES", defaultValue: "0")
- static final String _disableSpotifyImages = _Env._disableSpotifyImages;
-
- static bool get disableSpotifyImages => _disableSpotifyImages == "1";
-
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
? ReleaseChannel.stable
: ReleaseChannel.nightly;
diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart
index 8af40e71..7d201ae2 100644
--- a/lib/collections/fake.dart
+++ b/lib/collections/fake.dart
@@ -1,231 +1,112 @@
-import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
-import 'package:spotube/models/spotify/home_feed.dart';
-import 'package:spotube/models/spotify_friends.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/history/summary.dart';
abstract class FakeData {
- static final Image image = Image()
- ..height = 1
- ..width = 1
- ..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg";
+ static final SpotubeImageObject image = SpotubeImageObject(
+ height: 100,
+ width: 100,
+ url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
+ );
- static final Followers followers = Followers()
- ..href = "text"
- ..total = 1;
-
- static final Artist artist = Artist()
- ..id = "1"
- ..name = "Wow artist Good!"
- ..images = [image]
- ..popularity = 1
- ..type = "type"
- ..uri = "uri"
- ..externalUrls = externalUrls
- ..genres = ["genre"]
- ..href = "text"
- ..followers = followers;
-
- static final externalIds = ExternalIds()
- ..isrc = "text"
- ..ean = "text"
- ..upc = "text";
-
- static final externalUrls = ExternalUrls()..spotify = "text";
-
- static final Album album = Album()
- ..id = "1"
- ..genres = ["genre"]
- ..label = "label"
- ..popularity = 1
- ..albumType = AlbumType.album
- ..artists = [artist]
- ..availableMarkets = [Market.BD]
- ..externalUrls = externalUrls
- ..href = "text"
- ..images = [image]
- ..name = "Another good album"
- ..releaseDate = "2021-01-01"
- ..releaseDatePrecision = DatePrecision.day
- ..tracks = [track]
- ..type = "type"
- ..uri = "uri"
- ..externalIds = externalIds
- ..copyrights = [
- Copyright()
- ..type = CopyrightType.C
- ..text = "text",
- ];
-
- static final ArtistSimple artistSimple = ArtistSimple()
- ..id = "1"
- ..name = "What an artist"
- ..type = "type"
- ..uri = "uri"
- ..externalUrls = externalUrls;
-
- static final AlbumSimple albumSimple = AlbumSimple()
- ..id = "1"
- ..albumType = AlbumType.album
- ..artists = [artistSimple]
- ..availableMarkets = [Market.BD]
- ..externalUrls = externalUrls
- ..href = "text"
- ..images = [image]
- ..name = "A good album"
- ..releaseDate = "2021-01-01"
- ..releaseDatePrecision = DatePrecision.day
- ..type = "type"
- ..uri = "uri";
-
- static final Track track = Track()
- ..id = "1"
- ..artists = [artist, artist, artist]
- ..album = albumSimple
- ..availableMarkets = [Market.BD]
- ..discNumber = 1
- ..durationMs = 50000
- ..explicit = false
- ..externalUrls = externalUrls
- ..href = "text"
- ..name = "A Track Name"
- ..popularity = 1
- ..previewUrl = "url"
- ..trackNumber = 1
- ..type = "type"
- ..uri = "uri"
- ..externalIds = externalIds
- ..isPlayable = true
- ..explicit = false
- ..linkedFrom = trackLink;
-
- static final TrackLink trackLink = TrackLink()
- ..id = "1"
- ..type = "type"
- ..uri = "uri"
- ..externalUrls = {"spotify": "text"}
- ..href = "text";
-
- static final Paging paging = Paging()
- ..href = "text"
- ..itemsNative = [track.toJson()]
- ..limit = 1
- ..next = "text"
- ..offset = 1
- ..previous = "text"
- ..total = 1;
-
- static final User user = User()
- ..id = "1"
- ..displayName = "Your Name"
- ..birthdate = "2021-01-01"
- ..country = Market.BD
- ..email = "test@email.com"
- ..followers = followers
- ..href = "text"
- ..images = [image]
- ..type = "type"
- ..uri = "uri";
-
- static final TracksLink tracksLink = TracksLink()
- ..href = "text"
- ..total = 1;
-
- static final Playlist playlist = Playlist()
- ..id = "1"
- ..collaborative = false
- ..description = "A very good playlist description"
- ..externalUrls = externalUrls
- ..followers = followers
- ..href = "text"
- ..images = [image]
- ..name = "A good playlist"
- ..owner = user
- ..public = true
- ..snapshotId = "text"
- ..tracks = paging
- ..tracksLink = tracksLink
- ..type = "type"
- ..uri = "uri";
-
- static final PlaylistSimple playlistSimple = PlaylistSimple()
- ..id = "1"
- ..collaborative = false
- ..externalUrls = externalUrls
- ..href = "text"
- ..images = [image]
- ..name = "A good playlist"
- ..owner = user
- ..public = true
- ..snapshotId = "text"
- ..tracksLink = tracksLink
- ..type = "type"
- ..description = "A very good playlist description"
- ..uri = "uri";
-
- static final Category category = Category()
- ..href = "text"
- ..icons = [image]
- ..id = "1"
- ..name = "category";
-
- static final friends = SpotifyFriends(
- friends: [
- for (var i = 0; i < 3; i++)
- SpotifyFriendActivity(
- user: const SpotifyFriend(
- name: "name",
- imageUrl: "imageUrl",
- uri: "uri",
- ),
- track: SpotifyActivityTrack(
- name: "name",
- artist: const SpotifyActivityArtist(
- name: "name",
- uri: "uri",
- ),
- album: const SpotifyActivityAlbum(
- name: "name",
- uri: "uri",
- ),
- context: SpotifyActivityContext(
- name: "name",
- index: i,
- uri: "uri",
- ),
- imageUrl: "imageUrl",
- uri: "uri",
- ),
- ),
+ static final SpotubeFullArtistObject artist = SpotubeFullArtistObject(
+ id: "1",
+ name: "What an artist",
+ externalUri: "https://example.com",
+ followers: 10000,
+ genres: ["genre"],
+ images: [
+ SpotubeImageObject(
+ height: 100,
+ width: 100,
+ url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
+ ),
],
);
- static final feedSection = SpotifyHomeFeedSection(
- typename: "HomeGenericSectionData",
- uri: "spotify:section:lol",
- title: "Dummy",
- items: [
- for (int i = 0; i < 10; i++)
- SpotifyHomeFeedSectionItem(
- typename: "PlaylistResponseWrapper",
- playlist: SpotifySectionPlaylist(
- name: "Playlist $i",
- description: "Really super important description $i",
- format: "daily-mix",
- images: [
- const SpotifySectionItemImage(
- height: 1,
- width: 1,
- url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
- ),
- ],
- owner: "Spotify",
- uri: "spotify:playlist:id",
- ),
- )
+ static final SpotubeFullAlbumObject album = SpotubeFullAlbumObject(
+ id: "1",
+ name: "A good album",
+ externalUri: "https://example.com",
+ artists: [artistSimple],
+ releaseDate: "2021-01-01",
+ albumType: SpotubeAlbumType.album,
+ images: [image],
+ totalTracks: 10,
+ genres: ["genre"],
+ recordLabel: "Record Label",
+ );
+
+ static final SpotubeSimpleArtistObject artistSimple =
+ SpotubeSimpleArtistObject(
+ id: "1",
+ name: "What an artist",
+ externalUri: "https://example.com",
+ images: null,
+ );
+
+ static final SpotubeSimpleAlbumObject albumSimple = SpotubeSimpleAlbumObject(
+ albumType: SpotubeAlbumType.album,
+ artists: [],
+ externalUri: "https://example.com",
+ id: "1",
+ name: "A good album",
+ releaseDate: "2021-01-01",
+ images: [
+ SpotubeImageObject(
+ height: 1,
+ width: 1,
+ url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
+ )
],
);
+ static final SpotubeFullTrackObject track = SpotubeTrackObject.full(
+ id: "1",
+ name: "A good track",
+ externalUri: "https://example.com",
+ album: albumSimple,
+ durationMs: 3 * 60 * 1000, // 3 minutes
+ isrc: "USUM72112345",
+ explicit: false,
+ ) as SpotubeFullTrackObject;
+
+ static final SpotubeUserObject user = SpotubeUserObject(
+ id: "1",
+ name: "User Name",
+ externalUri: "https://example.com",
+ images: [image],
+ );
+
+ static final SpotubeFullPlaylistObject playlist = SpotubeFullPlaylistObject(
+ id: "1",
+ name: "A good playlist",
+ description: "A very good playlist description",
+ externalUri: "https://example.com",
+ collaborative: false,
+ public: true,
+ owner: user,
+ images: [image],
+ collaborators: [user]);
+
+ static final SpotubeSimplePlaylistObject playlistSimple =
+ SpotubeSimplePlaylistObject(
+ id: "1",
+ name: "A good playlist",
+ description: "A very good playlist description",
+ externalUri: "https://example.com",
+ owner: user,
+ images: [image],
+ );
+
+ static final SpotubeBrowseSectionObject browseSection =
+ SpotubeBrowseSectionObject(
+ id: "section-id",
+ title: "Browse Section",
+ browseMore: true,
+ externalUri: "https://example.com/browse/section",
+ items: [playlistSimple, playlistSimple, playlistSimple]);
+
static const historySummary = PlaybackHistorySummary(
albums: 1,
artists: 1,
diff --git a/lib/collections/fonts.gen.dart b/lib/collections/fonts.gen.dart
index 033d3a79..16cc6e82 100644
--- a/lib/collections/fonts.gen.dart
+++ b/lib/collections/fonts.gen.dart
@@ -13,6 +13,12 @@ class FontFamily {
/// Font family: BootstrapIcons
static const String bootstrapIcons = 'BootstrapIcons';
+ /// Font family: Cookie
+ static const String cookie = 'Cookie';
+
/// Font family: RadixIcons
static const String radixIcons = 'RadixIcons';
+
+ /// Font family: Ubuntu Mono
+ static const String ubuntuMono = 'Ubuntu Mono';
}
diff --git a/lib/collections/http-override.dart b/lib/collections/http-override.dart
new file mode 100644
index 00000000..3bf4f30e
--- /dev/null
+++ b/lib/collections/http-override.dart
@@ -0,0 +1,17 @@
+import 'dart:io';
+
+const allowList = [
+ "spotify.com",
+];
+
+class BadCertificateAllowlistOverrides extends HttpOverrides {
+ @override
+ HttpClient createHttpClient(SecurityContext? context) {
+ return super.createHttpClient(context)
+ ..badCertificateCallback = (X509Certificate cert, String host, int port) {
+ return allowList.any((allowedHost) {
+ return host.endsWith(allowedHost);
+ });
+ };
+ }
+}
diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart
index 75d1e65d..f97a16bf 100644
--- a/lib/collections/language_codes.dart
+++ b/lib/collections/language_codes.dart
@@ -137,6 +137,10 @@ abstract class LanguageLocals {
name: "Simplified Chinese",
nativeName: "简体中文",
),
+ "zh_TW": const ISOLanguageName(
+ name: "Traditional Chinese",
+ nativeName: "繁體中文(台灣)",
+ ),
// "cv": const ISOLanguageName(
// name: "Chuvash",
// nativeName: "чӑваш чӗлхи",
diff --git a/lib/collections/spotify_markets.dart b/lib/collections/markets.dart
similarity index 98%
rename from lib/collections/spotify_markets.dart
rename to lib/collections/markets.dart
index 514b3f0b..8398c662 100644
--- a/lib/collections/spotify_markets.dart
+++ b/lib/collections/markets.dart
@@ -1,8 +1,8 @@
// Country Codes contributed by momobobe
-import 'package:spotify/spotify.dart';
+import 'package:spotube/models/metadata/market.dart';
-final spotifyMarkets = [
+final marketsMap = [
(Market.AL, "Albania (AL)"),
(Market.DZ, "Algeria (DZ)"),
(Market.AD, "Andorra (AD)"),
diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart
index 543bc471..4dcd9657 100644
--- a/lib/collections/routes.dart
+++ b/lib/collections/routes.dart
@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.gr.dart';
-import 'package:spotube/provider/authentication/authentication.dart';
+import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
final rootNavigatorKey = GlobalKey();
@@ -28,9 +28,10 @@ class AppRouter extends RootStackRouter {
guards: [
AutoRouteGuardCallback(
(resolver, router) async {
- final auth = await ref.read(authenticationProvider.future);
+ final authenticated = await ref
+ .read(metadataPluginAuthenticatedProvider.future);
- if (auth == null && !KVStoreService.doneGettingStarted) {
+ if (!authenticated && !KVStoreService.doneGettingStarted) {
resolver.redirect(const GettingStartedRoute());
} else {
resolver.next(true);
@@ -40,16 +41,8 @@ class AppRouter extends RootStackRouter {
],
),
AutoRoute(
- path: "home/genres",
- page: GenreRoute.page,
- ),
- AutoRoute(
- path: "home/genre/:categoryId",
- page: GenrePlaylistsRoute.page,
- ),
- AutoRoute(
- path: "home/feeds/:feedId",
- page: HomeFeedSectionRoute.page,
+ path: "home/sections/:sectionId",
+ page: HomeBrowseSectionItemsRoute.page,
),
AutoRoute(
path: "search",
@@ -86,14 +79,6 @@ class AppRouter extends RootStackRouter {
page: LocalLibraryRoute.page,
// parentNavigatorKey: shellRouteNavigatorKey,
),
- AutoRoute(
- path: "library/generate",
- page: PlaylistGeneratorRoute.page,
- ),
- AutoRoute(
- path: "library/generate/result",
- page: PlaylistGenerateResultRoute.page,
- ),
AutoRoute(
path: "lyrics",
page: LyricsRoute.page,
@@ -102,6 +87,14 @@ class AppRouter extends RootStackRouter {
path: "settings",
page: SettingsRoute.page,
),
+ AutoRoute(
+ path: "settings/metadata-provider",
+ page: SettingsMetadataProviderRoute.page,
+ ),
+ AutoRoute(
+ path: "settings/metadata-provider/metadata-form",
+ page: SettingsMetadataProviderFormRoute.page,
+ ),
AutoRoute(
path: "settings/blacklist",
page: BlackListRoute.page,
@@ -115,6 +108,10 @@ class AppRouter extends RootStackRouter {
path: "settings/about",
page: AboutSpotubeRoute.page,
),
+ AutoRoute(
+ path: "settings/scrobbling",
+ page: SettingsScrobblingRoute.page,
+ ),
AutoRoute(
path: "album/:id",
page: AlbumRoute.page,
@@ -221,11 +218,6 @@ class AppRouter extends RootStackRouter {
page: GettingStartedRoute.page,
// parentNavigatorKey: rootNavigatorKey,
),
- AutoRoute(
- path: "/login",
- page: WebViewLoginRoute.page,
- // parentNavigatorKey: rootNavigatorKey,
- ),
AutoRoute(
path: "/lastfm-login",
page: LastFMLoginRoute.page,
diff --git a/lib/collections/routes.gr.dart b/lib/collections/routes.gr.dart
index 1d608896..e039abb9 100644
--- a/lib/collections/routes.gr.dart
+++ b/lib/collections/routes.gr.dart
@@ -8,62 +8,57 @@
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'package:auto_route/auto_route.dart' as _i43;
-import 'package:flutter/material.dart' as _i44;
-import 'package:shadcn_flutter/shadcn_flutter.dart' as _i46;
-import 'package:spotify/spotify.dart' as _i45;
-import 'package:spotube/models/spotify/recommendation_seeds.dart' as _i47;
+import 'package:auto_route/auto_route.dart' as _i41;
+import 'package:flutter/material.dart' as _i42;
+import 'package:shadcn_flutter/shadcn_flutter.dart' as _i44;
+import 'package:spotube/models/metadata/metadata.dart' as _i43;
import 'package:spotube/pages/album/album.dart' as _i2;
import 'package:spotube/pages/artist/artist.dart' as _i3;
import 'package:spotube/pages/connect/connect.dart' as _i6;
import 'package:spotube/pages/connect/control/control.dart' as _i5;
-import 'package:spotube/pages/getting_started/getting_started.dart' as _i9;
-import 'package:spotube/pages/home/feed/feed_section.dart' as _i10;
-import 'package:spotube/pages/home/genres/genre_playlists.dart' as _i8;
-import 'package:spotube/pages/home/genres/genres.dart' as _i7;
-import 'package:spotube/pages/home/home.dart' as _i11;
-import 'package:spotube/pages/lastfm_login/lastfm_login.dart' as _i12;
-import 'package:spotube/pages/library/library.dart' as _i13;
-import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'
- as _i23;
-import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart'
- as _i22;
-import 'package:spotube/pages/library/user_albums.dart' as _i37;
-import 'package:spotube/pages/library/user_artists.dart' as _i38;
-import 'package:spotube/pages/library/user_downloads.dart' as _i39;
+import 'package:spotube/pages/getting_started/getting_started.dart' as _i7;
+import 'package:spotube/pages/home/home.dart' as _i9;
+import 'package:spotube/pages/home/sections/section_items.dart' as _i8;
+import 'package:spotube/pages/lastfm_login/lastfm_login.dart' as _i10;
+import 'package:spotube/pages/library/library.dart' as _i11;
+import 'package:spotube/pages/library/user_albums.dart' as _i36;
+import 'package:spotube/pages/library/user_artists.dart' as _i37;
+import 'package:spotube/pages/library/user_downloads.dart' as _i38;
import 'package:spotube/pages/library/user_local_tracks/local_folder.dart'
- as _i15;
+ as _i13;
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'
- as _i40;
-import 'package:spotube/pages/library/user_playlists.dart' as _i41;
-import 'package:spotube/pages/lyrics/lyrics.dart' as _i17;
-import 'package:spotube/pages/lyrics/mini_lyrics.dart' as _i18;
-import 'package:spotube/pages/mobile_login/mobile_login.dart' as _i42;
-import 'package:spotube/pages/player/lyrics.dart' as _i19;
-import 'package:spotube/pages/player/queue.dart' as _i20;
-import 'package:spotube/pages/player/sources.dart' as _i21;
-import 'package:spotube/pages/playlist/liked_playlist.dart' as _i14;
-import 'package:spotube/pages/playlist/playlist.dart' as _i24;
-import 'package:spotube/pages/profile/profile.dart' as _i25;
-import 'package:spotube/pages/root/root_app.dart' as _i26;
-import 'package:spotube/pages/search/search.dart' as _i27;
+ as _i39;
+import 'package:spotube/pages/library/user_playlists.dart' as _i40;
+import 'package:spotube/pages/lyrics/lyrics.dart' as _i15;
+import 'package:spotube/pages/lyrics/mini_lyrics.dart' as _i16;
+import 'package:spotube/pages/player/lyrics.dart' as _i17;
+import 'package:spotube/pages/player/queue.dart' as _i18;
+import 'package:spotube/pages/player/sources.dart' as _i19;
+import 'package:spotube/pages/playlist/liked_playlist.dart' as _i12;
+import 'package:spotube/pages/playlist/playlist.dart' as _i20;
+import 'package:spotube/pages/profile/profile.dart' as _i21;
+import 'package:spotube/pages/root/root_app.dart' as _i22;
+import 'package:spotube/pages/search/search.dart' as _i23;
import 'package:spotube/pages/settings/about.dart' as _i1;
import 'package:spotube/pages/settings/blacklist.dart' as _i4;
-import 'package:spotube/pages/settings/logs.dart' as _i16;
-import 'package:spotube/pages/settings/settings.dart' as _i28;
-import 'package:spotube/pages/stats/albums/albums.dart' as _i29;
-import 'package:spotube/pages/stats/artists/artists.dart' as _i30;
-import 'package:spotube/pages/stats/fees/fees.dart' as _i34;
-import 'package:spotube/pages/stats/minutes/minutes.dart' as _i31;
-import 'package:spotube/pages/stats/playlists/playlists.dart' as _i33;
-import 'package:spotube/pages/stats/stats.dart' as _i32;
-import 'package:spotube/pages/stats/streams/streams.dart' as _i35;
-import 'package:spotube/pages/track/track.dart' as _i36;
+import 'package:spotube/pages/settings/logs.dart' as _i14;
+import 'package:spotube/pages/settings/metadata/metadata_form.dart' as _i24;
+import 'package:spotube/pages/settings/metadata_plugins.dart' as _i25;
+import 'package:spotube/pages/settings/scrobbling/scrobbling.dart' as _i27;
+import 'package:spotube/pages/settings/settings.dart' as _i26;
+import 'package:spotube/pages/stats/albums/albums.dart' as _i28;
+import 'package:spotube/pages/stats/artists/artists.dart' as _i29;
+import 'package:spotube/pages/stats/fees/fees.dart' as _i33;
+import 'package:spotube/pages/stats/minutes/minutes.dart' as _i30;
+import 'package:spotube/pages/stats/playlists/playlists.dart' as _i32;
+import 'package:spotube/pages/stats/stats.dart' as _i31;
+import 'package:spotube/pages/stats/streams/streams.dart' as _i34;
+import 'package:spotube/pages/track/track.dart' as _i35;
/// generated route for
/// [_i1.AboutSpotubePage]
-class AboutSpotubeRoute extends _i43.PageRouteInfo {
- const AboutSpotubeRoute({List<_i43.PageRouteInfo>? children})
+class AboutSpotubeRoute extends _i41.PageRouteInfo {
+ const AboutSpotubeRoute({List<_i41.PageRouteInfo>? children})
: super(
AboutSpotubeRoute.name,
initialChildren: children,
@@ -71,7 +66,7 @@ class AboutSpotubeRoute extends _i43.PageRouteInfo {
static const String name = 'AboutSpotubeRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
return const _i1.AboutSpotubePage();
@@ -81,12 +76,12 @@ class AboutSpotubeRoute extends _i43.PageRouteInfo {
/// generated route for
/// [_i2.AlbumPage]
-class AlbumRoute extends _i43.PageRouteInfo {
+class AlbumRoute extends _i41.PageRouteInfo {
AlbumRoute({
- _i44.Key? key,
+ _i42.Key? key,
required String id,
- required _i45.AlbumSimple album,
- List<_i43.PageRouteInfo>? children,
+ required _i43.SpotubeSimpleAlbumObject album,
+ List<_i41.PageRouteInfo>? children,
}) : super(
AlbumRoute.name,
args: AlbumRouteArgs(
@@ -100,7 +95,7 @@ class AlbumRoute extends _i43.PageRouteInfo {
static const String name = 'AlbumRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final args = data.argsAs();
@@ -120,11 +115,11 @@ class AlbumRouteArgs {
required this.album,
});
- final _i44.Key? key;
+ final _i42.Key? key;
final String id;
- final _i45.AlbumSimple album;
+ final _i43.SpotubeSimpleAlbumObject album;
@override
String toString() {
@@ -134,11 +129,11 @@ class AlbumRouteArgs {
/// generated route for
/// [_i3.ArtistPage]
-class ArtistRoute extends _i43.PageRouteInfo {
+class ArtistRoute extends _i41.PageRouteInfo {
ArtistRoute({
required String artistId,
- _i44.Key? key,
- List<_i43.PageRouteInfo>? children,
+ _i42.Key? key,
+ List<_i41.PageRouteInfo>? children,
}) : super(
ArtistRoute.name,
args: ArtistRouteArgs(
@@ -151,7 +146,7 @@ class ArtistRoute extends _i43.PageRouteInfo {
static const String name = 'ArtistRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final pathParams = data.inheritedPathParams;
@@ -173,7 +168,7 @@ class ArtistRouteArgs {
final String artistId;
- final _i44.Key? key;
+ final _i42.Key? key;
@override
String toString() {
@@ -183,8 +178,8 @@ class ArtistRouteArgs {
/// generated route for
/// [_i4.BlackListPage]
-class BlackListRoute extends _i43.PageRouteInfo {
- const BlackListRoute({List<_i43.PageRouteInfo>? children})
+class BlackListRoute extends _i41.PageRouteInfo {
+ const BlackListRoute({List<_i41.PageRouteInfo>? children})
: super(
BlackListRoute.name,
initialChildren: children,
@@ -192,7 +187,7 @@ class BlackListRoute extends _i43.PageRouteInfo {
static const String name = 'BlackListRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
return const _i4.BlackListPage();
@@ -202,8 +197,8 @@ class BlackListRoute extends _i43.PageRouteInfo {
/// generated route for
/// [_i5.ConnectControlPage]
-class ConnectControlRoute extends _i43.PageRouteInfo {
- const ConnectControlRoute({List<_i43.PageRouteInfo>? children})
+class ConnectControlRoute extends _i41.PageRouteInfo {
+ const ConnectControlRoute({List<_i41.PageRouteInfo>? children})
: super(
ConnectControlRoute.name,
initialChildren: children,
@@ -211,7 +206,7 @@ class ConnectControlRoute extends _i43.PageRouteInfo {
static const String name = 'ConnectControlRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
return const _i5.ConnectControlPage();
@@ -221,8 +216,8 @@ class ConnectControlRoute extends _i43.PageRouteInfo {
/// generated route for
/// [_i6.ConnectPage]
-class ConnectRoute extends _i43.PageRouteInfo {
- const ConnectRoute({List<_i43.PageRouteInfo>? children})
+class ConnectRoute extends _i41.PageRouteInfo {
+ const ConnectRoute({List<_i41.PageRouteInfo>? children})
: super(
ConnectRoute.name,
initialChildren: children,
@@ -230,7 +225,7 @@ class ConnectRoute extends _i43.PageRouteInfo {
static const String name = 'ConnectRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
return const _i6.ConnectPage();
@@ -239,81 +234,9 @@ class ConnectRoute extends _i43.PageRouteInfo {
}
/// generated route for
-/// [_i7.GenrePage]
-class GenreRoute extends _i43.PageRouteInfo {
- const GenreRoute({List<_i43.PageRouteInfo>? children})
- : super(
- GenreRoute.name,
- initialChildren: children,
- );
-
- static const String name = 'GenreRoute';
-
- static _i43.PageInfo page = _i43.PageInfo(
- name,
- builder: (data) {
- return const _i7.GenrePage();
- },
- );
-}
-
-/// generated route for
-/// [_i8.GenrePlaylistsPage]
-class GenrePlaylistsRoute extends _i43.PageRouteInfo {
- GenrePlaylistsRoute({
- _i44.Key? key,
- required String id,
- required _i45.Category category,
- List<_i43.PageRouteInfo>? children,
- }) : super(
- GenrePlaylistsRoute.name,
- args: GenrePlaylistsRouteArgs(
- key: key,
- id: id,
- category: category,
- ),
- rawPathParams: {'categoryId': id},
- initialChildren: children,
- );
-
- static const String name = 'GenrePlaylistsRoute';
-
- static _i43.PageInfo page = _i43.PageInfo(
- name,
- builder: (data) {
- final args = data.argsAs();
- return _i8.GenrePlaylistsPage(
- key: args.key,
- id: args.id,
- category: args.category,
- );
- },
- );
-}
-
-class GenrePlaylistsRouteArgs {
- const GenrePlaylistsRouteArgs({
- this.key,
- required this.id,
- required this.category,
- });
-
- final _i44.Key? key;
-
- final String id;
-
- final _i45.Category category;
-
- @override
- String toString() {
- return 'GenrePlaylistsRouteArgs{key: $key, id: $id, category: $category}';
- }
-}
-
-/// generated route for
-/// [_i9.GettingStartedPage]
-class GettingStartedRoute extends _i43.PageRouteInfo {
- const GettingStartedRoute({List<_i43.PageRouteInfo>? children})
+/// [_i7.GettingStartedPage]
+class GettingStartedRoute extends _i41.PageRouteInfo {
+ const GettingStartedRoute({List<_i41.PageRouteInfo>? children})
: super(
GettingStartedRoute.name,
initialChildren: children,
@@ -321,69 +244,72 @@ class GettingStartedRoute extends _i43.PageRouteInfo {
static const String name = 'GettingStartedRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i9.GettingStartedPage();
+ return const _i7.GettingStartedPage();
},
);
}
/// generated route for
-/// [_i10.HomeFeedSectionPage]
-class HomeFeedSectionRoute
- extends _i43.PageRouteInfo {
- HomeFeedSectionRoute({
- _i46.Key? key,
- required String sectionUri,
- List<_i43.PageRouteInfo>? children,
+/// [_i8.HomeBrowseSectionItemsPage]
+class HomeBrowseSectionItemsRoute
+ extends _i41.PageRouteInfo {
+ HomeBrowseSectionItemsRoute({
+ _i44.Key? key,
+ required String sectionId,
+ required _i43.SpotubeBrowseSectionObject section,
+ List<_i41.PageRouteInfo>? children,
}) : super(
- HomeFeedSectionRoute.name,
- args: HomeFeedSectionRouteArgs(
+ HomeBrowseSectionItemsRoute.name,
+ args: HomeBrowseSectionItemsRouteArgs(
key: key,
- sectionUri: sectionUri,
+ sectionId: sectionId,
+ section: section,
),
- rawPathParams: {'feedId': sectionUri},
+ rawPathParams: {'sectionId': sectionId},
initialChildren: children,
);
- static const String name = 'HomeFeedSectionRoute';
+ static const String name = 'HomeBrowseSectionItemsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- final pathParams = data.inheritedPathParams;
- final args = data.argsAs(
- orElse: () => HomeFeedSectionRouteArgs(
- sectionUri: pathParams.getString('feedId')));
- return _i10.HomeFeedSectionPage(
+ final args = data.argsAs();
+ return _i8.HomeBrowseSectionItemsPage(
key: args.key,
- sectionUri: args.sectionUri,
+ sectionId: args.sectionId,
+ section: args.section,
);
},
);
}
-class HomeFeedSectionRouteArgs {
- const HomeFeedSectionRouteArgs({
+class HomeBrowseSectionItemsRouteArgs {
+ const HomeBrowseSectionItemsRouteArgs({
this.key,
- required this.sectionUri,
+ required this.sectionId,
+ required this.section,
});
- final _i46.Key? key;
+ final _i44.Key? key;
- final String sectionUri;
+ final String sectionId;
+
+ final _i43.SpotubeBrowseSectionObject section;
@override
String toString() {
- return 'HomeFeedSectionRouteArgs{key: $key, sectionUri: $sectionUri}';
+ return 'HomeBrowseSectionItemsRouteArgs{key: $key, sectionId: $sectionId, section: $section}';
}
}
/// generated route for
-/// [_i11.HomePage]
-class HomeRoute extends _i43.PageRouteInfo {
- const HomeRoute({List<_i43.PageRouteInfo>? children})
+/// [_i9.HomePage]
+class HomeRoute extends _i41.PageRouteInfo {
+ const HomeRoute({List<_i41.PageRouteInfo>? children})
: super(
HomeRoute.name,
initialChildren: children,
@@ -391,18 +317,18 @@ class HomeRoute extends _i43.PageRouteInfo {
static const String name = 'HomeRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i11.HomePage();
+ return const _i9.HomePage();
},
);
}
/// generated route for
-/// [_i12.LastFMLoginPage]
-class LastFMLoginRoute extends _i43.PageRouteInfo {
- const LastFMLoginRoute({List<_i43.PageRouteInfo>? children})
+/// [_i10.LastFMLoginPage]
+class LastFMLoginRoute extends _i41.PageRouteInfo {
+ const LastFMLoginRoute({List<_i41.PageRouteInfo>? children})
: super(
LastFMLoginRoute.name,
initialChildren: children,
@@ -410,18 +336,18 @@ class LastFMLoginRoute extends _i43.PageRouteInfo {
static const String name = 'LastFMLoginRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i12.LastFMLoginPage();
+ return const _i10.LastFMLoginPage();
},
);
}
/// generated route for
-/// [_i13.LibraryPage]
-class LibraryRoute extends _i43.PageRouteInfo {
- const LibraryRoute({List<_i43.PageRouteInfo>? children})
+/// [_i11.LibraryPage]
+class LibraryRoute extends _i41.PageRouteInfo {
+ const LibraryRoute({List<_i41.PageRouteInfo>? children})
: super(
LibraryRoute.name,
initialChildren: children,
@@ -429,21 +355,21 @@ class LibraryRoute extends _i43.PageRouteInfo {
static const String name = 'LibraryRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i13.LibraryPage();
+ return const _i11.LibraryPage();
},
);
}
/// generated route for
-/// [_i14.LikedPlaylistPage]
-class LikedPlaylistRoute extends _i43.PageRouteInfo {
+/// [_i12.LikedPlaylistPage]
+class LikedPlaylistRoute extends _i41.PageRouteInfo {
LikedPlaylistRoute({
- _i44.Key? key,
- required _i45.PlaylistSimple playlist,
- List<_i43.PageRouteInfo>? children,
+ _i42.Key? key,
+ required _i43.SpotubeSimplePlaylistObject playlist,
+ List<_i41.PageRouteInfo>? children,
}) : super(
LikedPlaylistRoute.name,
args: LikedPlaylistRouteArgs(
@@ -455,11 +381,11 @@ class LikedPlaylistRoute extends _i43.PageRouteInfo {
static const String name = 'LikedPlaylistRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final args = data.argsAs();
- return _i14.LikedPlaylistPage(
+ return _i12.LikedPlaylistPage(
key: args.key,
playlist: args.playlist,
);
@@ -473,9 +399,9 @@ class LikedPlaylistRouteArgs {
required this.playlist,
});
- final _i44.Key? key;
+ final _i42.Key? key;
- final _i45.PlaylistSimple playlist;
+ final _i43.SpotubeSimplePlaylistObject playlist;
@override
String toString() {
@@ -484,14 +410,14 @@ class LikedPlaylistRouteArgs {
}
/// generated route for
-/// [_i15.LocalLibraryPage]
-class LocalLibraryRoute extends _i43.PageRouteInfo {
+/// [_i13.LocalLibraryPage]
+class LocalLibraryRoute extends _i41.PageRouteInfo {
LocalLibraryRoute({
required String location,
- _i44.Key? key,
+ _i42.Key? key,
bool isDownloads = false,
bool isCache = false,
- List<_i43.PageRouteInfo>? children,
+ List<_i41.PageRouteInfo>? children,
}) : super(
LocalLibraryRoute.name,
args: LocalLibraryRouteArgs(
@@ -505,11 +431,11 @@ class LocalLibraryRoute extends _i43.PageRouteInfo {
static const String name = 'LocalLibraryRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final args = data.argsAs();
- return _i15.LocalLibraryPage(
+ return _i13.LocalLibraryPage(
args.location,
key: args.key,
isDownloads: args.isDownloads,
@@ -529,7 +455,7 @@ class LocalLibraryRouteArgs {
final String location;
- final _i44.Key? key;
+ final _i42.Key? key;
final bool isDownloads;
@@ -542,9 +468,9 @@ class LocalLibraryRouteArgs {
}
/// generated route for
-/// [_i16.LogsPage]
-class LogsRoute extends _i43.PageRouteInfo {
- const LogsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i14.LogsPage]
+class LogsRoute extends _i41.PageRouteInfo {
+ const LogsRoute({List<_i41.PageRouteInfo>? children})
: super(
LogsRoute.name,
initialChildren: children,
@@ -552,18 +478,18 @@ class LogsRoute extends _i43.PageRouteInfo {
static const String name = 'LogsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i16.LogsPage();
+ return const _i14.LogsPage();
},
);
}
/// generated route for
-/// [_i17.LyricsPage]
-class LyricsRoute extends _i43.PageRouteInfo {
- const LyricsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i15.LyricsPage]
+class LyricsRoute extends _i41.PageRouteInfo {
+ const LyricsRoute({List<_i41.PageRouteInfo>? children})
: super(
LyricsRoute.name,
initialChildren: children,
@@ -571,21 +497,21 @@ class LyricsRoute extends _i43.PageRouteInfo {
static const String name = 'LyricsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i17.LyricsPage();
+ return const _i15.LyricsPage();
},
);
}
/// generated route for
-/// [_i18.MiniLyricsPage]
-class MiniLyricsRoute extends _i43.PageRouteInfo {
+/// [_i16.MiniLyricsPage]
+class MiniLyricsRoute extends _i41.PageRouteInfo {
MiniLyricsRoute({
- _i46.Key? key,
- required _i46.Size prevSize,
- List<_i43.PageRouteInfo>? children,
+ _i44.Key? key,
+ required _i44.Size prevSize,
+ List<_i41.PageRouteInfo>? children,
}) : super(
MiniLyricsRoute.name,
args: MiniLyricsRouteArgs(
@@ -597,11 +523,11 @@ class MiniLyricsRoute extends _i43.PageRouteInfo {
static const String name = 'MiniLyricsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final args = data.argsAs();
- return _i18.MiniLyricsPage(
+ return _i16.MiniLyricsPage(
key: args.key,
prevSize: args.prevSize,
);
@@ -615,9 +541,9 @@ class MiniLyricsRouteArgs {
required this.prevSize,
});
- final _i46.Key? key;
+ final _i44.Key? key;
- final _i46.Size prevSize;
+ final _i44.Size prevSize;
@override
String toString() {
@@ -626,9 +552,9 @@ class MiniLyricsRouteArgs {
}
/// generated route for
-/// [_i19.PlayerLyricsPage]
-class PlayerLyricsRoute extends _i43.PageRouteInfo {
- const PlayerLyricsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i17.PlayerLyricsPage]
+class PlayerLyricsRoute extends _i41.PageRouteInfo {
+ const PlayerLyricsRoute({List<_i41.PageRouteInfo>? children})
: super(
PlayerLyricsRoute.name,
initialChildren: children,
@@ -636,18 +562,18 @@ class PlayerLyricsRoute extends _i43.PageRouteInfo {
static const String name = 'PlayerLyricsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i19.PlayerLyricsPage();
+ return const _i17.PlayerLyricsPage();
},
);
}
/// generated route for
-/// [_i20.PlayerQueuePage]
-class PlayerQueueRoute extends _i43.PageRouteInfo {
- const PlayerQueueRoute({List<_i43.PageRouteInfo>? children})
+/// [_i18.PlayerQueuePage]
+class PlayerQueueRoute extends _i41.PageRouteInfo {
+ const PlayerQueueRoute({List<_i41.PageRouteInfo>? children})
: super(
PlayerQueueRoute.name,
initialChildren: children,
@@ -655,18 +581,18 @@ class PlayerQueueRoute extends _i43.PageRouteInfo {
static const String name = 'PlayerQueueRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i20.PlayerQueuePage();
+ return const _i18.PlayerQueuePage();
},
);
}
/// generated route for
-/// [_i21.PlayerTrackSourcesPage]
-class PlayerTrackSourcesRoute extends _i43.PageRouteInfo {
- const PlayerTrackSourcesRoute({List<_i43.PageRouteInfo>? children})
+/// [_i19.PlayerTrackSourcesPage]
+class PlayerTrackSourcesRoute extends _i41.PageRouteInfo {
+ const PlayerTrackSourcesRoute({List<_i41.PageRouteInfo>? children})
: super(
PlayerTrackSourcesRoute.name,
initialChildren: children,
@@ -674,88 +600,22 @@ class PlayerTrackSourcesRoute extends _i43.PageRouteInfo {
static const String name = 'PlayerTrackSourcesRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i21.PlayerTrackSourcesPage();
+ return const _i19.PlayerTrackSourcesPage();
},
);
}
/// generated route for
-/// [_i22.PlaylistGenerateResultPage]
-class PlaylistGenerateResultRoute
- extends _i43.PageRouteInfo {
- PlaylistGenerateResultRoute({
- _i46.Key? key,
- required _i47.GeneratePlaylistProviderInput state,
- List<_i43.PageRouteInfo>? children,
- }) : super(
- PlaylistGenerateResultRoute.name,
- args: PlaylistGenerateResultRouteArgs(
- key: key,
- state: state,
- ),
- initialChildren: children,
- );
-
- static const String name = 'PlaylistGenerateResultRoute';
-
- static _i43.PageInfo page = _i43.PageInfo(
- name,
- builder: (data) {
- final args = data.argsAs();
- return _i22.PlaylistGenerateResultPage(
- key: args.key,
- state: args.state,
- );
- },
- );
-}
-
-class PlaylistGenerateResultRouteArgs {
- const PlaylistGenerateResultRouteArgs({
- this.key,
- required this.state,
- });
-
- final _i46.Key? key;
-
- final _i47.GeneratePlaylistProviderInput state;
-
- @override
- String toString() {
- return 'PlaylistGenerateResultRouteArgs{key: $key, state: $state}';
- }
-}
-
-/// generated route for
-/// [_i23.PlaylistGeneratorPage]
-class PlaylistGeneratorRoute extends _i43.PageRouteInfo {
- const PlaylistGeneratorRoute({List<_i43.PageRouteInfo>? children})
- : super(
- PlaylistGeneratorRoute.name,
- initialChildren: children,
- );
-
- static const String name = 'PlaylistGeneratorRoute';
-
- static _i43.PageInfo page = _i43.PageInfo(
- name,
- builder: (data) {
- return const _i23.PlaylistGeneratorPage();
- },
- );
-}
-
-/// generated route for
-/// [_i24.PlaylistPage]
-class PlaylistRoute extends _i43.PageRouteInfo {
+/// [_i20.PlaylistPage]
+class PlaylistRoute extends _i41.PageRouteInfo {
PlaylistRoute({
- _i44.Key? key,
+ _i42.Key? key,
required String id,
- required _i45.PlaylistSimple playlist,
- List<_i43.PageRouteInfo>? children,
+ required _i43.SpotubeSimplePlaylistObject playlist,
+ List<_i41.PageRouteInfo>? children,
}) : super(
PlaylistRoute.name,
args: PlaylistRouteArgs(
@@ -769,11 +629,11 @@ class PlaylistRoute extends _i43.PageRouteInfo {
static const String name = 'PlaylistRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final args = data.argsAs();
- return _i24.PlaylistPage(
+ return _i20.PlaylistPage(
key: args.key,
id: args.id,
playlist: args.playlist,
@@ -789,11 +649,11 @@ class PlaylistRouteArgs {
required this.playlist,
});
- final _i44.Key? key;
+ final _i42.Key? key;
final String id;
- final _i45.PlaylistSimple playlist;
+ final _i43.SpotubeSimplePlaylistObject playlist;
@override
String toString() {
@@ -802,9 +662,9 @@ class PlaylistRouteArgs {
}
/// generated route for
-/// [_i25.ProfilePage]
-class ProfileRoute extends _i43.PageRouteInfo {
- const ProfileRoute({List<_i43.PageRouteInfo>? children})
+/// [_i21.ProfilePage]
+class ProfileRoute extends _i41.PageRouteInfo {
+ const ProfileRoute({List<_i41.PageRouteInfo>? children})
: super(
ProfileRoute.name,
initialChildren: children,
@@ -812,18 +672,18 @@ class ProfileRoute extends _i43.PageRouteInfo {
static const String name = 'ProfileRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i25.ProfilePage();
+ return const _i21.ProfilePage();
},
);
}
/// generated route for
-/// [_i26.RootAppPage]
-class RootAppRoute extends _i43.PageRouteInfo {
- const RootAppRoute({List<_i43.PageRouteInfo>? children})
+/// [_i22.RootAppPage]
+class RootAppRoute extends _i41.PageRouteInfo {
+ const RootAppRoute({List<_i41.PageRouteInfo>? children})
: super(
RootAppRoute.name,
initialChildren: children,
@@ -831,18 +691,18 @@ class RootAppRoute extends _i43.PageRouteInfo {
static const String name = 'RootAppRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i26.RootAppPage();
+ return const _i22.RootAppPage();
},
);
}
/// generated route for
-/// [_i27.SearchPage]
-class SearchRoute extends _i43.PageRouteInfo {
- const SearchRoute({List<_i43.PageRouteInfo>? children})
+/// [_i23.SearchPage]
+class SearchRoute extends _i41.PageRouteInfo {
+ const SearchRoute({List<_i41.PageRouteInfo>? children})
: super(
SearchRoute.name,
initialChildren: children,
@@ -850,18 +710,90 @@ class SearchRoute extends _i43.PageRouteInfo {
static const String name = 'SearchRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i27.SearchPage();
+ return const _i23.SearchPage();
},
);
}
/// generated route for
-/// [_i28.SettingsPage]
-class SettingsRoute extends _i43.PageRouteInfo {
- const SettingsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i24.SettingsMetadataProviderFormPage]
+class SettingsMetadataProviderFormRoute
+ extends _i41.PageRouteInfo {
+ SettingsMetadataProviderFormRoute({
+ _i44.Key? key,
+ required String title,
+ required List<_i43.MetadataFormFieldObject> fields,
+ List<_i41.PageRouteInfo>? children,
+ }) : super(
+ SettingsMetadataProviderFormRoute.name,
+ args: SettingsMetadataProviderFormRouteArgs(
+ key: key,
+ title: title,
+ fields: fields,
+ ),
+ initialChildren: children,
+ );
+
+ static const String name = 'SettingsMetadataProviderFormRoute';
+
+ static _i41.PageInfo page = _i41.PageInfo(
+ name,
+ builder: (data) {
+ final args = data.argsAs();
+ return _i24.SettingsMetadataProviderFormPage(
+ key: args.key,
+ title: args.title,
+ fields: args.fields,
+ );
+ },
+ );
+}
+
+class SettingsMetadataProviderFormRouteArgs {
+ const SettingsMetadataProviderFormRouteArgs({
+ this.key,
+ required this.title,
+ required this.fields,
+ });
+
+ final _i44.Key? key;
+
+ final String title;
+
+ final List<_i43.MetadataFormFieldObject> fields;
+
+ @override
+ String toString() {
+ return 'SettingsMetadataProviderFormRouteArgs{key: $key, title: $title, fields: $fields}';
+ }
+}
+
+/// generated route for
+/// [_i25.SettingsMetadataProviderPage]
+class SettingsMetadataProviderRoute extends _i41.PageRouteInfo {
+ const SettingsMetadataProviderRoute({List<_i41.PageRouteInfo>? children})
+ : super(
+ SettingsMetadataProviderRoute.name,
+ initialChildren: children,
+ );
+
+ static const String name = 'SettingsMetadataProviderRoute';
+
+ static _i41.PageInfo page = _i41.PageInfo(
+ name,
+ builder: (data) {
+ return const _i25.SettingsMetadataProviderPage();
+ },
+ );
+}
+
+/// generated route for
+/// [_i26.SettingsPage]
+class SettingsRoute extends _i41.PageRouteInfo {
+ const SettingsRoute({List<_i41.PageRouteInfo>? children})
: super(
SettingsRoute.name,
initialChildren: children,
@@ -869,18 +801,37 @@ class SettingsRoute extends _i43.PageRouteInfo {
static const String name = 'SettingsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i28.SettingsPage();
+ return const _i26.SettingsPage();
},
);
}
/// generated route for
-/// [_i29.StatsAlbumsPage]
-class StatsAlbumsRoute extends _i43.PageRouteInfo {
- const StatsAlbumsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i27.SettingsScrobblingPage]
+class SettingsScrobblingRoute extends _i41.PageRouteInfo {
+ const SettingsScrobblingRoute({List<_i41.PageRouteInfo>? children})
+ : super(
+ SettingsScrobblingRoute.name,
+ initialChildren: children,
+ );
+
+ static const String name = 'SettingsScrobblingRoute';
+
+ static _i41.PageInfo page = _i41.PageInfo(
+ name,
+ builder: (data) {
+ return const _i27.SettingsScrobblingPage();
+ },
+ );
+}
+
+/// generated route for
+/// [_i28.StatsAlbumsPage]
+class StatsAlbumsRoute extends _i41.PageRouteInfo {
+ const StatsAlbumsRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsAlbumsRoute.name,
initialChildren: children,
@@ -888,18 +839,18 @@ class StatsAlbumsRoute extends _i43.PageRouteInfo {
static const String name = 'StatsAlbumsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i29.StatsAlbumsPage();
+ return const _i28.StatsAlbumsPage();
},
);
}
/// generated route for
-/// [_i30.StatsArtistsPage]
-class StatsArtistsRoute extends _i43.PageRouteInfo {
- const StatsArtistsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i29.StatsArtistsPage]
+class StatsArtistsRoute extends _i41.PageRouteInfo {
+ const StatsArtistsRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsArtistsRoute.name,
initialChildren: children,
@@ -907,18 +858,18 @@ class StatsArtistsRoute extends _i43.PageRouteInfo {
static const String name = 'StatsArtistsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i30.StatsArtistsPage();
+ return const _i29.StatsArtistsPage();
},
);
}
/// generated route for
-/// [_i31.StatsMinutesPage]
-class StatsMinutesRoute extends _i43.PageRouteInfo {
- const StatsMinutesRoute({List<_i43.PageRouteInfo>? children})
+/// [_i30.StatsMinutesPage]
+class StatsMinutesRoute extends _i41.PageRouteInfo {
+ const StatsMinutesRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsMinutesRoute.name,
initialChildren: children,
@@ -926,18 +877,18 @@ class StatsMinutesRoute extends _i43.PageRouteInfo {
static const String name = 'StatsMinutesRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i31.StatsMinutesPage();
+ return const _i30.StatsMinutesPage();
},
);
}
/// generated route for
-/// [_i32.StatsPage]
-class StatsRoute extends _i43.PageRouteInfo {
- const StatsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i31.StatsPage]
+class StatsRoute extends _i41.PageRouteInfo {
+ const StatsRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsRoute.name,
initialChildren: children,
@@ -945,18 +896,18 @@ class StatsRoute extends _i43.PageRouteInfo {
static const String name = 'StatsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i32.StatsPage();
+ return const _i31.StatsPage();
},
);
}
/// generated route for
-/// [_i33.StatsPlaylistsPage]
-class StatsPlaylistsRoute extends _i43.PageRouteInfo {
- const StatsPlaylistsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i32.StatsPlaylistsPage]
+class StatsPlaylistsRoute extends _i41.PageRouteInfo {
+ const StatsPlaylistsRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsPlaylistsRoute.name,
initialChildren: children,
@@ -964,18 +915,18 @@ class StatsPlaylistsRoute extends _i43.PageRouteInfo {
static const String name = 'StatsPlaylistsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i33.StatsPlaylistsPage();
+ return const _i32.StatsPlaylistsPage();
},
);
}
/// generated route for
-/// [_i34.StatsStreamFeesPage]
-class StatsStreamFeesRoute extends _i43.PageRouteInfo {
- const StatsStreamFeesRoute({List<_i43.PageRouteInfo>? children})
+/// [_i33.StatsStreamFeesPage]
+class StatsStreamFeesRoute extends _i41.PageRouteInfo {
+ const StatsStreamFeesRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsStreamFeesRoute.name,
initialChildren: children,
@@ -983,18 +934,18 @@ class StatsStreamFeesRoute extends _i43.PageRouteInfo {
static const String name = 'StatsStreamFeesRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i34.StatsStreamFeesPage();
+ return const _i33.StatsStreamFeesPage();
},
);
}
/// generated route for
-/// [_i35.StatsStreamsPage]
-class StatsStreamsRoute extends _i43.PageRouteInfo {
- const StatsStreamsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i34.StatsStreamsPage]
+class StatsStreamsRoute extends _i41.PageRouteInfo {
+ const StatsStreamsRoute({List<_i41.PageRouteInfo>? children})
: super(
StatsStreamsRoute.name,
initialChildren: children,
@@ -1002,21 +953,21 @@ class StatsStreamsRoute extends _i43.PageRouteInfo {
static const String name = 'StatsStreamsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i35.StatsStreamsPage();
+ return const _i34.StatsStreamsPage();
},
);
}
/// generated route for
-/// [_i36.TrackPage]
-class TrackRoute extends _i43.PageRouteInfo {
+/// [_i35.TrackPage]
+class TrackRoute extends _i41.PageRouteInfo {
TrackRoute({
- _i46.Key? key,
+ _i44.Key? key,
required String trackId,
- List<_i43.PageRouteInfo>? children,
+ List<_i41.PageRouteInfo>? children,
}) : super(
TrackRoute.name,
args: TrackRouteArgs(
@@ -1029,13 +980,13 @@ class TrackRoute extends _i43.PageRouteInfo {
static const String name = 'TrackRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
final pathParams = data.inheritedPathParams;
final args = data.argsAs(
orElse: () => TrackRouteArgs(trackId: pathParams.getString('id')));
- return _i36.TrackPage(
+ return _i35.TrackPage(
key: args.key,
trackId: args.trackId,
);
@@ -1049,7 +1000,7 @@ class TrackRouteArgs {
required this.trackId,
});
- final _i46.Key? key;
+ final _i44.Key? key;
final String trackId;
@@ -1060,9 +1011,9 @@ class TrackRouteArgs {
}
/// generated route for
-/// [_i37.UserAlbumsPage]
-class UserAlbumsRoute extends _i43.PageRouteInfo {
- const UserAlbumsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i36.UserAlbumsPage]
+class UserAlbumsRoute extends _i41.PageRouteInfo {
+ const UserAlbumsRoute({List<_i41.PageRouteInfo>? children})
: super(
UserAlbumsRoute.name,
initialChildren: children,
@@ -1070,18 +1021,18 @@ class UserAlbumsRoute extends _i43.PageRouteInfo {
static const String name = 'UserAlbumsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i37.UserAlbumsPage();
+ return const _i36.UserAlbumsPage();
},
);
}
/// generated route for
-/// [_i38.UserArtistsPage]
-class UserArtistsRoute extends _i43.PageRouteInfo {
- const UserArtistsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i37.UserArtistsPage]
+class UserArtistsRoute extends _i41.PageRouteInfo {
+ const UserArtistsRoute({List<_i41.PageRouteInfo>? children})
: super(
UserArtistsRoute.name,
initialChildren: children,
@@ -1089,18 +1040,18 @@ class UserArtistsRoute extends _i43.PageRouteInfo {
static const String name = 'UserArtistsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i38.UserArtistsPage();
+ return const _i37.UserArtistsPage();
},
);
}
/// generated route for
-/// [_i39.UserDownloadsPage]
-class UserDownloadsRoute extends _i43.PageRouteInfo {
- const UserDownloadsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i38.UserDownloadsPage]
+class UserDownloadsRoute extends _i41.PageRouteInfo {
+ const UserDownloadsRoute({List<_i41.PageRouteInfo>? children})
: super(
UserDownloadsRoute.name,
initialChildren: children,
@@ -1108,18 +1059,18 @@ class UserDownloadsRoute extends _i43.PageRouteInfo {
static const String name = 'UserDownloadsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i39.UserDownloadsPage();
+ return const _i38.UserDownloadsPage();
},
);
}
/// generated route for
-/// [_i40.UserLocalLibraryPage]
-class UserLocalLibraryRoute extends _i43.PageRouteInfo {
- const UserLocalLibraryRoute({List<_i43.PageRouteInfo>? children})
+/// [_i39.UserLocalLibraryPage]
+class UserLocalLibraryRoute extends _i41.PageRouteInfo {
+ const UserLocalLibraryRoute({List<_i41.PageRouteInfo>? children})
: super(
UserLocalLibraryRoute.name,
initialChildren: children,
@@ -1127,18 +1078,18 @@ class UserLocalLibraryRoute extends _i43.PageRouteInfo {
static const String name = 'UserLocalLibraryRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i40.UserLocalLibraryPage();
+ return const _i39.UserLocalLibraryPage();
},
);
}
/// generated route for
-/// [_i41.UserPlaylistsPage]
-class UserPlaylistsRoute extends _i43.PageRouteInfo {
- const UserPlaylistsRoute({List<_i43.PageRouteInfo>? children})
+/// [_i40.UserPlaylistsPage]
+class UserPlaylistsRoute extends _i41.PageRouteInfo {
+ const UserPlaylistsRoute({List<_i41.PageRouteInfo>? children})
: super(
UserPlaylistsRoute.name,
initialChildren: children,
@@ -1146,29 +1097,10 @@ class UserPlaylistsRoute extends _i43.PageRouteInfo {
static const String name = 'UserPlaylistsRoute';
- static _i43.PageInfo page = _i43.PageInfo(
+ static _i41.PageInfo page = _i41.PageInfo(
name,
builder: (data) {
- return const _i41.UserPlaylistsPage();
- },
- );
-}
-
-/// generated route for
-/// [_i42.WebViewLoginPage]
-class WebViewLoginRoute extends _i43.PageRouteInfo {
- const WebViewLoginRoute({List<_i43.PageRouteInfo>? children})
- : super(
- WebViewLoginRoute.name,
- initialChildren: children,
- );
-
- static const String name = 'WebViewLoginRoute';
-
- static _i43.PageInfo page = _i43.PageInfo(
- name,
- builder: (data) {
- return const _i42.WebViewLoginPage();
+ return const _i40.UserPlaylistsPage();
},
);
}
diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart
index bd9d037c..b10ef7e3 100644
--- a/lib/collections/spotube_icons.dart
+++ b/lib/collections/spotube_icons.dart
@@ -105,7 +105,6 @@ abstract class SpotubeIcons {
static const file = FeatherIcons.file;
static const stream = Icons.stream_rounded;
static const lastFm = SimpleIcons.lastdotfm;
- static const spotify = SimpleIcons.spotify;
static const eye = FeatherIcons.eye;
static const noEye = FeatherIcons.eyeOff;
static const normalize = FeatherIcons.barChart2;
@@ -135,4 +134,9 @@ abstract class SpotubeIcons {
static const list = FeatherIcons.list;
static const device = FeatherIcons.smartphone;
static const engine = FeatherIcons.server;
+ static const extensions = FeatherIcons.package;
+ static const message = FeatherIcons.send;
+ static const upload = FeatherIcons.uploadCloud;
+ static const plugin = Icons.extension_outlined;
+ static const warning = FeatherIcons.alertTriangle;
}
diff --git a/lib/components/dialogs/playlist_add_track_dialog.dart b/lib/components/dialogs/playlist_add_track_dialog.dart
index 5098bf9d..09d831ea 100644
--- a/lib/components/dialogs/playlist_add_track_dialog.dart
+++ b/lib/components/dialogs/playlist_add_track_dialog.dart
@@ -1,18 +1,18 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
+import 'package:spotube/models/metadata/metadata.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/extensions/image.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
+import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
+import 'package:spotube/provider/metadata_plugin/core/user.dart';
class PlaylistAddTrackDialog extends HookConsumerWidget {
/// The id of the playlist this dialog was opened from
final String? openFromPlaylist;
- final List tracks;
+ final List tracks;
const PlaylistAddTrackDialog({
required this.tracks,
required this.openFromPlaylist,
@@ -22,24 +22,23 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final typography = Theme.of(context).typography;
- final userPlaylists = ref.watch(favoritePlaylistsProvider);
+ final userPlaylists = ref.watch(metadataPluginSavedPlaylistsProvider);
final favoritePlaylistsNotifier =
- ref.watch(favoritePlaylistsProvider.notifier);
+ ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
- final me = ref.watch(meProvider);
+ final me = ref.watch(metadataPluginUserProvider);
final filteredPlaylists = useMemoized(
() =>
userPlaylists.asData?.value.items
.where(
(playlist) =>
- playlist.owner?.id != null &&
- playlist.owner!.id == me.asData?.value.id &&
+ playlist.owner.id == me.asData?.value?.id &&
playlist.id != openFromPlaylist,
)
.toList() ??
[],
- [userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist],
+ [userPlaylists.asData?.value, me.asData?.value?.id, openFromPlaylist],
);
final playlistsCheck = useState({});
@@ -60,7 +59,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
selectedPlaylists.map(
(playlistId) => favoritePlaylistsNotifier.addTracks(
playlistId,
- tracks.map((e) => e.id!).toList(),
+ tracks.map((e) => e.id).toList(),
),
),
).then((_) => context.mounted ? Navigator.pop(context, true) : null);
@@ -109,8 +108,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
},
),
leading: Avatar(
- initials:
- Avatar.getInitials(playlist.name ?? "Playlist"),
+ initials: Avatar.getInitials(playlist.name),
provider: UniversalImage.imageProvider(
playlist.images.asUrlString(
placeholder: ImagePlaceholder.collection,
@@ -124,20 +122,20 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
onChanged: (val) {
playlistsCheck.value = {
...playlistsCheck.value,
- playlist.id!: val == CheckboxState.checked,
+ playlist.id: val == CheckboxState.checked,
};
},
),
onPressed: () {
playlistsCheck.value = {
...playlistsCheck.value,
- playlist.id!:
+ playlist.id:
!(playlistsCheck.value[playlist.id] ?? false),
};
},
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
- child: Text(playlist.name!),
+ child: Text(playlist.name),
),
);
},
diff --git a/lib/components/dialogs/replace_downloaded_dialog.dart b/lib/components/dialogs/replace_downloaded_dialog.dart
index 3a0f3a1d..6634a039 100644
--- a/lib/components/dialogs/replace_downloaded_dialog.dart
+++ b/lib/components/dialogs/replace_downloaded_dialog.dart
@@ -1,13 +1,13 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/models/metadata/metadata.dart';
final replaceDownloadedFileState = StateProvider((ref) => null);
class ReplaceDownloadedDialog extends ConsumerWidget {
- final Track track;
+ final SpotubeTrackObject track;
const ReplaceDownloadedDialog({required this.track, super.key});
@override
@@ -16,7 +16,7 @@ class ReplaceDownloadedDialog extends ConsumerWidget {
final replaceAll = ref.watch(replaceDownloadedFileState);
return AlertDialog(
- title: Text(context.l10n.track_exists(track.name ?? "")),
+ title: Text(context.l10n.track_exists(track.name)),
content: RadioGroup(
value: groupValue,
onChanged: (value) {
diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart
index bfb4a318..3d3fd7e9 100644
--- a/lib/components/dialogs/track_details_dialog.dart
+++ b/lib/components/dialogs/track_details_dialog.dart
@@ -1,53 +1,52 @@
-import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/hyper_link.dart';
-import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/extensions/duration.dart';
+import 'package:spotube/models/metadata/metadata.dart';
+import 'package:spotube/models/playback/track_sources.dart';
+import 'package:spotube/provider/server/track_sources.dart';
-class TrackDetailsDialog extends HookWidget {
- final Track track;
+class TrackDetailsDialog extends HookConsumerWidget {
+ final SpotubeFullTrackObject track;
const TrackDetailsDialog({
super.key,
required this.track,
});
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
+ final sourcedTrack =
+ ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track)));
final detailsMap = {
- context.l10n.title: track.name!,
+ context.l10n.title: track.name,
context.l10n.artist: ArtistLink(
- artists: track.artists ?? [],
+ artists: track.artists,
mainAxisAlignment: WrapAlignment.start,
textStyle: const TextStyle(color: Colors.blue),
hideOverflowArtist: false,
),
- context.l10n.album: LinkText(
- track.album!.name!,
- AlbumRoute(album: track.album!, id: track.album!.id!),
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(color: Colors.blue),
- ),
- context.l10n.duration: (track is SourcedTrack
- ? (track as SourcedTrack).sourceInfo.duration
- : track.duration!)
- .toHumanReadableString(),
- if (track.album!.releaseDate != null)
- context.l10n.released: track.album!.releaseDate,
- context.l10n.popularity: track.popularity?.toString() ?? "0",
+ // context.l10n.album: LinkText(
+ // track.album!.name!,
+ // AlbumRoute(album: track.album!, id: track.album!.id!),
+ // overflow: TextOverflow.ellipsis,
+ // style: const TextStyle(color: Colors.blue),
+ // ),
+ context.l10n.duration: sourcedTrack.asData != null
+ ? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs)
+ .toHumanReadableString()
+ : Duration(milliseconds: track.durationMs).toHumanReadableString(),
+ if (track.album.releaseDate != null)
+ context.l10n.released: track.album.releaseDate,
};
- final sourceInfo =
- track is SourcedTrack ? (track as SourcedTrack).sourceInfo : null;
+ final sourceInfo = sourcedTrack.asData?.value.info;
final ytTracksDetailsMap = sourceInfo == null
? {}
@@ -58,18 +57,14 @@ class TrackDetailsDialog extends HookWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
- context.l10n.channel: Hyperlink(
- sourceInfo.artist,
- sourceInfo.artistUrl,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- context.l10n.streamUrl: Hyperlink(
- (track as SourcedTrack).url,
- (track as SourcedTrack).url,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
+ context.l10n.channel: Text(sourceInfo.artists),
+ if (sourcedTrack.asData?.value.url != null)
+ context.l10n.streamUrl: Hyperlink(
+ sourcedTrack.asData!.value.url ?? "",
+ sourcedTrack.asData!.value.url ?? "",
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
};
return AlertDialog(
diff --git a/lib/components/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart
index 293df932..cb6028a7 100644
--- a/lib/components/fallbacks/anonymous_fallback.dart
+++ b/lib/components/fallbacks/anonymous_fallback.dart
@@ -5,8 +5,8 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/provider/metadata_plugin/core/auth.dart';
-import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart';
class AnonymousFallback extends ConsumerWidget {
@@ -18,13 +18,13 @@ class AnonymousFallback extends ConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final isLoggedIn = ref.watch(authenticationProvider);
+ final isLoggedIn = ref.watch(metadataPluginAuthenticatedProvider);
if (isLoggedIn.isLoading) {
return const Center(child: CircularProgressIndicator());
}
- if (isLoggedIn.asData?.value != null && child != null) return child!;
+ if (isLoggedIn.asData?.value == true && child != null) return child!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -39,7 +39,7 @@ class AnonymousFallback extends ConsumerWidget {
),
Text(context.l10n.not_logged_in),
Button.primary(
- child: Text(context.l10n.login_with_spotify),
+ child: Text(context.l10n.login),
onPressed: () => context.navigateTo(const SettingsRoute()),
)
],
diff --git a/lib/components/fallbacks/error_box.dart b/lib/components/fallbacks/error_box.dart
new file mode 100644
index 00000000..fd56cb58
--- /dev/null
+++ b/lib/components/fallbacks/error_box.dart
@@ -0,0 +1,138 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:shadcn_flutter/shadcn_flutter.dart';
+import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/extensions/context.dart';
+
+class ErrorBox extends StatelessWidget {
+ final Object error;
+ final VoidCallback? onRetry;
+ const ErrorBox({
+ super.key,
+ required this.error,
+ this.onRetry,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ // Make a monospace error log view. Make sure it's only 4 lines
+ return ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 400),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ spacing: 12,
+ children: [
+ Basic(
+ leading: const Icon(SpotubeIcons.error),
+ contentSpacing: 8,
+ title: Text(context.l10n.an_error_occurred),
+ ),
+ Card(
+ padding: const EdgeInsets.all(8.0),
+ filled: true,
+ fillColor: context.theme.colorScheme.muted,
+ child: Text(
+ error.toString(),
+ style: TextStyle(
+ // Use monospace
+ fontFamily: 'Ubuntu Mono',
+ color: context.theme.colorScheme.mutedForeground,
+ fontSize: 14,
+ ),
+ maxLines: 6,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ // Show a dialog with full log and a retry button as well
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Button.text(
+ leading: const Icon(SpotubeIcons.logs),
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth: 480,
+ maxHeight:
+ MediaQuery.of(context).size.height * 0.8,
+ ),
+ child: AlertDialog(
+ padding: const EdgeInsets.all(12),
+ title: Row(
+ spacing: 8,
+ children: [
+ const Icon(SpotubeIcons.logs),
+ Text(context.l10n.logs),
+ const Spacer(),
+ IconButton.ghost(
+ icon: const Icon(SpotubeIcons.close),
+ onPressed: () => context.maybePop(),
+ )
+ ],
+ ),
+ actions: [
+ HookBuilder(builder: (context) {
+ final copied = useState(false);
+
+ return Button.ghost(
+ leading: copied.value
+ ? const Icon(SpotubeIcons.done)
+ : const Icon(SpotubeIcons.clipboard),
+ child: Text(context.l10n.copy_to_clipboard),
+ onPressed: () {
+ Clipboard.setData(
+ ClipboardData(text: error.toString()),
+ );
+ copied.value = true;
+ },
+ );
+ })
+ ],
+ content: SingleChildScrollView(
+ child: Card(
+ padding: const EdgeInsets.all(8.0),
+ filled: true,
+ fillColor: context.theme.colorScheme.muted,
+ child: SelectableText(
+ error.toString(),
+ style: TextStyle(
+ // Use monospace
+ fontFamily: 'Ubuntu Mono',
+ color: context
+ .theme.colorScheme.mutedForeground,
+ fontSize: 16,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ },
+ child: Text(context.l10n.view_logs),
+ ),
+ if (onRetry != null)
+ Button.text(
+ leading: const Icon(SpotubeIcons.refresh),
+ onPressed: onRetry,
+ child: Text(context.l10n.retry),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/components/fallbacks/no_default_metadata_plugin.dart b/lib/components/fallbacks/no_default_metadata_plugin.dart
new file mode 100644
index 00000000..1cabcdb1
--- /dev/null
+++ b/lib/components/fallbacks/no_default_metadata_plugin.dart
@@ -0,0 +1,42 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:auto_size_text/auto_size_text.dart';
+import 'package:flutter_undraw/flutter_undraw.dart';
+import 'package:shadcn_flutter/shadcn_flutter.dart';
+import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
+import 'package:spotube/collections/routes.gr.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/extensions/context.dart';
+
+class NoDefaultMetadataPlugin extends StatelessWidget {
+ const NoDefaultMetadataPlugin({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 10,
+ children: [
+ Undraw(
+ height: 200 * context.theme.scaling,
+ illustration: UndrawIllustration.stars,
+ color: context.theme.colorScheme.primary,
+ ),
+ AutoSizeText(
+ context.l10n.no_default_metadata_provider_selected,
+ style: context.theme.typography.h4,
+ maxLines: 1,
+ ),
+ Button.primary(
+ leading: const Icon(SpotubeIcons.extensions),
+ child: Text(context.l10n.manage_metadata_providers),
+ onPressed: () {
+ context.pushRoute(const SettingsMetadataProviderRoute());
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/form/text_form_field.dart b/lib/components/form/text_form_field.dart
index b07c33e3..dc92c257 100644
--- a/lib/components/form/text_form_field.dart
+++ b/lib/components/form/text_form_field.dart
@@ -14,7 +14,7 @@ class TextFormBuilderField extends StatelessWidget {
// final AlignmentGeometry? placeholderAlignment;
// final AlignmentGeometry? leadingAlignment;
// final AlignmentGeometry? trailingAlignment;
- final bool border;
+ final Border? border;
final List features;
final EdgeInsetsGeometry? padding;
final ValueChanged? onSubmitted;
@@ -61,7 +61,7 @@ class TextFormBuilderField extends StatelessWidget {
this.minLines,
this.filled = false,
this.placeholder,
- this.border = true,
+ this.border,
this.padding,
this.onSubmitted,
this.onEditingComplete,
diff --git a/lib/components/heart_button/heart_button.dart b/lib/components/heart_button/heart_button.dart
index 275d5db1..14a0572f 100644
--- a/lib/components/heart_button/heart_button.dart
+++ b/lib/components/heart_button/heart_button.dart
@@ -1,11 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/provider/authentication/authentication.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
+import 'package:spotube/models/metadata/metadata.dart';
+import 'package:spotube/provider/metadata_plugin/core/auth.dart';
+import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
+import 'package:spotube/provider/metadata_plugin/core/user.dart';
class HeartButton extends HookConsumerWidget {
final bool isLiked;
@@ -28,15 +29,16 @@ class HeartButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final auth = ref.watch(authenticationProvider);
+ final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
- if (auth.asData?.value == null) return const SizedBox.shrink();
+ if (authenticated.asData?.value != true) return const SizedBox.shrink();
return Tooltip(
tooltip: TooltipContainer(child: Text(tooltip ?? "")).call,
child: IconButton(
variance: variance,
size: size,
+ enabled: onPressed != null,
icon: AnimatedSwitcher(
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
@@ -63,7 +65,7 @@ class HeartButton extends HookConsumerWidget {
}
class TrackHeartButton extends HookConsumerWidget {
- final Track track;
+ final SpotubeTrackObject track;
const TrackHeartButton({
super.key,
required this.track,
@@ -71,9 +73,10 @@ class TrackHeartButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final savedTracks = ref.watch(likedTracksProvider);
- final me = ref.watch(meProvider);
- final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
+ final savedTracks = ref.watch(metadataPluginSavedTracksProvider);
+ final me = ref.watch(metadataPluginUserProvider);
+ final (:isLiked, :isLoading, :toggleTrackLike) =
+ useTrackToggleLike(track, ref);
if (me.isLoading) {
return const CircularProgressIndicator();
@@ -84,11 +87,11 @@ class TrackHeartButton extends HookConsumerWidget {
? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite,
isLiked: isLiked,
- onPressed: savedTracks.asData?.value != null
- ? () {
+ onPressed: savedTracks.asData?.value == null || isLoading
+ ? null
+ : () {
toggleTrackLike(track);
- }
- : null,
+ },
);
}
}
diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart
index ba5cbee1..af961578 100644
--- a/lib/components/heart_button/use_track_toggle_like.dart
+++ b/lib/components/heart_button/use_track_toggle_like.dart
@@ -1,36 +1,31 @@
-import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/provider/scrobbler/scrobbler.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
+import 'package:spotube/models/metadata/metadata.dart';
+import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
typedef UseTrackToggleLike = ({
bool isLiked,
- Future Function(Track track) toggleTrackLike,
+ bool isLoading,
+ Future Function(SpotubeTrackObject track) toggleTrackLike,
});
-UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
- final savedTracks = ref.watch(likedTracksProvider);
- final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
+UseTrackToggleLike useTrackToggleLike(SpotubeTrackObject track, WidgetRef ref) {
+ final savedTracksNotifier =
+ ref.watch(metadataPluginSavedTracksProvider.notifier);
- final isLiked = useMemoized(
- () =>
- savedTracks.asData?.value.any((element) => element.id == track.id) ??
- false,
- [savedTracks.asData?.value, track.id],
- );
-
- final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
+ final isSavedTrack = ref.watch(metadataPluginIsSavedTrackProvider(track.id));
return (
- isLiked: isLiked,
+ isLiked: isSavedTrack.asData?.value ?? false,
+ isLoading: isSavedTrack.isLoading,
toggleTrackLike: (track) async {
- await savedTracksNotifier.toggleFavorite(track);
+ final isLikedTrack = await ref.read(
+ metadataPluginIsSavedTrackProvider(track.id).future,
+ );
- if (!isLiked) {
- await scrobblerNotifier.love(track);
+ if (isLikedTrack) {
+ await savedTracksNotifier.removeFavorite([track]);
} else {
- await scrobblerNotifier.unlove(track);
+ await savedTracksNotifier.addFavorite([track]);
}
},
);
diff --git a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
index 47fb0f33..3ac90a06 100644
--- a/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
+++ b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
@@ -4,8 +4,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/modules/artist/artist_card.dart';
import 'package:spotube/modules/playlist/playlist_card.dart';
@@ -14,6 +14,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class HorizontalPlaybuttonCardView extends HookWidget {
final Widget title;
final List items;
+ final Widget? error;
final VoidCallback onFetchMore;
final bool isLoadingNextPage;
final bool hasNextPage;
@@ -26,18 +27,21 @@ class HorizontalPlaybuttonCardView extends HookWidget {
required this.onFetchMore,
required this.isLoadingNextPage,
this.titleTrailing,
+ this.error,
super.key,
}) : assert(
items.every(
(item) =>
- item is PlaylistSimple || item is Artist || item is AlbumSimple,
+ item is SpotubeSimpleAlbumObject ||
+ item is SpotubeSimplePlaylistObject ||
+ item is SpotubeFullArtistObject,
),
);
@override
Widget build(BuildContext context) {
final scrollController = useScrollController();
- final isArtist = items.every((s) => s is Artist);
+ final isArtist = items.every((s) => s is SpotubeFullArtistObject);
final scale = context.theme.scaling;
return Padding(
@@ -62,52 +66,57 @@ class HorizontalPlaybuttonCardView extends HookWidget {
if (titleTrailing != null) titleTrailing!,
],
),
- SizedBox(
- height: isArtist ? 250 : 225,
- child: NotificationListener(
- // disable multiple scrollbar to use this
- onNotification: (notification) => true,
- child: ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: PointerDeviceKind.values.toSet(),
- ),
- child: items.isEmpty
- ? ListView.builder(
- scrollDirection: Axis.horizontal,
- itemCount: 5,
- itemBuilder: (context, index) {
- return AlbumCard(FakeData.albumSimple);
- },
- )
- : InfiniteList(
- scrollController: scrollController,
- scrollDirection: Axis.horizontal,
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- itemCount: items.length,
- onFetchData: onFetchMore,
- loadingBuilder: (context) => Skeletonizer(
- enabled: true,
- child: isArtist
- ? ArtistCard(FakeData.artist)
- : AlbumCard(FakeData.albumSimple),
- ),
- isLoading: isLoadingNextPage,
- hasReachedMax: !hasNextPage,
- separatorBuilder: (context, index) => Gap(12 * scale),
- itemBuilder: (context, index) {
- final item = items[index];
+ if (error != null)
+ error!
+ else
+ SizedBox(
+ height: isArtist ? 250 : 225,
+ child: NotificationListener(
+ // disable multiple scrollbar to use this
+ onNotification: (notification) => true,
+ child: ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(
+ dragDevices: PointerDeviceKind.values.toSet(),
+ ),
+ child: items.isEmpty
+ ? ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: 5,
+ itemBuilder: (context, index) {
+ return AlbumCard(FakeData.albumSimple);
+ },
+ )
+ : InfiniteList(
+ scrollController: scrollController,
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ itemCount: items.length,
+ onFetchData: onFetchMore,
+ loadingBuilder: (context) => Skeletonizer(
+ enabled: true,
+ child: isArtist
+ ? ArtistCard(FakeData.artist)
+ : AlbumCard(FakeData.albumSimple),
+ ),
+ isLoading: isLoadingNextPage,
+ hasReachedMax: !hasNextPage,
+ separatorBuilder: (context, index) => Gap(12 * scale),
+ itemBuilder: (context, index) {
+ final item = items[index];
- return switch (item) {
- PlaylistSimple() =>
- PlaylistCard(item as PlaylistSimple),
- AlbumSimple() => AlbumCard(item as AlbumSimple),
- Artist() => ArtistCard(item as Artist),
- _ => const SizedBox.shrink(),
- };
- }),
+ return switch (item) {
+ SpotubeSimplePlaylistObject() => PlaylistCard(
+ item as SpotubeSimplePlaylistObject),
+ SpotubeSimpleAlbumObject() =>
+ AlbumCard(item as SpotubeSimpleAlbumObject),
+ SpotubeFullArtistObject() =>
+ ArtistCard(item as SpotubeFullArtistObject),
+ _ => const SizedBox.shrink(),
+ };
+ }),
+ ),
),
),
- ),
],
),
);
diff --git a/lib/components/image/universal_image.dart b/lib/components/image/universal_image.dart
index d8902e63..e157f96a 100644
--- a/lib/components/image/universal_image.dart
+++ b/lib/components/image/universal_image.dart
@@ -58,10 +58,10 @@ class UniversalImage extends HookWidget {
),
height: height,
width: width,
- placeholder: AssetImage(placeholder ?? Assets.placeholder.path),
+ placeholder: AssetImage(placeholder ?? Assets.images.placeholder.path),
imageErrorBuilder: (context, error, stackTrace) {
return Image.asset(
- placeholder ?? Assets.placeholder.path,
+ placeholder ?? Assets.images.placeholder.path,
width: width,
height: height,
cacheHeight: height?.toInt(),
@@ -82,7 +82,7 @@ class UniversalImage extends HookWidget {
fit: fit,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
- placeholder ?? Assets.placeholder.path,
+ placeholder ?? Assets.images.placeholder.path,
width: width,
height: height,
cacheHeight: height?.toInt(),
@@ -102,7 +102,7 @@ class UniversalImage extends HookWidget {
fit: fit,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
- placeholder ?? Assets.placeholder.path,
+ placeholder ?? Assets.images.placeholder.path,
width: width,
height: height,
cacheHeight: height?.toInt(),
@@ -123,7 +123,7 @@ class UniversalImage extends HookWidget {
fit: fit,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
- placeholder ?? Assets.placeholder.path,
+ placeholder ?? Assets.images.placeholder.path,
width: width,
height: height,
cacheHeight: height?.toInt(),
diff --git a/lib/components/links/artist_link.dart b/lib/components/links/artist_link.dart
index 9467cb38..dc093345 100644
--- a/lib/components/links/artist_link.dart
+++ b/lib/components/links/artist_link.dart
@@ -1,12 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/models/metadata/metadata.dart';
class ArtistLink extends StatelessWidget {
- final List artists;
+ final List artists;
final WrapCrossAlignment crossAxisAlignment;
final WrapAlignment mainAxisAlignment;
final TextStyle textStyle;
@@ -38,19 +38,16 @@ class ArtistLink extends StatelessWidget {
.entries
.map(
(artist) => Builder(builder: (context) {
- if (artist.value.name == null) {
- return Text("Spotify", style: textStyle);
- }
return AnchorButton(
(artist.key != artists.length - 1)
? "${artist.value.name}, "
- : artist.value.name!,
+ : artist.value.name,
onTap: () {
if (onRouteChange != null) {
onRouteChange?.call("/artist/${artist.value.id}");
} else {
context
- .navigateTo(ArtistRoute(artistId: artist.value.id!));
+ .navigateTo(ArtistRoute(artistId: artist.value.id));
}
},
overflow: TextOverflow.ellipsis,
diff --git a/lib/components/markdown/markdown.dart b/lib/components/markdown/markdown.dart
new file mode 100644
index 00000000..9ea2e77c
--- /dev/null
+++ b/lib/components/markdown/markdown.dart
@@ -0,0 +1,98 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
+import 'package:shadcn_flutter/shadcn_flutter.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:url_launcher/url_launcher_string.dart';
+
+class AppMarkdown extends StatelessWidget {
+ final String data;
+ const AppMarkdown({
+ super.key,
+ required this.data,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return MarkdownBody(
+ data: data,
+ imageBuilder: (uri, title, alt) {
+ final url = uri.toString();
+ return CachedNetworkImage(
+ imageUrl: url,
+ fit: BoxFit.cover,
+ );
+ },
+ onTapLink: (text, href, title) async {
+ final allowOpeningLink = await showDialog(
+ context: context,
+ builder: (context) {
+ return ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 450),
+ child: AlertDialog(
+ title: Row(
+ spacing: 8,
+ children: [
+ const Icon(SpotubeIcons.warning),
+ Text(context.l10n.open_link_in_browser),
+ ],
+ ),
+ content: Text.rich(
+ TextSpan(
+ children: [
+ TextSpan(
+ text:
+ "${context.l10n.do_you_want_to_open_the_following_link}:\n",
+ ),
+ if (href != null)
+ TextSpan(
+ text: "$href\n\n",
+ style: const TextStyle(color: Colors.blue),
+ ),
+ TextSpan(text: context.l10n.unsafe_url_warning),
+ ],
+ ),
+ ),
+ actions: [
+ Button.ghost(
+ onPressed: () => Navigator.of(context).pop(false),
+ child: Text(context.l10n.cancel),
+ ),
+ Button.ghost(
+ onPressed: () {
+ if (href != null) {
+ Clipboard.setData(ClipboardData(text: href));
+ }
+ Navigator.of(context).pop(false);
+ },
+ child: Text(context.l10n.copy_link),
+ ),
+ Button.destructive(
+ onPressed: () {
+ if (href != null) {
+ launchUrlString(
+ href,
+ mode: LaunchMode.externalApplication,
+ );
+ }
+ Navigator.of(context).pop(true);
+ },
+ child: Text(context.l10n.open),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+
+ if (href != null && allowOpeningLink == true) {
+ launchUrlString(
+ href,
+ mode: LaunchMode.externalApplication,
+ );
+ }
+ },
+ );
+ }
+}
diff --git a/lib/components/track_presentation/presentation_actions.dart b/lib/components/track_presentation/presentation_actions.dart
index bbeb90a5..735a4514 100644
--- a/lib/components/track_presentation/presentation_actions.dart
+++ b/lib/components/track_presentation/presentation_actions.dart
@@ -1,7 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/dialogs/confirm_download_dialog.dart';
@@ -10,54 +9,59 @@ import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
+ToastOverlay showToastForAction(
+ BuildContext context,
+ String action,
+ int count,
+) {
+ final message = switch (action) {
+ "download" => (context.l10n.download_count(count), SpotubeIcons.download),
+ "add-to-playlist" => (
+ context.l10n.add_count_to_playlist(count),
+ SpotubeIcons.playlistAdd
+ ),
+ "add-to-queue" => (
+ context.l10n.add_count_to_queue(count),
+ SpotubeIcons.queueAdd
+ ),
+ "play-next" => (
+ context.l10n.play_count_next(count),
+ SpotubeIcons.lightning
+ ),
+ _ => ("", SpotubeIcons.error),
+ };
+
+ return showToast(
+ context: context,
+ location: ToastLocation.topRight,
+ builder: (context, overlay) {
+ return SurfaceCard(
+ child: Basic(
+ leading: Icon(message.$2),
+ title: Text(message.$1),
+ leadingAlignment: Alignment.center,
+ trailing: IconButton.ghost(
+ size: ButtonSize.small,
+ icon: const Icon(SpotubeIcons.close),
+ onPressed: () {
+ overlay.close();
+ },
+ ),
+ ),
+ );
+ },
+ );
+}
+
class TrackPresentationActionsSection extends HookConsumerWidget {
const TrackPresentationActionsSection({super.key});
- showToastForAction(BuildContext context, String action, int count) {
- final message = switch (action) {
- "download" => (context.l10n.download_count(count), SpotubeIcons.download),
- "add-to-playlist" => (
- context.l10n.add_count_to_playlist(count),
- SpotubeIcons.playlistAdd
- ),
- "add-to-queue" => (
- context.l10n.add_count_to_queue(count),
- SpotubeIcons.queueAdd
- ),
- "play-next" => (
- context.l10n.play_count_next(count),
- SpotubeIcons.lightning
- ),
- _ => ("", SpotubeIcons.error),
- };
-
- showToast(
- context: context,
- location: ToastLocation.topRight,
- builder: (context, overlay) {
- return SurfaceCard(
- child: Basic(
- leading: Icon(message.$2),
- title: Text(message.$1),
- leadingAlignment: Alignment.center,
- trailing: IconButton.ghost(
- size: ButtonSize.small,
- icon: const Icon(SpotubeIcons.close),
- onPressed: () {
- overlay.close();
- },
- ),
- ),
- );
- },
- );
- }
-
@override
Widget build(BuildContext context, ref) {
final options = TrackPresentationOptions.of(context);
@@ -76,9 +80,11 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
Future actionDownloadTracks({
required BuildContext context,
- required List tracks,
+ required List tracks,
required String action,
}) async {
+ final fullTrackObjects =
+ tracks.whereType().toList();
final confirmed = audioSource == AudioSource.piped ||
(await showDialog(
context: context,
@@ -88,10 +94,10 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
) ??
false);
if (confirmed != true) return;
- downloader.batchAddToQueue(tracks);
+ downloader.batchAddToQueue(fullTrackObjects);
notifier.deselectAllTracks();
if (!context.mounted) return;
- showToastForAction(context, action, tracks.length);
+ showToastForAction(context, action, fullTrackObjects.length);
}
return AdaptivePopSheetList(
@@ -143,11 +149,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
{
playlistNotifier.addTracksAtFirst(tracks);
playlistNotifier.addCollection(options.collectionId);
- if (options.collection is AlbumSimple) {
- historyNotifier.addAlbums([options.collection as AlbumSimple]);
+ if (options.collection is SpotubeSimpleAlbumObject) {
+ historyNotifier.addAlbums(
+ [options.collection as SpotubeSimpleAlbumObject]);
} else {
- historyNotifier
- .addPlaylists([options.collection as PlaylistSimple]);
+ historyNotifier.addPlaylists(
+ [options.collection as SpotubeSimplePlaylistObject]);
}
notifier.deselectAllTracks();
if (!context.mounted) return;
@@ -158,11 +165,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
{
playlistNotifier.addTracks(tracks);
playlistNotifier.addCollection(options.collectionId);
- if (options.collection is AlbumSimple) {
- historyNotifier.addAlbums([options.collection as AlbumSimple]);
+ if (options.collection is SpotubeSimpleAlbumObject) {
+ historyNotifier.addAlbums(
+ [options.collection as SpotubeSimpleAlbumObject]);
} else {
- historyNotifier
- .addPlaylists([options.collection as PlaylistSimple]);
+ historyNotifier.addPlaylists(
+ [options.collection as SpotubeSimplePlaylistObject]);
}
notifier.deselectAllTracks();
if (!context.mounted) return;
diff --git a/lib/components/track_presentation/presentation_list.dart b/lib/components/track_presentation/presentation_list.dart
index dda7dffa..19772c7c 100644
--- a/lib/components/track_presentation/presentation_list.dart
+++ b/lib/components/track_presentation/presentation_list.dart
@@ -1,10 +1,12 @@
import 'package:flutter/services.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
+import 'package:spotube/components/fallbacks/error_box.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/components/track_presentation/use_track_tile_play_callback.dart';
@@ -29,6 +31,19 @@ class PresentationListSection extends HookConsumerWidget {
final onTileTap = useTrackTilePlayCallback(ref);
if (state.presentationTracks.isEmpty && !options.pagination.isLoading) {
+ if (options.error != null) {
+ return SliverToBoxAdapter(
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: ErrorBox(
+ error: options.error!,
+ onRetry: options.pagination.onRefresh,
+ ),
+ ),
+ ),
+ );
+ }
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
@@ -80,9 +95,12 @@ class PresentationListSection extends HookConsumerWidget {
),
),
),
- itemBuilder: (context, index) {
+ itemBuilder: (context, index) => HookBuilder(builder: (context) {
final track = state.presentationTracks[index];
- final isSelected = state.selectedTracks.any((e) => e.id == track.id);
+ final isSelected = useMemoized(
+ () => state.selectedTracks.any((e) => e.id == track.id),
+ [track.id, state.selectedTracks],
+ );
return TrackTile(
userPlaylist: isUserPlaylist,
playlistId: options.collectionId,
@@ -105,7 +123,7 @@ class PresentationListSection extends HookConsumerWidget {
HapticFeedback.selectionClick();
},
);
- },
+ }),
);
}
}
diff --git a/lib/components/track_presentation/presentation_props.dart b/lib/components/track_presentation/presentation_props.dart
index 144cf0e8..1992487f 100644
--- a/lib/components/track_presentation/presentation_props.dart
+++ b/lib/components/track_presentation/presentation_props.dart
@@ -1,14 +1,14 @@
import 'dart:async';
import 'package:shadcn_flutter/shadcn_flutter.dart';
-import 'package:spotify/spotify.dart';
+import 'package:spotube/models/metadata/metadata.dart';
class PaginationProps {
final bool hasNextPage;
final bool isLoading;
final VoidCallback onFetchMore;
final Future Function() onRefresh;
- final Future> Function() onFetchAll;
+ final Future> Function() onFetchAll;
const PaginationProps({
required this.hasNextPage,
@@ -46,10 +46,11 @@ class TrackPresentationOptions {
final String? ownerImage;
final String image;
final String routePath;
- final List tracks;
+ final List tracks;
final PaginationProps pagination;
final bool isLiked;
final String? shareUrl;
+ final Object? error;
// events
final FutureOr Function()? onHeart; // if null heart button will hidden
@@ -67,11 +68,13 @@ class TrackPresentationOptions {
this.shareUrl,
this.isLiked = false,
this.onHeart,
- }) : assert(collection is AlbumSimple || collection is PlaylistSimple);
+ this.error,
+ }) : assert(collection is SpotubeSimpleAlbumObject ||
+ collection is SpotubeSimplePlaylistObject);
- String get collectionId => collection is AlbumSimple
- ? (collection as AlbumSimple).id!
- : (collection as PlaylistSimple).id!;
+ String get collectionId => collection is SpotubeSimpleAlbumObject
+ ? (collection as SpotubeSimpleAlbumObject).id
+ : (collection as SpotubeSimplePlaylistObject).id;
static TrackPresentationOptions of(BuildContext context) {
return Data.of(context);
@@ -89,7 +92,8 @@ class TrackPresentationOptions {
other.pagination == pagination &&
other.isLiked == isLiked &&
other.shareUrl == shareUrl &&
- other.onHeart == onHeart;
+ other.onHeart == onHeart &&
+ other.error == error;
}
@override
@@ -104,5 +108,6 @@ class TrackPresentationOptions {
pagination.hashCode ^
isLiked.hashCode ^
shareUrl.hashCode ^
- onHeart.hashCode;
+ onHeart.hashCode ^
+ error.hashCode;
}
diff --git a/lib/components/track_presentation/presentation_state.dart b/lib/components/track_presentation/presentation_state.dart
index d3428861..32b7353a 100644
--- a/lib/components/track_presentation/presentation_state.dart
+++ b/lib/components/track_presentation/presentation_state.dart
@@ -1,14 +1,16 @@
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
+import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
+import 'package:spotube/provider/metadata_plugin/tracks/album.dart';
+import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
import 'package:spotube/utils/service_utils.dart';
class PresentationState {
- final List selectedTracks;
- final List presentationTracks;
+ final List selectedTracks;
+ final List presentationTracks;
final SortBy sortBy;
const PresentationState({
@@ -18,8 +20,8 @@ class PresentationState {
});
PresentationState copyWith({
- List? selectedTracks,
- List? presentationTracks,
+ List? selectedTracks,
+ List? presentationTracks,
SortBy? sortBy,
}) {
return PresentationState(
@@ -34,15 +36,15 @@ class PresentationStateNotifier
extends AutoDisposeFamilyNotifier {
@override
PresentationState build(collection) {
- if (arg case PlaylistSimple() || AlbumSimple()) {
+ if (arg case SpotubeSimplePlaylistObject() || SpotubeSimpleAlbumObject()) {
if (isSavedTrackPlaylist) {
ref.listen(
- likedTracksProvider,
+ metadataPluginSavedTracksProvider,
(previous, next) {
next.whenData((value) {
state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks(
- value,
+ value.items,
state.sortBy,
),
);
@@ -51,9 +53,11 @@ class PresentationStateNotifier
);
} else {
ref.listen(
- arg is PlaylistSimple
- ? playlistTracksProvider((arg as PlaylistSimple).id!)
- : albumTracksProvider((arg as AlbumSimple)),
+ arg is SpotubeSimplePlaylistObject
+ ? metadataPluginPlaylistTracksProvider(
+ (arg as SpotubeSimplePlaylistObject).id)
+ : metadataPluginAlbumTracksProvider(
+ (arg as SpotubeSimpleAlbumObject).id),
(previous, next) {
next.whenData((value) {
state = state.copyWith(
@@ -76,36 +80,39 @@ class PresentationStateNotifier
}
bool get isSavedTrackPlaylist =>
- arg is PlaylistSimple &&
- (arg as PlaylistSimple).id == "user-liked-tracks";
+ arg is SpotubeSimplePlaylistObject &&
+ (arg as SpotubeSimplePlaylistObject).id == "user-liked-tracks";
- List get tracks {
+ List get tracks {
assert(
- arg is PlaylistSimple || arg is AlbumSimple,
- "arg must be PlaylistSimple or AlbumSimple",
+ arg is SpotubeSimplePlaylistObject || arg is SpotubeSimpleAlbumObject,
+ "arg must be SpotubeSimplePlaylistObject or SpotubeSimpleAlbumObject",
);
- final isPlaylist = arg is PlaylistSimple;
+ final isPlaylist = arg is SpotubeSimplePlaylistObject;
final tracks = switch ((isPlaylist, isSavedTrackPlaylist)) {
- (true, true) => ref.read(likedTracksProvider).asData?.value,
+ (true, true) =>
+ ref.read(metadataPluginSavedTracksProvider).asData?.value.items,
(true, false) => ref
- .read(playlistTracksProvider((arg as PlaylistSimple).id!))
+ .read(metadataPluginPlaylistTracksProvider(
+ (arg as SpotubeSimplePlaylistObject).id))
.asData
?.value
.items,
_ => ref
- .read(albumTracksProvider((arg as AlbumSimple)))
+ .read(metadataPluginAlbumTracksProvider(
+ (arg as SpotubeSimpleAlbumObject).id))
.asData
?.value
.items,
} ??
- [];
+ [];
return tracks;
}
- void selectTrack(Track track) {
+ void selectTrack(SpotubeTrackObject track) {
if (state.selectedTracks.any((e) => e.id == track.id)) {
return;
}
@@ -121,7 +128,7 @@ class PresentationStateNotifier
);
}
- void deselectTrack(Track track) {
+ void deselectTrack(SpotubeTrackObject track) {
state = state.copyWith(
selectedTracks: state.selectedTracks.where((e) => e != track).toList(),
);
@@ -141,7 +148,7 @@ class PresentationStateNotifier
state = state.copyWith(
presentationTracks: ServiceUtils.sortTracks(
tracks
- .map((e) => (weightedRatio(e.name!, query), e))
+ .map((e) => (weightedRatio(e.name, query), e))
.sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50)
.map((e) => e.$2)
diff --git a/lib/components/track_presentation/presentation_top.dart b/lib/components/track_presentation/presentation_top.dart
index 5935fa13..d2576cc0 100644
--- a/lib/components/track_presentation/presentation_top.dart
+++ b/lib/components/track_presentation/presentation_top.dart
@@ -3,8 +3,6 @@ import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/heart_button/heart_button.dart';
import 'package:spotube/components/image/universal_image.dart';
@@ -14,7 +12,6 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
class TrackPresentationTopSection extends HookConsumerWidget {
const TrackPresentationTopSection({super.key});
@@ -26,29 +23,14 @@ class TrackPresentationTopSection extends HookConsumerWidget {
final scale = context.theme.scaling;
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
- final playlistImage = (options.collection is PlaylistSimple &&
- (options.collection as PlaylistSimple).owner?.displayName ==
- "Spotify" &&
- Env.disableSpotifyImages)
- ? ref.watch(playlistImageProvider(options.collectionId))
- : null;
- final decorationImage = playlistImage != null
- ? DecorationImage(
- image: AssetImage(playlistImage.src),
- fit: BoxFit.cover,
- colorFilter: ColorFilter.mode(
- playlistImage.color,
- playlistImage.colorBlendMode,
- ),
- )
- : DecorationImage(
- image: UniversalImage.imageProvider(options.image),
- fit: BoxFit.cover,
- );
+ final decorationImage = DecorationImage(
+ image: UniversalImage.imageProvider(options.image),
+ fit: BoxFit.cover,
+ );
final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
- final (:isLoading, :isActive, :onPlay, :onShuffle) =
+ final (:isLoading, :isActive, :onPlay, :onShuffle, :onAddToQueue) =
useActionCallbacks(ref);
final playbackActions = Row(
@@ -77,15 +59,15 @@ class TrackPresentationTopSection extends HookConsumerWidget {
child: IconButton.secondary(
icon: const Icon(SpotubeIcons.queueAdd),
enabled: !isLoading && !isActive,
- onPressed: () {},
+ onPressed: onAddToQueue,
),
)
else
Button.secondary(
leading: const Icon(SpotubeIcons.add),
enabled: !isLoading && !isActive,
+ onPressed: onAddToQueue,
child: Text(context.l10n.queue),
- onPressed: () {},
),
Button.primary(
alignment: Alignment.center,
@@ -116,7 +98,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
builder: (context) {
return PlaylistCreateDialog(
playlistId: options.collectionId,
- trackIds: options.tracks.map((e) => e.id!).toList(),
+ trackIds: options.tracks.map((e) => e.id).toList(),
);
},
);
@@ -244,6 +226,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
.imageProvider(
options.ownerImage!,
),
+ size: 20 * scale,
)
: null,
child: Text(
diff --git a/lib/components/track_presentation/track_presentation.dart b/lib/components/track_presentation/track_presentation.dart
index 47089bd6..2b2a9f6f 100644
--- a/lib/components/track_presentation/track_presentation.dart
+++ b/lib/components/track_presentation/track_presentation.dart
@@ -55,37 +55,35 @@ class TrackPresentation extends HookConsumerWidget {
slivers: [
const TrackPresentationTopSection(),
const SliverGap(16),
- SliverLayoutBuilder(
- builder: (context, constrains) {
- return SliverList.list(
- children: [
- TrackPresentationModifiersSection(
- focusNode: focusNode,
+ SliverList.list(
+ children: [
+ TrackPresentationModifiersSection(
+ focusNode: focusNode,
+ ),
+ LayoutBuilder(builder: (context, constrains) {
+ return Basic(
+ padding: const EdgeInsets.symmetric(
+ vertical: 8,
+ horizontal: 16,
),
- Basic(
- padding: const EdgeInsets.symmetric(
- vertical: 8,
- horizontal: 16,
- ),
- leading: constrains.mdAndUp ? const Text(" #") : null,
- title: Row(
- children: [
+ leading: constrains.mdAndUp ? const Text(" #") : null,
+ title: Row(
+ children: [
+ Expanded(
+ flex: constrains.lgAndUp ? 5 : 6,
+ child: Text(context.l10n.title),
+ ),
+ if (constrains.mdAndUp)
Expanded(
- flex: constrains.lgAndUp ? 5 : 6,
- child: Text(context.l10n.title),
+ flex: 3,
+ child: Text(context.l10n.album),
),
- if (constrains.mdAndUp)
- Expanded(
- flex: 3,
- child: Text(context.l10n.album),
- ),
- Text(context.l10n.duration),
- ],
- ),
- ).small().muted(),
- ],
- );
- },
+ Text(context.l10n.duration),
+ ],
+ ),
+ ).small().muted();
+ }),
+ ],
),
const PresentationListSection(),
const SliverSafeArea(sliver: SliverGap(10)),
diff --git a/lib/components/track_presentation/use_action_callbacks.dart b/lib/components/track_presentation/use_action_callbacks.dart
index 0012594a..6707dd36 100644
--- a/lib/components/track_presentation/use_action_callbacks.dart
+++ b/lib/components/track_presentation/use_action_callbacks.dart
@@ -1,22 +1,26 @@
import 'dart:math';
+import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart';
+import 'package:spotube/components/track_presentation/presentation_actions.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/models/connect/connect.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
+import 'package:spotube/services/logger/logger.dart';
typedef UseActionCallbacks = ({
bool isActive,
bool isLoading,
Future Function() onShuffle,
Future Function() onPlay,
+ VoidCallback onAddToQueue,
});
UseActionCallbacks useActionCallbacks(WidgetRef ref) {
@@ -45,14 +49,14 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
final allTracks = await options.pagination.onFetchAll();
final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load(
- options.collection is AlbumSimple
+ options.collection is SpotubeSimpleAlbumObject
? WebSocketLoadEventData.album(
tracks: allTracks,
- collection: options.collection as AlbumSimple,
+ collection: options.collection as SpotubeSimpleAlbumObject,
initialIndex: Random().nextInt(allTracks.length))
: WebSocketLoadEventData.playlist(
tracks: allTracks,
- collection: options.collection as PlaylistSimple,
+ collection: options.collection as SpotubeSimplePlaylistObject,
initialIndex: Random().nextInt(allTracks.length),
),
);
@@ -65,10 +69,12 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
);
await audioPlayer.setShuffle(true);
playlistNotifier.addCollection(options.collectionId);
- if (options.collection is AlbumSimple) {
- historyNotifier.addAlbums([options.collection as AlbumSimple]);
+ if (options.collection is SpotubeSimpleAlbumObject) {
+ historyNotifier
+ .addAlbums([options.collection as SpotubeSimpleAlbumObject]);
} else {
- historyNotifier.addPlaylists([options.collection as PlaylistSimple]);
+ historyNotifier.addPlaylists(
+ [options.collection as SpotubeSimplePlaylistObject]);
}
final allTracks = await options.pagination.onFetchAll();
@@ -77,6 +83,9 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
allTracks.sublist(initialTracks.length),
);
}
+ } catch (e, stack) {
+ AppLogger.reportError(e, stack);
+ rethrow;
} finally {
isLoading.value = false;
}
@@ -94,25 +103,33 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
if (isRemoteDevice == null) return;
if (isRemoteDevice) {
final allTracks = await options.pagination.onFetchAll();
+
final remotePlayback = ref.read(connectProvider.notifier);
await remotePlayback.load(
- options.collection is AlbumSimple
+ options.collection is SpotubeSimpleAlbumObject
? WebSocketLoadEventData.album(
tracks: allTracks,
- collection: options.collection as AlbumSimple,
+ collection: options.collection as SpotubeSimpleAlbumObject,
)
: WebSocketLoadEventData.playlist(
tracks: allTracks,
- collection: options.collection as PlaylistSimple,
+ collection: options.collection as SpotubeSimplePlaylistObject,
),
);
} else {
+ if (initialTracks.isEmpty) return;
+
await playlistNotifier.load(initialTracks, autoPlay: true);
playlistNotifier.addCollection(options.collectionId);
- if (options.collection is AlbumSimple) {
- historyNotifier.addAlbums([options.collection as AlbumSimple]);
+
+ if (options.collection is SpotubeSimpleAlbumObject) {
+ historyNotifier.addAlbums(
+ [options.collection as SpotubeSimpleAlbumObject],
+ );
} else {
- historyNotifier.addPlaylists([options.collection as PlaylistSimple]);
+ historyNotifier.addPlaylists(
+ [options.collection as SpotubeSimplePlaylistObject],
+ );
}
final allTracks = await options.pagination.onFetchAll();
@@ -121,6 +138,9 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
allTracks.sublist(initialTracks.length),
);
}
+ } catch (e, stack) {
+ AppLogger.reportError(e, stack);
+ rethrow;
} finally {
if (context.mounted) {
isLoading.value = false;
@@ -128,10 +148,26 @@ UseActionCallbacks useActionCallbacks(WidgetRef ref) {
}
}, [options, playlistNotifier, historyNotifier]);
+ final onAddToQueue = useCallback(() {
+ final tracks = options.tracks;
+ playlistNotifier.addTracks(tracks);
+ playlistNotifier.addCollection(options.collectionId);
+ if (options.collection is SpotubeSimpleAlbumObject) {
+ historyNotifier
+ .addAlbums([options.collection as SpotubeSimpleAlbumObject]);
+ } else {
+ historyNotifier
+ .addPlaylists([options.collection as SpotubeSimplePlaylistObject]);
+ }
+ if (!context.mounted) return;
+ showToastForAction(context, "add-to-queue", tracks.length);
+ }, [options, playlistNotifier, historyNotifier]);
+
return (
isActive: isActive,
isLoading: isLoading.value,
onShuffle: onShuffle,
onPlay: onPlay,
+ onAddToQueue: onAddToQueue,
);
}
diff --git a/lib/components/track_presentation/use_is_user_playlist.dart b/lib/components/track_presentation/use_is_user_playlist.dart
index 2f87ccc8..8792f6e7 100644
--- a/lib/components/track_presentation/use_is_user_playlist.dart
+++ b/lib/components/track_presentation/use_is_user_playlist.dart
@@ -1,17 +1,18 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
+import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
+import 'package:spotube/provider/metadata_plugin/core/user.dart';
bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
- final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider);
- final me = ref.watch(meProvider);
+ final userPlaylistsQuery = ref.watch(metadataPluginSavedPlaylistsProvider);
+ final me = ref.watch(metadataPluginUserProvider);
return useMemoized(
() =>
userPlaylistsQuery.asData?.value.items.any((e) =>
e.id == playlistId &&
me.asData?.value != null &&
- e.owner?.id == me.asData?.value.id) ??
+ e.owner.id == me.asData?.value?.id) ??
false,
[userPlaylistsQuery.asData?.value, playlistId, me.asData?.value],
);
diff --git a/lib/components/track_presentation/use_track_tile_play_callback.dart b/lib/components/track_presentation/use_track_tile_play_callback.dart
index b519f781..99f44f1e 100644
--- a/lib/components/track_presentation/use_track_tile_play_callback.dart
+++ b/lib/components/track_presentation/use_track_tile_play_callback.dart
@@ -1,18 +1,19 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/list.dart';
import 'package:spotube/models/connect/connect.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart';
-Future Function(Track track, int index) useTrackTilePlayCallback(
+Future Function(SpotubeTrackObject track, int index)
+ useTrackTilePlayCallback(
WidgetRef ref,
) {
final context = useContext();
@@ -26,7 +27,8 @@ Future Function(Track track, int index) useTrackTilePlayCallback(
[playlist.collections, options.collectionId],
);
- final onTapTrackTile = useCallback((Track track, int index) async {
+ final onTapTrackTile =
+ useCallback((SpotubeTrackObject track, int index) async {
final state = ref.read(presentationStateProvider(options.collection));
final notifier =
ref.read(presentationStateProvider(options.collection).notifier);
@@ -52,15 +54,15 @@ Future Function(Track track, int index) useTrackTilePlayCallback(
} else {
final tracks = await options.pagination.onFetchAll();
await remotePlayback.load(
- options.collection is AlbumSimple
+ options.collection is SpotubeSimpleAlbumObject
? WebSocketLoadEventData.album(
tracks: tracks,
- collection: options.collection as AlbumSimple,
+ collection: options.collection as SpotubeSimpleAlbumObject,
initialIndex: index,
)
: WebSocketLoadEventData.playlist(
tracks: tracks,
- collection: options.collection as PlaylistSimple,
+ collection: options.collection as SpotubeSimplePlaylistObject,
initialIndex: index,
),
);
@@ -76,10 +78,12 @@ Future Function(Track track, int index) useTrackTilePlayCallback(
autoPlay: true,
);
playlistNotifier.addCollection(options.collectionId);
- if (options.collection is AlbumSimple) {
- historyNotifier.addAlbums([options.collection as AlbumSimple]);
+ if (options.collection is SpotubeSimpleAlbumObject) {
+ historyNotifier
+ .addAlbums([options.collection as SpotubeSimpleAlbumObject]);
} else {
- historyNotifier.addPlaylists([options.collection as PlaylistSimple]);
+ historyNotifier.addPlaylists(
+ [options.collection as SpotubeSimplePlaylistObject]);
}
}
}
diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart
index a0738165..7943fe3d 100644
--- a/lib/components/track_tile/track_options.dart
+++ b/lib/components/track_tile/track_options.dart
@@ -1,501 +1,297 @@
-import 'dart:io';
-
-import 'package:auto_route/auto_route.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
-import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/assets.gen.dart';
-import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
-import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
-import 'package:spotube/components/dialogs/prompt_dialog.dart';
-import 'package:spotube/components/dialogs/track_details_dialog.dart';
-import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
-import 'package:spotube/components/image/universal_image.dart';
-import 'package:spotube/components/links/artist_link.dart';
+import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/extensions/image.dart';
-import 'package:spotube/models/database/database.dart';
-import 'package:spotube/models/local_track.dart';
-import 'package:spotube/provider/authentication/authentication.dart';
-import 'package:spotube/provider/blacklist_provider.dart';
-import 'package:spotube/provider/download_manager_provider.dart';
-import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
-import 'package:spotube/provider/audio_player/audio_player.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
-
-import 'package:url_launcher/url_launcher_string.dart';
-
-enum TrackOptionValue {
- album,
- share,
- songlink,
- addToPlaylist,
- addToQueue,
- removeFromPlaylist,
- removeFromQueue,
- blacklist,
- delete,
- playNext,
- favorite,
- details,
- download,
- startRadio,
-}
+import 'package:spotube/models/metadata/metadata.dart';
+import 'package:spotube/provider/track_options/track_options_provider.dart';
+/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
class TrackOptions extends HookConsumerWidget {
- final Track track;
+ final SpotubeTrackObject track;
final bool userPlaylist;
final String? playlistId;
- final ObjectRef?>? showMenuCbRef;
final Widget? icon;
+ final VoidCallback? onTapItem;
+
const TrackOptions({
super.key,
required this.track,
- this.showMenuCbRef,
this.userPlaylist = false,
this.playlistId,
this.icon,
- });
-
- void actionShare(BuildContext context, Track track) {
- final data = "https://open.spotify.com/track/${track.id}";
- Clipboard.setData(ClipboardData(text: data)).then((_) {
- if (context.mounted) {
- showToast(
- context: context,
- location: ToastLocation.topRight,
- builder: (context, overlay) {
- return SurfaceCard(
- child: Text(
- context.l10n.copied_to_clipboard(data),
- textAlign: TextAlign.center,
- ),
- );
- },
+ this.onTapItem,
+ }) : assert(
+ track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
+ "Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
);
- }
- });
- }
-
- void actionAddToPlaylist(
- BuildContext context,
- Track track,
- ) {
- /// showDialog doesn't work for some reason. So we have to
- /// manually push a Dialog Route in the Navigator to get it working
- showDialog(
- context: context,
- builder: (context) {
- return PlaylistAddTrackDialog(
- tracks: [track],
- openFromPlaylist: playlistId,
- );
- },
- );
- }
-
- void actionStartRadio(
- BuildContext context,
- WidgetRef ref,
- Track track,
- ) async {
- final playback = ref.read(audioPlayerProvider.notifier);
- final playlist = ref.read(audioPlayerProvider);
- final spotify = ref.read(spotifyProvider);
- final query = "${track.name} Radio";
- final pages = await spotify.invoke(
- (api) => api.search.get(query, types: [SearchType.playlist]).first(),
- );
-
- final radios = pages
- .expand((e) => e.items?.cast().toList() ?? [])
- .toList();
-
- final artists = track.artists!.map((e) => e.name);
-
- final radio = radios.firstWhere(
- (e) {
- final validPlaylists =
- artists.where((a) => e.description!.contains(a!));
- return e.name == "${track.name} Radio" &&
- (validPlaylists.length >= 2 ||
- validPlaylists.length == artists.length) &&
- e.owner?.displayName == "Spotify";
- },
- orElse: () => radios.first,
- );
-
- bool replaceQueue = false;
-
- if (context.mounted && playlist.tracks.isNotEmpty) {
- replaceQueue = await showPromptDialog(
- context: context,
- title: context.l10n.how_to_start_radio,
- message: context.l10n.replace_queue_question,
- okText: context.l10n.replace,
- cancelText: context.l10n.add_to_queue,
- );
- }
-
- if (replaceQueue || playlist.tracks.isEmpty) {
- await playback.stop();
- await playback.load([track], autoPlay: true);
-
- // we don't have to add those tracks as useEndlessPlayback will do it for us
- return;
- } else {
- await playback.addTrack(track);
- }
-
- final tracks = await spotify.invoke(
- (api) => api.playlists.getTracksByPlaylistId(radio.id!).all(),
- );
-
- await playback.addTracks(
- tracks.toList()
- ..removeWhere((e) {
- final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
- return e.id == track.id || isDuplicate;
- }),
- );
- }
@override
Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context);
final ThemeData(:colorScheme) = Theme.of(context);
- final playlist = ref.watch(audioPlayerProvider);
- final playback = ref.watch(audioPlayerProvider.notifier);
- final auth = ref.watch(authenticationProvider);
- ref.watch(downloadManagerProvider);
- final downloadManager = ref.watch(downloadManagerProvider.notifier);
- final blacklist = ref.watch(blacklistProvider);
- final me = ref.watch(meProvider);
+ final trackOptionActions = ref.watch(trackOptionActionsProvider(track));
+ final (
+ :isBlacklisted,
+ :isInDownloadQueue,
+ :isInQueue,
+ :isActiveTrack,
+ :isAuthenticated,
+ :isLiked,
+ :progressNotifier
+ ) = ref.watch(trackOptionsStateProvider(track));
+ final isLocalTrack = track is SpotubeLocalTrackObject;
- final favorites = useTrackToggleLike(track, ref);
-
- final isBlackListed = useMemoized(
- () => blacklist.asData?.value.any(
- (element) => element.elementId == track.id,
- ),
- [blacklist, track],
- );
-
- final removingTrack = useState(null);
- final favoritePlaylistsNotifier =
- ref.watch(favoritePlaylistsProvider.notifier);
-
- final isInQueue = useMemoized(() {
- if (playlist.activeTrack == null) return false;
- return downloadManager.isActive(playlist.activeTrack!);
- }, [
- playlist.activeTrack,
- downloadManager,
- ]);
-
- final progressNotifier = useMemoized(() {
- final spotubeTrack = downloadManager.mapToSourcedTrack(track);
- if (spotubeTrack == null) return null;
- return downloadManager.getProgressNotifier(spotubeTrack);
- });
-
- final isLocalTrack = track is LocalTrack;
-
- final adaptivePopSheetList = AdaptivePopSheetList(
- tooltip: context.l10n.more_actions,
- onSelected: (value) async {
- switch (value) {
- case TrackOptionValue.album:
- await context.navigateTo(
- AlbumRoute(id: track.album!.id!, album: track.album!),
- );
- break;
- case TrackOptionValue.delete:
- await File((track as LocalTrack).path).delete();
- ref.invalidate(localTracksProvider);
- break;
- case TrackOptionValue.addToQueue:
- await playback.addTrack(track);
- if (context.mounted) {
- showToast(
- context: context,
- location: ToastLocation.topRight,
- builder: (context, overlay) {
- return SurfaceCard(
- child: Text(
- context.l10n.added_track_to_queue(track.name!),
- textAlign: TextAlign.center,
- ),
- );
- },
- );
- }
- break;
- case TrackOptionValue.playNext:
- playback.addTracksAtFirst([track]);
-
- if (context.mounted) {
- showToast(
- context: context,
- location: ToastLocation.topRight,
- builder: (context, overlay) {
- return SurfaceCard(
- child: Text(
- context.l10n.track_will_play_next(track.name!),
- textAlign: TextAlign.center,
- ),
- );
- },
- );
- }
- break;
- case TrackOptionValue.removeFromQueue:
- playback.removeTrack(track.id!);
-
- if (context.mounted) {
- showToast(
- context: context,
- location: ToastLocation.topRight,
- builder: (context, overlay) {
- return SurfaceCard(
- child: Text(
- context.l10n.removed_track_from_queue(
- track.name!,
- ),
- textAlign: TextAlign.center,
- ),
- );
- },
- );
- }
- break;
- case TrackOptionValue.favorite:
- favorites.toggleTrackLike(track);
- break;
- case TrackOptionValue.addToPlaylist:
- actionAddToPlaylist(context, track);
- break;
- case TrackOptionValue.removeFromPlaylist:
- removingTrack.value = track.uri;
- favoritePlaylistsNotifier
- .removeTracks(playlistId ?? "", [track.id!]);
- break;
- case TrackOptionValue.blacklist:
- if (isBlackListed == null) break;
- if (isBlackListed == true) {
- await ref.read(blacklistProvider.notifier).remove(track.id!);
- } else {
- await ref.read(blacklistProvider.notifier).add(
- BlacklistTableCompanion.insert(
- name: track.name!,
- elementId: track.id!,
- elementType: BlacklistedType.track,
- ),
- );
- }
- break;
- case TrackOptionValue.share:
- actionShare(context, track);
- break;
- case TrackOptionValue.songlink:
- final url = "https://song.link/s/${track.id}";
- await launchUrlString(url);
- break;
- case TrackOptionValue.details:
- showDialog(
- context: context,
- builder: (context) => ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 400),
- child: TrackDetailsDialog(track: track),
- ),
- );
- break;
- case TrackOptionValue.download:
- await downloadManager.addToQueue(track);
- break;
- case TrackOptionValue.startRadio:
- actionStartRadio(context, ref, track);
- break;
- }
- },
- icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
- variance: ButtonVariance.outline,
- headings: [
- Basic(
- leading: AspectRatio(
- aspectRatio: 1,
- child: ClipRRect(
- borderRadius: BorderRadius.circular(10),
- child: UniversalImage(
- path: track.album!.images
- .asUrlString(placeholder: ImagePlaceholder.albumArt),
- fit: BoxFit.cover,
- ),
- ),
- ),
- title: Text(
- track.name!,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ).semiBold(),
- subtitle: Align(
- alignment: Alignment.centerLeft,
- child: ArtistLink(
- artists: track.artists!,
- onOverflowArtistClick: () => context.navigateTo(
- TrackRoute(trackId: track.id!),
- ),
- ),
- ),
- ),
- ],
- items: (context) => [
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 8,
+ children: [
if (isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.delete,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.delete,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.trash),
- child: Text(context.l10n.delete),
+ title: Text(context.l10n.delete),
),
if (mediaQuery.smAndDown && !isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.album,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.album,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.album),
- child: Column(
+ title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.go_to_album),
Text(
- track.album!.name!,
+ track.album.name,
style: context.theme.typography.xSmall,
),
],
),
),
- if (!playlist.containsTrack(track)) ...[
- AdaptiveMenuButton(
- value: TrackOptionValue.addToQueue,
+ if (!isInQueue) ...[
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.addToQueue,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.queueAdd),
- child: Text(context.l10n.add_to_queue),
+ title: Text(context.l10n.add_to_queue),
),
- AdaptiveMenuButton(
- value: TrackOptionValue.playNext,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.playNext,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.lightning),
- child: Text(context.l10n.play_next),
+ title: Text(context.l10n.play_next),
),
] else
- AdaptiveMenuButton(
- value: TrackOptionValue.removeFromQueue,
- enabled: playlist.activeTrack?.id != track.id,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.removeFromQueue,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
+ enabled: !isActiveTrack,
leading: const Icon(SpotubeIcons.queueRemove),
- child: Text(context.l10n.remove_from_queue),
+ title: Text(context.l10n.remove_from_queue),
),
- if (me.asData?.value != null && !isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.favorite,
- leading: favorites.isLiked
+ if (isAuthenticated && !isLocalTrack)
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.favorite,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
+ leading: isLiked
? const Icon(
SpotubeIcons.heartFilled,
color: Colors.pink,
)
: const Icon(SpotubeIcons.heart),
- child: Text(
- favorites.isLiked
+ title: Text(
+ isLiked
? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite,
),
),
- if (auth.asData?.value != null && !isLocalTrack) ...[
- AdaptiveMenuButton(
- value: TrackOptionValue.startRadio,
+ if (isAuthenticated && !isLocalTrack) ...[
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.startRadio,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.radio),
- child: Text(context.l10n.start_a_radio),
+ title: Text(context.l10n.start_a_radio),
),
- AdaptiveMenuButton(
- value: TrackOptionValue.addToPlaylist,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.addToPlaylist,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.playlistAdd),
- child: Text(context.l10n.add_to_playlist),
+ title: Text(context.l10n.add_to_playlist),
),
],
- if (userPlaylist && auth.asData?.value != null && !isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.removeFromPlaylist,
+ if (userPlaylist && isAuthenticated && !isLocalTrack)
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.removeFromPlaylist,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.removeFilled),
- child: Text(context.l10n.remove_from_playlist),
+ title: Text(context.l10n.remove_from_playlist),
),
if (!isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.download,
- enabled: !isInQueue,
- leading: isInQueue
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.download,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
+ enabled: !isInDownloadQueue,
+ leading: isInDownloadQueue
? HookBuilder(builder: (context) {
- final progress = useListenable(progressNotifier!);
+ final progress = useListenable(progressNotifier);
return CircularProgressIndicator(
- value: progress.value,
+ value: progress?.value,
);
})
: const Icon(SpotubeIcons.download),
- child: Text(context.l10n.download_track),
+ title: Text(context.l10n.download_track),
),
if (!isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.blacklist,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.blacklist,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: Icon(
SpotubeIcons.playlistRemove,
- color: isBlackListed != true ? Colors.red[400] : null,
+ color: isBlacklisted != true ? Colors.red[400] : null,
),
- child: Text(
- isBlackListed == true
+ title: Text(
+ isBlacklisted == true
? context.l10n.remove_from_blacklist
: context.l10n.add_to_blacklist,
style: TextStyle(
- color: isBlackListed != true ? Colors.red[400] : null,
+ color: isBlacklisted != true ? Colors.red[400] : null,
),
),
),
if (!isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.share,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.share,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.share),
- child: Text(context.l10n.share),
+ title: Text(context.l10n.share),
),
if (!isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.songlink,
- leading: Assets.logos.songlinkTransparent.image(
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.songlink,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
+ leading: Assets.images.logos.songlinkTransparent.image(
width: 22,
height: 22,
color: colorScheme.foreground.withValues(alpha: 0.5),
),
- child: Text(context.l10n.song_link),
+ title: Text(context.l10n.song_link),
),
if (!isLocalTrack)
- AdaptiveMenuButton(
- value: TrackOptionValue.details,
+ ButtonTile(
+ style: ButtonVariance.menu,
+ onPressed: () async {
+ await trackOptionActions.action(
+ context,
+ TrackOptionValue.details,
+ playlistId,
+ );
+ onTapItem?.call();
+ },
leading: const Icon(SpotubeIcons.info),
- child: Text(context.l10n.details),
+ title: Text(context.l10n.details),
),
],
);
-
- //! This is the most ANTI pattern I've ever done, but it works
- showMenuCbRef?.value = (relativeRect) {
- final offsetFromRect = Offset(
- relativeRect.left,
- relativeRect.top,
- );
- adaptivePopSheetList.showDropdownMenu(context, offsetFromRect);
- };
-
- return adaptivePopSheetList;
}
}
diff --git a/lib/components/track_tile/track_options_button.dart b/lib/components/track_tile/track_options_button.dart
new file mode 100644
index 00000000..51fff5ea
--- /dev/null
+++ b/lib/components/track_tile/track_options_button.dart
@@ -0,0 +1,152 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:shadcn_flutter/shadcn_flutter.dart';
+import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
+import 'package:spotube/collections/routes.gr.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/links/artist_link.dart';
+import 'package:spotube/components/track_tile/track_options.dart';
+import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/models/metadata/metadata.dart';
+
+class TrackOptionsButton extends HookConsumerWidget {
+ final SpotubeTrackObject track;
+ final bool userPlaylist;
+ final String? playlistId;
+ const TrackOptionsButton({
+ super.key,
+ required this.track,
+ required this.userPlaylist,
+ this.playlistId,
+ });
+
+ static OverlayCompleter showOptions(
+ BuildContext context,
+ Offset offset,
+ SpotubeTrackObject track, {
+ bool userPlaylist = false,
+ String? playlistId,
+ }) {
+ return showPopover(
+ context: context,
+ position: offset,
+ alignment: Alignment.bottomRight,
+ builder: (context) {
+ return SizedBox(
+ width: 220 * context.theme.scaling,
+ child: Card(
+ padding: const EdgeInsets.all(8),
+ child: TrackOptions(
+ track: track,
+ playlistId: playlistId,
+ userPlaylist: userPlaylist,
+ onTapItem: () {
+ closeOverlay(context);
+ },
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final imageProvider = useMemoized(
+ () => UniversalImage.imageProvider(
+ (track.album.images).smallest(ImagePlaceholder.albumArt),
+ ),
+ [track.album.images],
+ );
+
+ return IconButton.ghost(
+ icon: const Icon(SpotubeIcons.moreHorizontal),
+ onPressed: () {
+ final mediaQuery = MediaQuery.sizeOf(context);
+
+ if (mediaQuery.lgAndUp) {
+ final renderBox = context.findRenderObject() as RenderBox;
+ final position = RelativeRect.fromRect(
+ Rect.fromPoints(
+ renderBox.localToGlobal(Offset.zero,
+ ancestor: context.findRenderObject()),
+ renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero),
+ ancestor: context.findRenderObject()),
+ ),
+ Offset.zero & mediaQuery,
+ );
+ final offset = Offset(position.left, position.top);
+ showOptions(
+ context,
+ offset,
+ track,
+ userPlaylist: userPlaylist,
+ playlistId: playlistId,
+ );
+ } else {
+ openDrawer(
+ context: context,
+ position: OverlayPosition.bottom,
+ draggable: true,
+ showDragHandle: true,
+ borderRadius: context.theme.borderRadiusMd,
+ transformBackdrop: false,
+ builder: (context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 8,
+ children: [
+ Basic(
+ leading: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ borderRadius: context.theme.borderRadiusMd,
+ image: DecorationImage(
+ fit: BoxFit.cover,
+ image: imageProvider,
+ ),
+ ),
+ ),
+ title: Text(
+ track.name,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ).semiBold(),
+ subtitle: Align(
+ alignment: Alignment.centerLeft,
+ child: ArtistLink(
+ artists: track.artists,
+ onOverflowArtistClick: () => context.navigateTo(
+ TrackRoute(trackId: track.id),
+ ),
+ ),
+ ),
+ ),
+ const Divider(),
+ TrackOptions(
+ track: track,
+ userPlaylist: userPlaylist,
+ playlistId: playlistId,
+ onTapItem: () {
+ closeDrawer(context);
+ },
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+ },
+ );
+ }
+}
diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart
index f47980cd..955ac90d 100644
--- a/lib/components/track_tile/track_tile.dart
+++ b/lib/components/track_tile/track_tile.dart
@@ -5,31 +5,39 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
+import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
-import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/hover_builder.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/link_text.dart';
-import 'package:spotube/components/track_tile/track_options.dart';
+import 'package:spotube/components/track_tile/track_options_button.dart';
import 'package:spotube/components/ui/button_tile.dart';
-import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart';
-import 'package:spotube/extensions/image.dart';
-import 'package:spotube/models/local_track.dart';
+import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/platform.dart';
+final isBlacklistedProvider =
+ Provider.autoDispose.family(
+ (ref, track) {
+ ref.watch(blacklistProvider);
+ final blacklist = ref.read(blacklistProvider.notifier);
+ return blacklist.contains(track);
+ },
+);
+
+final _overlay = ValueNotifier?>(null);
+
class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null
final int? index;
- final Track track;
+ final SpotubeTrackObject track;
final bool selected;
final ValueChanged? onChanged;
final Future Function()? onTap;
@@ -58,15 +66,7 @@ class TrackTile extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
- final blacklist = ref.watch(blacklistProvider);
- final blacklistNotifier = ref.watch(blacklistProvider.notifier);
-
- final isBlackListed = useMemoized(
- () => blacklistNotifier.contains(track),
- [blacklist, track],
- );
-
- final showOptionCbRef = useRef?>(null);
+ final isBlackListed = ref.watch(isBlacklistedProvider(track));
final isLoading = useState(false);
@@ -74,17 +74,27 @@ class TrackTile extends HookConsumerWidget {
final isSelected = isPlaying || isLoading.value;
+ final imageProvider = useMemoized(
+ () => UniversalImage.imageProvider(
+ (track.album.images).smallest(ImagePlaceholder.albumArt),
+ ),
+ [track.album.images],
+ );
+
return LayoutBuilder(builder: (context, constrains) {
return Listener(
onPointerDown: (event) {
if (event.buttons != kSecondaryMouseButton) return;
- showOptionCbRef.value?.call(
- RelativeRect.fromLTRB(
- event.position.dx,
- event.position.dy,
- constrains.maxWidth - event.position.dx,
- constrains.maxHeight - event.position.dy,
- ),
+ if (_overlay.value != null) {
+ _overlay.value?.remove();
+ _overlay.value = null;
+ }
+ _overlay.value = TrackOptionsButton.showOptions(
+ context,
+ Offset.zero,
+ track,
+ userPlaylist: userPlaylist,
+ playlistId: playlistId,
);
},
child: HoverBuilder(
@@ -150,11 +160,7 @@ class TrackTile extends HookConsumerWidget {
borderRadius: theme.borderRadiusMd,
image: DecorationImage(
fit: BoxFit.cover,
- image: UniversalImage.imageProvider(
- (track.album?.images).asUrlString(
- placeholder: ImagePlaceholder.albumArt,
- ),
- ),
+ image: imageProvider,
),
),
),
@@ -217,8 +223,8 @@ class TrackTile extends HookConsumerWidget {
Expanded(
flex: 6,
child: switch (track) {
- LocalTrack() => Text(
- track.name!,
+ SpotubeLocalTrackObject() => Text(
+ track.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -233,10 +239,10 @@ class TrackTile extends HookConsumerWidget {
),
onPressed: () {
context
- .navigateTo(TrackRoute(trackId: track.id!));
+ .navigateTo(TrackRoute(trackId: track.id));
},
child: Text(
- track.name!,
+ track.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -251,17 +257,19 @@ class TrackTile extends HookConsumerWidget {
Expanded(
flex: 4,
child: switch (track) {
- LocalTrack() => Text(
- track.album!.name!,
+ SpotubeLocalTrackObject() => Text(
+ track.album.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
_ => Align(
alignment: Alignment.centerLeft,
child: LinkText(
- track.album!.name!,
+ track.album.name,
AlbumRoute(
- album: track.album!, id: track.album!.id!),
+ album: track.album,
+ id: track.album.id,
+ ),
push: true,
overflow: TextOverflow.ellipsis,
),
@@ -273,18 +281,18 @@ class TrackTile extends HookConsumerWidget {
),
subtitle: Align(
alignment: Alignment.centerLeft,
- child: track is LocalTrack
+ child: track is SpotubeLocalTrackObject
? Text(
- track.artists?.asString() ?? '',
+ track.artists.asString(),
)
: ClipRect(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40),
child: ArtistLink(
- artists: track.artists ?? [],
+ artists: track.artists,
onOverflowArtistClick: () {
context.navigateTo(
- TrackRoute(trackId: track.id!),
+ TrackRoute(trackId: track.id),
);
},
),
@@ -296,16 +304,19 @@ class TrackTile extends HookConsumerWidget {
children: [
const SizedBox(width: 8),
Text(
- Duration(milliseconds: track.durationMs ?? 0)
+ Duration(milliseconds: track.durationMs)
.toHumanReadableString(padZero: false),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
- TrackOptions(
- track: track,
- playlistId: playlistId,
- userPlaylist: userPlaylist,
- showMenuCbRef: showOptionCbRef,
+ Builder(
+ builder: (context) {
+ return TrackOptionsButton(
+ track: track,
+ userPlaylist: userPlaylist,
+ playlistId: playlistId,
+ );
+ },
),
if (kIsDesktop) const Gap(10),
],
diff --git a/lib/extensions/album_simple.dart b/lib/extensions/album_simple.dart
deleted file mode 100644
index 5678390c..00000000
--- a/lib/extensions/album_simple.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-import 'package:spotify/spotify.dart';
-
-extension AlbumExtensions on AlbumSimple {
- Album toAlbum() {
- Album album = Album();
- album.albumType = albumType;
- album.artists = artists;
- album.availableMarkets = availableMarkets;
- album.externalUrls = externalUrls;
- album.href = href;
- album.id = id;
- album.images = images;
- album.name = name;
- album.releaseDate = releaseDate;
- album.releaseDatePrecision = releaseDatePrecision;
- album.tracks = tracks;
- album.type = type;
- album.uri = uri;
- return album;
- }
-}
diff --git a/lib/extensions/artist_simple.dart b/lib/extensions/artist_simple.dart
deleted file mode 100644
index 7997355d..00000000
--- a/lib/extensions/artist_simple.dart
+++ /dev/null
@@ -1,7 +0,0 @@
-import 'package:spotify/spotify.dart';
-
-extension ArtistExtension on List {
- String asString() {
- return map((e) => e.name?.replaceAll(",", " ")).join(", ");
- }
-}
diff --git a/lib/extensions/image.dart b/lib/extensions/image.dart
deleted file mode 100644
index ee78653a..00000000
--- a/lib/extensions/image.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:spotify/spotify.dart';
-import 'package:spotube/collections/assets.gen.dart';
-import 'package:spotube/utils/primitive_utils.dart';
-import 'package:collection/collection.dart';
-
-enum ImagePlaceholder {
- albumArt,
- artist,
- collection,
- online,
-}
-
-extension SpotifyImageExtensions on List? {
- String asUrlString({
- int index = 1,
- required ImagePlaceholder placeholder,
- }) {
- final String placeholderUrl = {
- ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
- ImagePlaceholder.artist: Assets.userPlaceholder.path,
- ImagePlaceholder.collection: Assets.placeholder.path,
- ImagePlaceholder.online:
- "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
- }[placeholder]!;
-
- final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
-
- return sortedImage != null && sortedImage.isNotEmpty
- ? sortedImage[
- index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
- .url!
- : placeholderUrl;
- }
-}
diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart
deleted file mode 100644
index 92d8b0da..00000000
--- a/lib/extensions/track.dart
+++ /dev/null
@@ -1,113 +0,0 @@
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:metadata_god/metadata_god.dart';
-import 'package:path/path.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/provider/spotify/spotify.dart';
-import 'package:spotube/services/audio_player/audio_player.dart';
-import 'package:spotube/services/logger/logger.dart';
-
-extension TrackExtensions on Track {
- Track fromFile(
- File file, {
- Metadata? metadata,
- String? art,
- }) {
- album = Album()
- ..name = metadata?.album ?? "Unknown"
- ..images = [if (art != null) Image()..url = art]
- ..genres = [if (metadata?.genre != null) metadata!.genre!]
- ..artists = [
- Artist()
- ..name = metadata?.albumArtist ?? "Unknown"
- ..id = metadata?.albumArtist ?? "Unknown"
- ..type = "artist",
- ]
- ..id = metadata?.album
- ..releaseDate = metadata?.year?.toString();
- artists = [
- Artist()
- ..name = metadata?.artist ?? "Unknown"
- ..id = metadata?.artist ?? "Unknown"
- ];
-
- id = metadata?.title ?? basenameWithoutExtension(file.path);
- name = metadata?.title ?? basenameWithoutExtension(file.path);
- type = "track";
- uri = file.path;
- durationMs = (metadata?.durationMs?.toInt() ?? 0);
-
- return this;
- }
-
- Metadata toMetadata({
- required int fileLength,
- Uint8List? imageBytes,
- }) {
- return Metadata(
- title: name,
- artist: artists?.map((a) => a.name).join(", "),
- album: album?.name,
- albumArtist: artists?.map((a) => a.name).join(", "),
- year: album?.releaseDate != null
- ? int.tryParse(album!.releaseDate!.split("-").first) ?? 1969
- : 1969,
- trackNumber: trackNumber,
- discNumber: discNumber,
- durationMs: durationMs?.toDouble() ?? 0.0,
- fileSize: BigInt.from(fileLength),
- trackTotal: album?.tracks?.length ?? 0,
- picture: imageBytes != null
- ? Picture(
- data: imageBytes,
- // Spotify images are always JPEGs
- mimeType: 'image/jpeg',
- )
- : null,
- );
- }
-}
-
-extension IterableTrackSimpleExtensions on Iterable {
- Future