Merge pull request #2118 from KRTirtho/dev

chore: release 3.9.0
This commit is contained in:
Kingkor Roy Tirtho 2024-12-09 00:04:29 +06:00 committed by GitHub
commit 8c1337d1fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 20356 additions and 8423 deletions

View File

@ -1,16 +1,16 @@
# The format: # The format:
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2 # SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
SPOTIFY_SECRETS= SPOTIFY_SECRETS=$SPOTIFY_SECRETS
# 0 or 1 # 0 or 1
# 0 = disable # 0 = disable
# 1 = enable # 1 = enable
ENABLE_UPDATE_CHECK= ENABLE_UPDATE_CHECK=$ENABLE_UPDATE_CHECK
LASTFM_API_KEY= LASTFM_API_KEY=$LASTFM_API_KEY
LASTFM_API_SECRET= LASTFM_API_SECRET=$LASTFM_API_SECRET
# Release channel. Can be: nightly, stable # Release channel. Can be: nightly, stable
RELEASE_CHANNEL= RELEASE_CHANNEL=$RELEASE_CHANNEL
HIDE_DONATIONS= HIDE_DONATIONS=$HIDE_DONATIONS

View File

@ -1,3 +1,3 @@
{ {
"flutterSdkVersion": "3.24.3" "flutterSdkVersion": "3.24.5"
} }

2
.fvmrc
View File

@ -1,4 +1,4 @@
{ {
"flutter": "3.24.3", "flutter": "3.24.5",
"flavors": {} "flavors": {}
} }

View File

@ -4,7 +4,7 @@ on:
pull_request: pull_request:
env: env:
FLUTTER_VERSION: 3.22.2 FLUTTER_VERSION: 3.24.5
jobs: jobs:
lint: lint:
@ -17,18 +17,23 @@ jobs:
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Dummy Envs
run: |
envsubst < .env.example > .env
env:
SPOTIFY_SECRETS: xxx:xxx
ENABLE_UPDATE_CHECK: true
LASTFM_API_KEY: xxx
LASTFM_API_SECRET: xxx
RELEASE_CHANNEL: nightly
HIDE_DONATIONS: 0
- name: Configure repo - name: Configure repo
run: | run: |
flutter pub get flutter pub get
echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
dart run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs
- name: Lint Dart files - name: Lint Dart files
run: | run: |
dart analyze --no-fatal-warnings dart analyze --no-fatal-warnings
- name: Lint translations & config files
run: |
npm install -g @prantlf/jsonlint
jsonlint -q -D --enforce-double-quotes ./lib/l10n/*.arb
jsonlint -q -D --enforce-double-quotes -T .vscode/*.json

View File

@ -20,7 +20,7 @@ on:
description: Dry run without uploading to release description: Dry run without uploading to release
env: env:
FLUTTER_VERSION: 3.24.3 FLUTTER_VERSION: 3.24.5
permissions: permissions:
contents: write contents: write

View File

@ -2,6 +2,22 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [3.9.0](https://github.com/krtirtho/spotube/compare/v3.8.3...v3.9.0) (2024-12-08)
## Changes
### Bug Fixes
- UI glitch when loading more user artists and albums
- selecting an Alternative Track Source removes the current song from the queue #2039
- **mobile**: ensure audio session is activated when playback is resumed after interruption #2092
### Features
- add invidious audio source and fix auto skipping tracks (#2005)
- track caching and cached track export support (#2117)
## [3.8.3](https://github.com/krtirtho/spotube/compare/v3.8.2...v3.8.3) (2024-10-09) ## [3.8.3](https://github.com/krtirtho/spotube/compare/v3.8.2...v3.8.3) (2024-10-09)
## Changes ## Changes

View File

@ -42,4 +42,7 @@ apk:
mv build/app/outputs/apk/release/app-release.apk build/Spotube-android-all-arch.apk mv build/app/outputs/apk/release/app-release.apk build/Spotube-android-all-arch.apk
gensums: gensums:
sh -c scripts/gensums.sh sh -c scripts/gensums.sh
migrate:
dart run drift_dev make-migrations

View File

@ -86,6 +86,19 @@ This handy table lists all the methods you can use to install Spotube:
</a> </a>
</td> </td>
</tr> </tr>
<tr>
<tr>
<td>iOS</td>
<td>
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-iOS.ipa">
<img width="220" alt="Download iOS IPA" src="https://github.com/user-attachments/assets/3e50d93d-fb39-435c-be6b-337745f7c423">
</a>
<br/>
<blockquote style="color:red">
*iPA file only. Requires sideloading with <a href="https://altstore.io/">AltStore</a> or similar tools.
</blockquote>
</td>
</tr>
<tr> <tr>
<td>Flatpak</td> <td>Flatpak</td>
<td> <td>
@ -213,40 +226,32 @@ If you are concerned, you can [read the reason of choosing this license](https:/
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. [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. [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. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
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](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. [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_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds. 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. [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. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button. 1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
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. [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. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration. 1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration.
1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules. 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. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop.
1. [device_info_plus](https://plus.fluttercommunity.dev/) - 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. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.
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. [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. [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. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. 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. [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_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. [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. [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_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications. 1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
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_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_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_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_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_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_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_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_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. [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_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_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_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.
@ -254,58 +259,53 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. 1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets 1. [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. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! 1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views. 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. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more 1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
1. [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. [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. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256. 1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze. 1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
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. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser. 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. [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. [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. [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. [introduction_screen](https://pub.dev/packages/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities 1. [invidious](https://pub.dev/packages/invidious) - Invidious API client for Dart and Flutter.
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. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com 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. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications. 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. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
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. [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_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
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](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 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. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. 1. [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. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image.
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
1. [path](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](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. [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. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area. 1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
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. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
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. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter 1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in 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. [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_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. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget 1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. 1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort. 1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework 1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet. 1. [smtc_windows](https://pub.dev/packages/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API. 1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
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. [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://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS 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. [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. [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. [tray_manager](https://github.com/leanflutter/tray_manager) - This plugin allows Flutter desktop apps to defines system tray.
@ -318,8 +318,27 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter 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. [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. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
1. [xml](https://github.com/renggli/dart-xml) - A lightweight library for parsing, traversing, querying, transforming and building XML documents.
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. [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. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
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. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
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. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules.
1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
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. [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. [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. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
</details> </details>
<div align="center"><h4>© Copyright Spotube 2024</h4></div> <div align="center"><h4>© Copyright Spotube 2024</h4></div>

View File

@ -39,3 +39,4 @@ analyzer:
- "**.g.dart" - "**.g.dart"
- "**.gr.dart" - "**.gr.dart"
- "**/generated_plugin_registrant.dart" - "**/generated_plugin_registrant.dart"
- test/**/*.dart

View File

@ -1,8 +1,18 @@
{ {
"title": "Spotube", "title": "Spotube",
"icon": "assets/spotube-logo.png", "icon": "assets/spotube-logo-macos.png",
"contents": [ "contents": [
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" }, {
{ "x": 192, "y": 344, "type": "file", "path": "build/macos/Build/Products/Release/spotube.app" } "x": 448,
"y": 344,
"type": "link",
"path": "/Applications"
},
{
"x": 192,
"y": 344,
"type": "file",
"path": "build/macos/Build/Products/Release/Spotube.app"
}
] ]
} }

BIN
assets/invidious.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -10,6 +10,8 @@ targets:
explicit_to_json: true explicit_to_json: true
drift_dev: drift_dev:
options: options:
databases:
app_db: lib/models/database/database.dart
sql: sql:
dialect: sqlite dialect: sqlite
options: options:

View File

@ -51,6 +51,7 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
await shell.run( await shell.run(
""" """
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs dart run build_runner build --delete-conflicting-outputs
flutter build appbundle --flavor ${CliEnv.channel.name} flutter build appbundle --flavor ${CliEnv.channel.name}
""", """,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
flutter_launcher_icons:
ios: true
android: true
image_path: "assets/spotube-logo.png"
adaptive_icon_foreground: "assets/spotube-logo-foreground.jpg"
adaptive_icon_background: "#242832"
windows:
generate: true
image_path: "assets/spotube-logo.png"
icon_size: 48 # min:48, max:256, default: 48
macos:
generate: true
image_path: "assets/spotube-logo-macos.png"

View File

@ -58,7 +58,7 @@ PODS:
- flutter_inappwebview_ios/Core (0.0.1): - flutter_inappwebview_ios/Core (0.0.1):
- Flutter - Flutter
- OrderedSet (~> 6.0.3) - OrderedSet (~> 6.0.3)
- flutter_native_splash (0.0.1): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- flutter_secure_storage (6.0.0): - flutter_secure_storage (6.0.0):
- Flutter - Flutter
@ -74,6 +74,8 @@ PODS:
- Flutter - Flutter
- metadata_god (0.0.1): - metadata_god (0.0.1):
- Flutter - Flutter
- open_file_ios (0.0.1):
- Flutter
- OrderedSet (6.0.3) - OrderedSet (6.0.3)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
@ -88,21 +90,24 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.3): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- "sqlite3 (3.46.0+1)": - sqlite3 (3.47.1):
- "sqlite3/common (= 3.46.0+1)" - sqlite3/common (= 3.47.1)
- "sqlite3/common (3.46.0+1)" - sqlite3/common (3.47.1)
- "sqlite3/fts5 (3.46.0+1)": - sqlite3/dbstatvtab (3.47.1):
- sqlite3/common - sqlite3/common
- "sqlite3/perf-threadsafe (3.46.0+1)": - sqlite3/fts5 (3.47.1):
- sqlite3/common - sqlite3/common
- "sqlite3/rtree (3.46.0+1)": - sqlite3/perf-threadsafe (3.47.1):
- sqlite3/common
- sqlite3/rtree (3.47.1):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- sqlite3 (~> 3.46.0) - sqlite3 (~> 3.47.0)
- sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
- sqlite3/rtree - sqlite3/rtree
@ -130,11 +135,12 @@ DEPENDENCIES:
- media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`) - media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- metadata_god (from `.symlinks/plugins/metadata_god/ios`) - metadata_god (from `.symlinks/plugins/metadata_god/ios`)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -186,6 +192,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios" :path: ".symlinks/plugins/media_kit_native_event_loop/ios"
metadata_god: metadata_god:
:path: ".symlinks/plugins/metadata_god/ios" :path: ".symlinks/plugins/metadata_god/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@ -194,8 +202,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite: sqflite_darwin:
:path: ".symlinks/plugins/sqflite/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs: sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios" :path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios: url_launcher_ios:
@ -206,35 +214,36 @@ SPEC CHECKSUMS:
audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_selector_ios: 78baf21d03f1e37a7df97bb2494f9cd86de8fa5d file_selector_ios: f0670c1064a8c8450e38145d8043160105d0b97c
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882 flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5 flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3 media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3
media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837 media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837
metadata_god: 4bbd8523cdb5d42c5e59d2fabad01ff8f4bc53f9 metadata_god: 4bbd8523cdb5d42c5e59d2fabad01ff8f4bc53f9
open_file_ios: 461db5853723763573e140de3193656f91990d9e
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 sqlite3: 1e522f0938463e44b7faf50393b40bdc1e1e456d
sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31 sqlite3_flutter_libs: b55ef23cfafea5318ae5081e0bf3fbbce8417c94
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2

View File

@ -1,7 +1,7 @@
import UIKit import UIKit
import Flutter import Flutter
@UIApplicationMain @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,

View File

@ -43,12 +43,14 @@ class $AssetsTutorialGen {
class Assets { class Assets {
Assets._(); Assets._();
static const String license = 'LICENSE';
static const AssetGenImage albumPlaceholder = static const AssetGenImage albumPlaceholder =
AssetGenImage('assets/album-placeholder.png'); AssetGenImage('assets/album-placeholder.png');
static const AssetGenImage bengaliPatternsBg = static const AssetGenImage bengaliPatternsBg =
AssetGenImage('assets/bengali-patterns-bg.jpg'); AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage branding = AssetGenImage('assets/branding.png');
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.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 jiosaavn = AssetGenImage('assets/jiosaavn.png');
static const AssetGenImage likedTracks = static const AssetGenImage likedTracks =
AssetGenImage('assets/liked-tracks.jpg'); AssetGenImage('assets/liked-tracks.jpg');
@ -91,10 +93,12 @@ class Assets {
/// List of all assets /// List of all assets
static List<dynamic> get values => [ static List<dynamic> get values => [
license,
albumPlaceholder, albumPlaceholder,
bengaliPatternsBg, bengaliPatternsBg,
branding, branding,
emptyBox, emptyBox,
invidious,
jiosaavn, jiosaavn,
likedTracks, likedTracks,
placeholder, placeholder,
@ -120,10 +124,17 @@ class Assets {
} }
class AssetGenImage { class AssetGenImage {
const AssetGenImage(this._assetName); const AssetGenImage(
this._assetName, {
this.size,
this.flavors = const {},
});
final String _assetName; final String _assetName;
final Size? size;
final Set<String> flavors;
Image image({ Image image({
Key? key, Key? key,
AssetBundle? bundle, AssetBundle? bundle,
@ -142,7 +153,7 @@ class AssetGenImage {
ImageRepeat repeat = ImageRepeat.noRepeat, ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice, Rect? centerSlice,
bool matchTextDirection = false, bool matchTextDirection = false,
bool gaplessPlayback = false, bool gaplessPlayback = true,
bool isAntiAlias = false, bool isAntiAlias = false,
String? package, String? package,
FilterQuality filterQuality = FilterQuality.low, FilterQuality filterQuality = FilterQuality.low,

View File

@ -128,9 +128,12 @@ final routerProvider = Provider((ref) {
pageBuilder: (context, state) { pageBuilder: (context, state) {
assert(state.extra is String); assert(state.extra is String);
return SpotubePage( return SpotubePage(
child: LocalLibraryPage(state.extra as String, child: LocalLibraryPage(
isDownloads: state.extra as String,
state.uri.queryParameters["downloads"] != null), isDownloads:
state.uri.queryParameters["downloads"] != null,
isCache: state.uri.queryParameters["cache"] != null,
),
); );
}, },
), ),

View File

@ -124,4 +124,7 @@ abstract class SpotubeIcons {
static const chart = FeatherIcons.barChart2; static const chart = FeatherIcons.barChart2;
static const folderAdd = FeatherIcons.folderPlus; static const folderAdd = FeatherIcons.folderPlus;
static const folderRemove = FeatherIcons.folderMinus; static const folderRemove = FeatherIcons.folderMinus;
static const cache = FeatherIcons.hardDrive;
static const export = Icons.file_open_outlined;
static const delete = FeatherIcons.trash2;
} }

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -37,6 +38,33 @@ extension TrackExtensions on Track {
return this; 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 TrackSimpleExtensions on TrackSimple { extension TrackSimpleExtensions on TrackSimple {

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
void useCustomStatusBarColor( VoidCallback useCustomStatusBarColor(
Color color, Color color,
bool isCurrentRoute, { bool isCurrentRoute, {
bool noSetBGColor = false, bool noSetBGColor = false,
@ -10,14 +10,19 @@ void useCustomStatusBarColor(
}) { }) {
final context = useContext(); final context = useContext();
final backgroundColor = Theme.of(context).scaffoldBackgroundColor; final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
resetStatusbar() => SystemChrome.setSystemUIOverlayStyle( // ignore: invalid_use_of_visible_for_testing_member
SystemUiOverlayStyle( final previousState = SystemChrome.latestStyle;
statusBarColor: backgroundColor, // status bar color
statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179 void resetStatusbar() => previousState != null
? Brightness.dark ? SystemChrome.setSystemUIOverlayStyle(previousState)
: Brightness.light, : SystemChrome.setSystemUIOverlayStyle(
), SystemUiOverlayStyle(
); statusBarColor: backgroundColor, // status bar color
statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179
? Brightness.dark
: Brightness.light,
),
);
// ignore: invalid_use_of_visible_for_testing_member // ignore: invalid_use_of_visible_for_testing_member
final statusBarColor = SystemChrome.latestStyle?.statusBarColor; final statusBarColor = SystemChrome.latestStyle?.statusBarColor;
@ -54,4 +59,6 @@ void useCustomStatusBarColor(
useEffect(() { useEffect(() {
return resetStatusbar; return resetStatusbar;
}, []); }, []);
return resetStatusbar;
} }

View File

@ -387,5 +387,19 @@
"total_money": "المجموع {money}", "total_money": "المجموع {money}",
"webview_not_found": "لم يتم العثور على Webview", "webview_not_found": "لم يتم العثور على Webview",
"webview_not_found_description": "لم يتم تثبيت بيئة تشغيل Webview على جهازك.\nإذا كانت مثبتة، تأكد من وجودها في environment PATH\n\nبعد التثبيت، أعد تشغيل التطبيق", "webview_not_found_description": "لم يتم تثبيت بيئة تشغيل Webview على جهازك.\nإذا كانت مثبتة، تأكد من وجودها في environment PATH\n\nبعد التثبيت، أعد تشغيل التطبيق",
"unsupported_platform": "المنصة غير مدعومة" "unsupported_platform": "المنصة غير مدعومة",
"invidious_instance": "مثيل خادم Invidious",
"invidious_description": "مثيل خادم Invidious المستخدم لمطابقة المسارات",
"invidious_warning": "قد لا تعمل بعض الخوادم بشكل جيد. استخدمها على مسؤوليتك الخاصة",
"invidious_source_description": "مشابه لـ Piped ولكن بتوافر أعلى",
"cache_music": "تخزين الموسيقى مؤقتًا",
"open": "فتح",
"cache_folder": "مجلد التخزين المؤقت",
"export": "تصدير",
"clear_cache": "مسح التخزين المؤقت",
"clear_cache_confirmation": "هل تريد مسح التخزين المؤقت؟",
"export_cache_files": "تصدير الملفات المخزنة مؤقتًا",
"found_n_files": "تم العثور على {count} ملف",
"export_cache_confirmation": "هل تريد تصدير هذه الملفات إلى",
"exported_n_out_of_m_files": "تم تصدير {filesExported} من أصل {files} ملفات"
} }

View File

@ -387,5 +387,19 @@
"total_money": "মোট {money}", "total_money": "মোট {money}",
"webview_not_found": "ওয়েবভিউ পাওয়া যায়নি", "webview_not_found": "ওয়েবভিউ পাওয়া যায়নি",
"webview_not_found_description": "আপনার ডিভাইসে কোনো ওয়েবভিউ রানটাইম ইনস্টল করা নেই।\nযদি ইনস্টল থাকে, তা নিশ্চিত করুন যে এটি environment PATH এ রয়েছে\n\nইনস্টল করার পর, অ্যাপটি পুনরায় চালু করুন", "webview_not_found_description": "আপনার ডিভাইসে কোনো ওয়েবভিউ রানটাইম ইনস্টল করা নেই।\nযদি ইনস্টল থাকে, তা নিশ্চিত করুন যে এটি environment PATH এ রয়েছে\n\nইনস্টল করার পর, অ্যাপটি পুনরায় চালু করুন",
"unsupported_platform": "সমর্থিত প্ল্যাটফর্ম নয়" "unsupported_platform": "সমর্থিত প্ল্যাটফর্ম নয়",
"invidious_instance": "ইনভিডিয়াস সার্ভার ইন্সটেন্স",
"invidious_description": "ট্রাক মিলানোর জন্য ব্যবহৃত ইনভিডিয়াস সার্ভার",
"invidious_warning": "কিছু সার্ভার ভাল কাজ নাও করতে পারে। নিজের ঝুঁকিতে ব্যবহার করুন",
"invidious_source_description": "পাইপের মতো কিন্তু আরও বেশি উপলব্ধতা সহ",
"cache_music": "ক্যাশে সংগীত",
"open": "খুলুন",
"cache_folder": "ক্যাশে ফোল্ডার",
"export": "রপ্তানি",
"clear_cache": "ক্যাশে পরিষ্কার",
"clear_cache_confirmation": "আপনি কি ক্যাশে পরিষ্কার করতে চান?",
"export_cache_files": "ক্যাশে ফাইল রপ্তানি",
"found_n_files": "{count} টি ফাইল পাওয়া গেছে",
"export_cache_confirmation": "আপনি কি এই ফাইলগুলি রপ্তানি করতে চান",
"exported_n_out_of_m_files": "{filesExported} টি ফাইল রপ্তানি করা হয়েছে {files} এর মধ্যে"
} }

View File

@ -387,5 +387,19 @@
"total_money": "total {money}", "total_money": "total {money}",
"webview_not_found": "No s'ha trobat el Webview", "webview_not_found": "No s'ha trobat el Webview",
"webview_not_found_description": "No hi ha cap temps d'execució de Webview instal·lat al dispositiu.\nSi està instal·lat, assegureu-vos que estigui en el environment PATH\n\nDesprés d'instal·lar-lo, reinicieu l'aplicació", "webview_not_found_description": "No hi ha cap temps d'execució de Webview instal·lat al dispositiu.\nSi està instal·lat, assegureu-vos que estigui en el environment PATH\n\nDesprés d'instal·lar-lo, reinicieu l'aplicació",
"unsupported_platform": "Plataforma no compatible" "unsupported_platform": "Plataforma no compatible",
"invidious_instance": "Instància del servidor Invidious",
"invidious_description": "La instància del servidor Invidious per fer coincidir pistes",
"invidious_warning": "Algunes instàncies podrien no funcionar bé. Feu-les servir sota la vostra responsabilitat",
"invidious_source_description": "Similar a Piped però amb més disponibilitat",
"cache_music": "Música en caché",
"open": "Obrir",
"cache_folder": "Carpeta de caché",
"export": "Exportar",
"clear_cache": "Netejar caché",
"clear_cache_confirmation": "Voleu netejar la memòria cau?",
"export_cache_files": "Exportar arxius en caché",
"found_n_files": "S'han trobat {count} arxius",
"export_cache_confirmation": "Voleu exportar aquests arxius a",
"exported_n_out_of_m_files": "S'han exportat {filesExported} de {files} arxius"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Celkem {money}", "total_money": "Celkem {money}",
"webview_not_found": "Webview nebyl nalezen", "webview_not_found": "Webview nebyl nalezen",
"webview_not_found_description": "Na vašem zařízení není nainstalováno žádné runtime prostředí Webview.\nPokud je nainstalováno, ujistěte se, že je v environment PATH\n\nPo instalaci restartujte aplikaci", "webview_not_found_description": "Na vašem zařízení není nainstalováno žádné runtime prostředí Webview.\nPokud je nainstalováno, ujistěte se, že je v environment PATH\n\nPo instalaci restartujte aplikaci",
"unsupported_platform": "Nepodporovaná platforma" "unsupported_platform": "Nepodporovaná platforma",
"invidious_instance": "Instance serveru Invidious",
"invidious_description": "Instance serveru Invidious pro párování stop",
"invidious_warning": "Některé instance nemusí fungovat správně. Používejte na vlastní riziko",
"invidious_source_description": "Podobné Piped, ale s vyšší dostupností",
"cache_music": "Hudba v mezipaměti",
"open": "Otevřít",
"cache_folder": "Složka mezipaměti",
"export": "Exportovat",
"clear_cache": "Vymazat mezipaměť",
"clear_cache_confirmation": "Opravdu chcete vymazat mezipaměť?",
"export_cache_files": "Exportovat soubory z mezipaměti",
"found_n_files": "Nalezeno {count} souborů",
"export_cache_confirmation": "Chcete exportovat tyto soubory do",
"exported_n_out_of_m_files": "Exportováno {filesExported} z {files} souborů"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Gesamt {money}", "total_money": "Gesamt {money}",
"webview_not_found": "Webview nicht gefunden", "webview_not_found": "Webview nicht gefunden",
"webview_not_found_description": "Es ist keine Webview-Laufzeitumgebung auf Ihrem Gerät installiert.\nFalls installiert, stellen Sie sicher, dass es im environment PATH ist\n\nNach der Installation starten Sie die App neu", "webview_not_found_description": "Es ist keine Webview-Laufzeitumgebung auf Ihrem Gerät installiert.\nFalls installiert, stellen Sie sicher, dass es im environment PATH ist\n\nNach der Installation starten Sie die App neu",
"unsupported_platform": "Nicht unterstützte Plattform" "unsupported_platform": "Nicht unterstützte Plattform",
"invidious_instance": "Invidious-Serverinstanz",
"invidious_description": "Die Invidious-Serverinstanz zur Titelerkennung",
"invidious_warning": "Einige Instanzen funktionieren möglicherweise nicht gut. Benutzung auf eigene Gefahr",
"invidious_source_description": "Ähnlich wie Piped, aber mit höherer Verfügbarkeit",
"cache_music": "Musik zwischenspeichern",
"open": "Öffnen",
"cache_folder": "Cache-Ordner",
"export": "Exportieren",
"clear_cache": "Cache leeren",
"clear_cache_confirmation": "Möchten Sie den Cache leeren?",
"export_cache_files": "Cachedateien exportieren",
"found_n_files": "{count} Dateien gefunden",
"export_cache_confirmation": "Möchten Sie diese Dateien exportieren nach",
"exported_n_out_of_m_files": "{filesExported} von {files} Dateien exportiert"
} }

View File

@ -190,6 +190,9 @@
"piped_instance": "Piped Server Instance", "piped_instance": "Piped Server Instance",
"piped_description": "The Piped server instance to use for track matching", "piped_description": "The Piped server instance to use for track matching",
"piped_warning": "Some of them might not work well. So use at your own risk", "piped_warning": "Some of them might not work well. So use at your own risk",
"invidious_instance": "Invidious Server Instance",
"invidious_description": "The Invidious server instance to use for track matching",
"invidious_warning": "Some of them might not work well. So use at your own risk",
"generate_playlist": "Generate Playlist", "generate_playlist": "Generate Playlist",
"track_exists": "Track {track} already exists", "track_exists": "Track {track} already exists",
"replace_downloaded_tracks": "Replace all downloaded tracks", "replace_downloaded_tracks": "Replace all downloaded tracks",
@ -307,6 +310,7 @@
"youtube_source_description": "Recommended and works best.", "youtube_source_description": "Recommended and works best.",
"piped_source_description": "Feeling free? Same as YouTube but a lot free.", "piped_source_description": "Feeling free? Same as YouTube but a lot free.",
"jiosaavn_source_description": "Best for South Asian region.", "jiosaavn_source_description": "Best for South Asian region.",
"invidious_source_description": "Similar to Piped but with higher availability.",
"highest_quality": "Highest Quality: {quality}", "highest_quality": "Highest Quality: {quality}",
"select_audio_source": "Select Audio Source", "select_audio_source": "Select Audio Source",
"endless_playback_description": "Automatically append new songs\nto the end of the queue", "endless_playback_description": "Automatically append new songs\nto the end of the queue",
@ -387,5 +391,15 @@
"total_money": "Total {money}", "total_money": "Total {money}",
"webview_not_found": "Webview not found", "webview_not_found": "Webview not found",
"webview_not_found_description": "No webview runtime is installed in your device.\nIf it's installed make sure it's in the Environment PATH\n\nAfter installing, restart the app", "webview_not_found_description": "No webview runtime is installed in your device.\nIf it's installed make sure it's in the Environment PATH\n\nAfter installing, restart the app",
"unsupported_platform": "Unsupported platform" "unsupported_platform": "Unsupported platform",
"cache_music": "Cache music",
"open": "Open",
"cache_folder": "Cache folder",
"export": "Export",
"clear_cache": "Clear cache",
"clear_cache_confirmation": "Do you want to clear the cache?",
"export_cache_files": "Export Cached Files",
"found_n_files": "Found {count} files",
"export_cache_confirmation": "Do you want to export these files to",
"exported_n_out_of_m_files": "Exported {filesExported} out of {files} files"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Total {money}", "total_money": "Total {money}",
"webview_not_found": "No se encontró el Webview", "webview_not_found": "No se encontró el Webview",
"webview_not_found_description": "No hay tiempo de ejecución de Webview instalado en su dispositivo.\nSi está instalado, asegúrese de que esté en el environment PATH\n\nDespués de instalar, reinicie la aplicación", "webview_not_found_description": "No hay tiempo de ejecución de Webview instalado en su dispositivo.\nSi está instalado, asegúrese de que esté en el environment PATH\n\nDespués de instalar, reinicie la aplicación",
"unsupported_platform": "Plataforma no soportada" "unsupported_platform": "Plataforma no soportada",
"invidious_instance": "Instancia del Servidor Invidious",
"invidious_description": "La instancia del servidor Invidious para identificar pistas",
"invidious_warning": "Algunas instancias podrían no funcionar bien. Úselas bajo su propio riesgo",
"invidious_source_description": "Similar a Piped, pero con mayor disponibilidad",
"cache_music": "Caché de música",
"open": "Abrir",
"cache_folder": "Carpeta de caché",
"export": "Exportar",
"clear_cache": "Limpiar caché",
"clear_cache_confirmation": "¿Desea limpiar la caché?",
"export_cache_files": "Exportar archivos en caché",
"found_n_files": "Se encontraron {count} archivos",
"export_cache_confirmation": "¿Desea exportar estos archivos a",
"exported_n_out_of_m_files": "Se exportaron {filesExported} de {files} archivos"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Guztira {money}", "total_money": "Guztira {money}",
"webview_not_found": "Ez da Webview aurkitu", "webview_not_found": "Ez da Webview aurkitu",
"webview_not_found_description": "Ez dago Webview abiarazte denbora-instalaziorik zure gailuan.\nInstalatuta badago, ziurtatu environment PATH-an dagoela\n\nInstalatu ondoren, berrabiarazi aplikazioa", "webview_not_found_description": "Ez dago Webview abiarazte denbora-instalaziorik zure gailuan.\nInstalatuta badago, ziurtatu environment PATH-an dagoela\n\nInstalatu ondoren, berrabiarazi aplikazioa",
"unsupported_platform": "Plataforma ez onartua" "unsupported_platform": "Plataforma ez onartua",
"invidious_instance": "Invidious zerbitzari instantzia",
"invidious_description": "Invidious zerbitzari instantzia, pistak bat egiteko",
"invidious_warning": "Instantzia batzuek ez dute ondo funtzionatuko. Zure erantzukizunpean erabili",
"invidious_source_description": "Piped-en antzekoa, baina eskuragarritasun handiagoarekin",
"cache_music": "Musika cachean",
"open": "Ireki",
"cache_folder": "Cache karpeta",
"export": "Esportatu",
"clear_cache": "Garbitu cachea",
"clear_cache_confirmation": "Cachea garbitu nahi al duzu?",
"export_cache_files": "Esportatu cache fitxategiak",
"found_n_files": "{count} fitxategi aurkitu dira",
"export_cache_confirmation": "Fitxategi hauek esportatu nahi al dituzu",
"exported_n_out_of_m_files": "{filesExported} fitxategi esportatu dira {files} -tik"
} }

View File

@ -387,5 +387,19 @@
"total_money": "مجموع {money}", "total_money": "مجموع {money}",
"webview_not_found": "وب‌ویو پیدا نشد", "webview_not_found": "وب‌ویو پیدا نشد",
"webview_not_found_description": "هیچ اجرای وب‌ویو روی دستگاه شما نصب نشده است.\nدر صورت نصب، مطمئن شوید که در environment PATH قرار دارد\n\nپس از نصب، برنامه را مجدداً راه‌اندازی کنید", "webview_not_found_description": "هیچ اجرای وب‌ویو روی دستگاه شما نصب نشده است.\nدر صورت نصب، مطمئن شوید که در environment PATH قرار دارد\n\nپس از نصب، برنامه را مجدداً راه‌اندازی کنید",
"unsupported_platform": "پلتفرم پشتیبانی نمی‌شود" "unsupported_platform": "پلتفرم پشتیبانی نمی‌شود",
"invidious_instance": "نمونه سرور Invidious",
"invidious_description": "نمونه سرور Invidious برای تطبیق آهنگ",
"invidious_warning": "برخی از نمونه‌ها ممکن است به خوبی کار نکنند. با احتیاط استفاده کنید",
"invidious_source_description": "شبیه Piped اما با در دسترس بودن بیشتر",
"cache_music": "موسیقی در حافظه موقت",
"open": "باز کردن",
"cache_folder": "پوشه حافظه موقت",
"export": "صادر کردن",
"clear_cache": "پاک کردن حافظه موقت",
"clear_cache_confirmation": "آیا می‌خواهید حافظه موقت را پاک کنید؟",
"export_cache_files": "صادر کردن فایل‌های حافظه موقت",
"found_n_files": "{count} فایل یافت شد",
"export_cache_confirmation": "آیا می‌خواهید این فایل‌ها را صادر کنید به",
"exported_n_out_of_m_files": "{filesExported} از {files} فایل صادر شد"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Yhteensä {money}", "total_money": "Yhteensä {money}",
"webview_not_found": "Webview ei löydy", "webview_not_found": "Webview ei löydy",
"webview_not_found_description": "Laitteellasi ei ole asennettua Webview-ajonaikaa.\nJos se on asennettu, varmista, että se on environment PATH:ssa\n\nAsennuksen jälkeen käynnistä sovellus uudelleen", "webview_not_found_description": "Laitteellasi ei ole asennettua Webview-ajonaikaa.\nJos se on asennettu, varmista, että se on environment PATH:ssa\n\nAsennuksen jälkeen käynnistä sovellus uudelleen",
"unsupported_platform": "Ei tuettu alusta" "unsupported_platform": "Ei tuettu alusta",
"invidious_instance": "Invidious-palvelinesiintymä",
"invidious_description": "Invidious-palvelinesiintymä raitojen yhteensovittamiseen",
"invidious_warning": "Jotkin esiintymät eivät välttämättä toimi hyvin. Käytä omalla vastuullasi",
"invidious_source_description": "Samankaltainen kuin Piped, mutta korkeammalla saatavuudella",
"cache_music": "Musiikki välimuistissa",
"open": "Avaa",
"cache_folder": "Välimuistikansio",
"export": "Vie",
"clear_cache": "Tyhjennä välimuisti",
"clear_cache_confirmation": "Haluatko tyhjentää välimuistin?",
"export_cache_files": "Vie välimuistitiedostot",
"found_n_files": "Löydettiin {count} tiedostoa",
"export_cache_confirmation": "Haluatko viedä nämä tiedostot",
"exported_n_out_of_m_files": "Vietiin {filesExported}/{files} tiedostoa"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Total {money}", "total_money": "Total {money}",
"webview_not_found": "Webview non trouvé", "webview_not_found": "Webview non trouvé",
"webview_not_found_description": "Aucun environnement d'exécution Webview installé sur votre appareil.\nSi c'est installé, assurez-vous qu'il soit dans le environment PATH\n\nAprès l'installation, redémarrez l'application", "webview_not_found_description": "Aucun environnement d'exécution Webview installé sur votre appareil.\nSi c'est installé, assurez-vous qu'il soit dans le environment PATH\n\nAprès l'installation, redémarrez l'application",
"unsupported_platform": "Plateforme non prise en charge" "unsupported_platform": "Plateforme non prise en charge",
"invidious_instance": "Instance de serveur Invidious",
"invidious_description": "L'instance de serveur Invidious à utiliser pour la correspondance de pistes",
"invidious_warning": "Certaines instances pourraient ne pas bien fonctionner. À utiliser à vos risques et périls",
"invidious_source_description": "Similaire à Piped mais avec une meilleure disponibilité",
"cache_music": "Mettre la musique en cache",
"open": "Ouvrir",
"cache_folder": "Dossier du cache",
"export": "Exporter",
"clear_cache": "Effacer le cache",
"clear_cache_confirmation": "Voulez-vous effacer le cache ?",
"export_cache_files": "Exporter les fichiers en cache",
"found_n_files": "{count} fichiers trouvés",
"export_cache_confirmation": "Voulez-vous exporter ces fichiers vers",
"exported_n_out_of_m_files": "{filesExported} fichiers exportés sur {files}"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।", "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।",
"webview_not_found": "वेबव्यू नहीं मिला", "webview_not_found": "वेबव्यू नहीं मिला",
"webview_not_found_description": "आपके डिवाइस पर वेबव्यू रनटाइम इंस्टॉल नहीं है।\nअगर इंस्टॉल है, तो सुनिश्चित करें कि यह environment PATH में है\n\nइंस्टॉल करने के बाद, ऐप को पुनः शुरू करें", "webview_not_found_description": "आपके डिवाइस पर वेबव्यू रनटाइम इंस्टॉल नहीं है।\nअगर इंस्टॉल है, तो सुनिश्चित करें कि यह environment PATH में है\n\nइंस्टॉल करने के बाद, ऐप को पुनः शुरू करें",
"unsupported_platform": "असमर्थित प्लेटफार्म" "unsupported_platform": "असमर्थित प्लेटफार्म",
"invidious_instance": "इन्विडियस सर्वर इंस्टेंस",
"invidious_description": "ट्रैक मिलान के लिए इन्विडियस सर्वर इंस्टेंस",
"invidious_warning": "कुछ इंस्टेंस अच्छी तरह से काम नहीं कर सकते। अपने जोखिम पर उपयोग करें",
"invidious_source_description": "पाइप्ड के समान, लेकिन अधिक उपलब्धता के साथ",
"cache_music": "संगीत को कैश करें",
"open": "खोलें",
"cache_folder": "कैश फ़ोल्डर",
"export": "निर्यात करें",
"clear_cache": "कैश साफ़ करें",
"clear_cache_confirmation": "क्या आप कैश साफ़ करना चाहते हैं?",
"export_cache_files": "कैश फ़ाइलें निर्यात करें",
"found_n_files": "{count} फ़ाइलें मिलीं",
"export_cache_confirmation": "क्या आप इन फ़ाइलों को निर्यात करना चाहते हैं",
"exported_n_out_of_m_files": "{filesExported} फ़ाइलें निर्यात की गईं {files} में से"
} }

View File

@ -387,5 +387,19 @@
"total_money": "Total {money}", "total_money": "Total {money}",
"webview_not_found": "Webview tidak ditemukan", "webview_not_found": "Webview tidak ditemukan",
"webview_not_found_description": "Tidak ada runtime Webview yang diinstal di perangkat Anda.\nJika sudah diinstal, pastikan itu ada di environment PATH\n\nSetelah diinstal, restart aplikasi", "webview_not_found_description": "Tidak ada runtime Webview yang diinstal di perangkat Anda.\nJika sudah diinstal, pastikan itu ada di environment PATH\n\nSetelah diinstal, restart aplikasi",
"unsupported_platform": "Platform tidak didukung" "unsupported_platform": "Platform tidak didukung",
"invidious_instance": "Invidious Server Instance",
"invidious_description": "The Invidious server instance to use for track matching",
"invidious_warning": "Some of them might not work well. So use at your own risk",
"invidious_source_description": "Similar to Piped but with higher availability.",
"cache_music": "Cache music",
"open": "Open",
"cache_folder": "Cache folder",
"export": "Export",
"clear_cache": "Clear cache",
"clear_cache_confirmation": "Do you want to clear the cache?",
"export_cache_files": "Export Cached Files",
"found_n_files": "Found {count} files",
"export_cache_confirmation": "Do you want to export these files to",
"exported_n_out_of_m_files": "Exported {filesExported} out of {files} files"
} }

View File

@ -388,5 +388,19 @@
"total_money": "Totale {money}", "total_money": "Totale {money}",
"webview_not_found": "Webview non trovato", "webview_not_found": "Webview non trovato",
"webview_not_found_description": "Nessun runtime Webview installato nel tuo dispositivo.\nSe è installato, assicurati che sia nel environment PATH\n\nDopo l'installazione, riavvia l'app", "webview_not_found_description": "Nessun runtime Webview installato nel tuo dispositivo.\nSe è installato, assicurati che sia nel environment PATH\n\nDopo l'installazione, riavvia l'app",
"unsupported_platform": "Piattaforma non supportata" "unsupported_platform": "Piattaforma non supportata",
"invidious_instance": "Istanza del server Invidious",
"invidious_description": "L'istanza del server Invidious da utilizzare per il matching delle tracce",
"invidious_warning": "Alcuni potrebbero non funzionare bene. Usali a tuo rischio",
"invidious_source_description": "Simile a Piped ma con maggiore disponibilità.",
"cache_music": "Cache musica",
"open": "Apri",
"cache_folder": "Cartella cache",
"export": "Esporta",
"clear_cache": "Cancella cache",
"clear_cache_confirmation": "Vuoi cancellare la cache?",
"export_cache_files": "Esporta file nella cache",
"found_n_files": "Trovati {count} file",
"export_cache_confirmation": "Vuoi esportare questi file su",
"exported_n_out_of_m_files": "Esportati {filesExported} su {files} file"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。", "spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。",
"webview_not_found": "Webviewが見つかりません", "webview_not_found": "Webviewが見つかりません",
"webview_not_found_description": "デバイスにWebviewランタイムがインストールされていません。\nインストールされている場合は、environment PATHにあることを確認してください\n\nインストール後、アプリを再起動してください", "webview_not_found_description": "デバイスにWebviewランタイムがインストールされていません。\nインストールされている場合は、environment PATHにあることを確認してください\n\nインストール後、アプリを再起動してください",
"unsupported_platform": "サポートされていないプラットフォーム" "unsupported_platform": "サポートされていないプラットフォーム",
"invidious_instance": "Invidiousサーバーインスタンス",
"invidious_description": "トラックマッチングに使用するInvidiousサーバーインスタンス",
"invidious_warning": "一部はうまく機能しない可能性があります。自己責任で使用してください",
"invidious_source_description": "Pipedに似ていますが、より高い可用性があります。",
"cache_music": "音楽をキャッシュ",
"open": "開く",
"cache_folder": "キャッシュフォルダー",
"export": "エクスポート",
"clear_cache": "キャッシュをクリア",
"clear_cache_confirmation": "キャッシュをクリアしますか?",
"export_cache_files": "キャッシュされたファイルをエクスポート",
"found_n_files": "{count}ファイルが見つかりました",
"export_cache_confirmation": "これらのファイルをエクスポートしますか",
"exported_n_out_of_m_files": "{filesExported} / {files}ファイルがエクスポートされました"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე.", "spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე.",
"webview_not_found": "ვებვიუ ვერ მოიძებნა", "webview_not_found": "ვებვიუ ვერ მოიძებნა",
"webview_not_found_description": "თქვენს მოწყობილობაზე ვებვიუის შესრულების დრო არ არის დაყენებული.\nთუ დაყენებულია, დარწმუნდით, რომ ის environment PATH-შია\n\nდაენების შემდეგ, გადატვირთეთ აპი", "webview_not_found_description": "თქვენს მოწყობილობაზე ვებვიუის შესრულების დრო არ არის დაყენებული.\nთუ დაყენებულია, დარწმუნდით, რომ ის environment PATH-შია\n\nდაენების შემდეგ, გადატვირთეთ აპი",
"unsupported_platform": "მოუხერხებელი პლატფორმა" "unsupported_platform": "მოუხერხებელი პლატფორმა",
"invidious_instance": "Invidious სერვერის ინსტანცია",
"invidious_description": "Invidious სერვერის ინსტანცია, რომელიც გამოიყენება ტრეკის შესატყვისად",
"invidious_warning": "ზოგიერთი შეიძლება კარგად არ მუშაობდეს. გამოიყენეთ თქვენს პასუხისმგებლობაზე",
"invidious_source_description": "მსგავსია Piped-ის, მაგრამ მაღალი ხელმისაწვდომობით.",
"cache_music": "მუსიკის ქეში",
"open": "გახსენით",
"cache_folder": "ქეშის საქაღალდე",
"export": "ექსპორტი",
"clear_cache": "ქეშის გასუფთავება",
"clear_cache_confirmation": "გსურთ ქეშის გასუფთავება?",
"export_cache_files": "ქეშირებული ფაილების ექსპორტი",
"found_n_files": "ნაპოვნია {count} ფაილი",
"export_cache_confirmation": "გსურთ ამ ფაილების ექსპორტი",
"exported_n_out_of_m_files": "{filesExported} ფაილი {files}-დან ექსპორტირებულია"
} }

View File

@ -388,5 +388,19 @@
"spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다.", "spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다.",
"webview_not_found": "웹뷰를 찾을 수 없음", "webview_not_found": "웹뷰를 찾을 수 없음",
"webview_not_found_description": "기기에 웹뷰 런타임이 설치되지 않았습니다.\n설치되어 있으면 environment PATH에 있는지 확인하십시오\n\n설치 후 앱을 다시 시작하세요", "webview_not_found_description": "기기에 웹뷰 런타임이 설치되지 않았습니다.\n설치되어 있으면 environment PATH에 있는지 확인하십시오\n\n설치 후 앱을 다시 시작하세요",
"unsupported_platform": "지원되지 않는 플랫폼" "unsupported_platform": "지원되지 않는 플랫폼",
"invidious_instance": "Invidious 서버 인스턴스",
"invidious_description": "트랙 매칭에 사용할 Invidious 서버 인스턴스",
"invidious_warning": "일부는 제대로 작동하지 않을 수 있습니다. 자신의 책임 하에 사용하세요",
"invidious_source_description": "Piped와 비슷하지만 가용성이 높습니다.",
"cache_music": "음악 캐시",
"open": "열기",
"cache_folder": "캐시 폴더",
"export": "내보내기",
"clear_cache": "캐시 지우기",
"clear_cache_confirmation": "캐시를 지우시겠습니까?",
"export_cache_files": "캐시된 파일 내보내기",
"found_n_files": "{count}개의 파일을 찾았습니다",
"export_cache_confirmation": "이 파일들을 내보내시겠습니까",
"exported_n_out_of_m_files": "{files}개 중 {filesExported}개 파일을 내보냈습니다"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।", "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।",
"webview_not_found": "वेबभ्यू फेला परेन", "webview_not_found": "वेबभ्यू फेला परेन",
"webview_not_found_description": "तपाईंको उपकरणमा कुनै वेबभ्यू रनटाइम स्थापना गरिएको छैन।\nयदि स्थापना गरिएको छ भने, environment PATH मा छ कि छैन भनेर सुनिश्चित गर्नुहोस्\n\nस्थापना पछि, अनुप्रयोग पुनः सुरु गर्नुहोस्", "webview_not_found_description": "तपाईंको उपकरणमा कुनै वेबभ्यू रनटाइम स्थापना गरिएको छैन।\nयदि स्थापना गरिएको छ भने, environment PATH मा छ कि छैन भनेर सुनिश्चित गर्नुहोस्\n\nस्थापना पछि, अनुप्रयोग पुनः सुरु गर्नुहोस्",
"unsupported_platform": "असमर्थित प्लेटफार्म" "unsupported_platform": "असमर्थित प्लेटफार्म",
"invidious_instance": "Invidious सर्भर इन्स्टेन्स",
"invidious_description": "ट्र्याक मिलाउनका लागि प्रयोग हुने Invidious सर्भर इन्स्टेन्स",
"invidious_warning": "केहीले राम्रोसँग काम नगर्न सक्छ। आफ्नो जोखिममा प्रयोग गर्नुहोस्",
"invidious_source_description": "Piped जस्तै तर उच्च उपलब्धतासँग।",
"cache_music": "सङ्गीत क्यास गर्नुहोस्",
"open": "खोल्नुहोस्",
"cache_folder": "क्यास फोल्डर",
"export": "निर्यात गर्नुहोस्",
"clear_cache": "क्यास खाली गर्नुहोस्",
"clear_cache_confirmation": "के तपाई क्यास खाली गर्न चाहनुहुन्छ?",
"export_cache_files": "क्यास फाइलहरू निर्यात गर्नुहोस्",
"found_n_files": "{count} फाइलहरू फेला परे",
"export_cache_confirmation": "यी फाइलहरू निर्यात गर्न चाहनुहुन्छ",
"exported_n_out_of_m_files": "{filesExported} मध्ये {files} फाइलहरू निर्यात गरियो"
} }

View File

@ -388,5 +388,19 @@
"spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren.", "spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren.",
"webview_not_found": "Webview niet gevonden", "webview_not_found": "Webview niet gevonden",
"webview_not_found_description": "Er is geen Webview-runtime geïnstalleerd op uw apparaat.\nAls het is geïnstalleerd, zorg ervoor dat het in het environment PATH staat\n\nHerstart de app na installatie", "webview_not_found_description": "Er is geen Webview-runtime geïnstalleerd op uw apparaat.\nAls het is geïnstalleerd, zorg ervoor dat het in het environment PATH staat\n\nHerstart de app na installatie",
"unsupported_platform": "Niet ondersteund platform" "unsupported_platform": "Niet ondersteund platform",
"invidious_instance": "Invidious-serverinstantie",
"invidious_description": "De Invidious-serverinstantie die gebruikt wordt voor trackmatching",
"invidious_warning": "Sommigen werken mogelijk niet goed. Gebruik op eigen risico",
"invidious_source_description": "Vergelijkbaar met Piped, maar met een hogere beschikbaarheid.",
"cache_music": "Cache muziek",
"open": "Open",
"cache_folder": "Cachemap",
"export": "Exporteren",
"clear_cache": "Cache wissen",
"clear_cache_confirmation": "Wilt u de cache wissen?",
"export_cache_files": "Gecacheerde bestanden exporteren",
"found_n_files": "{count} bestanden gevonden",
"export_cache_confirmation": "Wilt u deze bestanden exporteren naar",
"exported_n_out_of_m_files": "{filesExported} van de {files} bestanden geëxporteerd"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify.", "spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify.",
"webview_not_found": "Nie znaleziono Webview", "webview_not_found": "Nie znaleziono Webview",
"webview_not_found_description": "Na twoim urządzeniu nie zainstalowano środowiska uruchomieniowego Webview.\nJeśli jest zainstalowany, upewnij się, że jest w environment PATH\n\nPo instalacji uruchom ponownie aplikację", "webview_not_found_description": "Na twoim urządzeniu nie zainstalowano środowiska uruchomieniowego Webview.\nJeśli jest zainstalowany, upewnij się, że jest w environment PATH\n\nPo instalacji uruchom ponownie aplikację",
"unsupported_platform": "Nieobsługiwana platforma" "unsupported_platform": "Nieobsługiwana platforma",
"invidious_instance": "Instancja serwera Invidious",
"invidious_description": "Instancja serwera Invidious do dopasowywania utworów",
"invidious_warning": "Niektóre z nich mogą nie działać dobrze. Używaj na własne ryzyko",
"invidious_source_description": "Podobne do Piped, ale o wyższej dostępności.",
"cache_music": "Pamięć podręczna muzyki",
"open": "Otwórz",
"cache_folder": "Folder pamięci podręcznej",
"export": "Eksportuj",
"clear_cache": "Wyczyść pamięć podręczną",
"clear_cache_confirmation": "Czy chcesz wyczyścić pamięć podręczną?",
"export_cache_files": "Eksportuj pliki z pamięci podręcznej",
"found_n_files": "Znaleziono {count} plików",
"export_cache_confirmation": "Czy chcesz wyeksportować te pliki do",
"exported_n_out_of_m_files": "Wyeksportowano {filesExported} z {files} plików"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify.", "spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify.",
"webview_not_found": "Webview não encontrado", "webview_not_found": "Webview não encontrado",
"webview_not_found_description": "Nenhum runtime Webview está instalado no seu dispositivo.\nSe estiver instalado, certifique-se de que está no environment PATH\n\nApós a instalação, reinicie o aplicativo", "webview_not_found_description": "Nenhum runtime Webview está instalado no seu dispositivo.\nSe estiver instalado, certifique-se de que está no environment PATH\n\nApós a instalação, reinicie o aplicativo",
"unsupported_platform": "Plataforma não suportada" "unsupported_platform": "Plataforma não suportada",
"invidious_instance": "Instância do Servidor Invidious",
"invidious_description": "A instância do servidor Invidious a ser usada para correspondência de faixas",
"invidious_warning": "Alguns podem não funcionar bem. Use por sua conta e risco",
"invidious_source_description": "Semelhante ao Piped, mas com maior disponibilidade.",
"cache_music": "Música em cache",
"open": "Abrir",
"cache_folder": "Pasta de cache",
"export": "Exportar",
"clear_cache": "Limpar cache",
"clear_cache_confirmation": "Deseja limpar o cache?",
"export_cache_files": "Exportar Arquivos em Cache",
"found_n_files": "Encontrados {count} arquivos",
"export_cache_confirmation": "Deseja exportar estes arquivos para",
"exported_n_out_of_m_files": "Exportados {filesExported} de {files} arquivos"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify.", "spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify.",
"webview_not_found": "Webview не найден", "webview_not_found": "Webview не найден",
"webview_not_found_description": "На вашем устройстве не установлена среда выполнения Webview.\nЕсли он установлен, убедитесь, что он находится в environment PATH\n\nПосле установки перезапустите приложение", "webview_not_found_description": "На вашем устройстве не установлена среда выполнения Webview.\nЕсли он установлен, убедитесь, что он находится в environment PATH\n\nПосле установки перезапустите приложение",
"unsupported_platform": "Платформа не поддерживается" "unsupported_platform": "Платформа не поддерживается",
"invidious_instance": "Экземпляр сервера Invidious",
"invidious_description": "Экземпляр сервера Invidious для сопоставления треков",
"invidious_warning": "Некоторые могут работать не очень хорошо. Используйте на свой страх и риск",
"invidious_source_description": "Похож на Piped, но с более высокой доступностью.",
"cache_music": "Кэшировать музыку",
"open": "Открыть",
"cache_folder": "Папка кэша",
"export": "Экспорт",
"clear_cache": "Очистить кэш",
"clear_cache_confirmation": "Вы хотите очистить кэш?",
"export_cache_files": "Экспортировать кэшированные файлы",
"found_n_files": "Найдено {count} файлов",
"export_cache_confirmation": "Вы хотите экспортировать эти файлы в",
"exported_n_out_of_m_files": "Экспортировано {filesExported} из {files} файлов"
} }

View File

@ -388,5 +388,19 @@
"spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify.", "spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify.",
"webview_not_found": "ไม่พบ Webview", "webview_not_found": "ไม่พบ Webview",
"webview_not_found_description": "ไม่พบ runtime ของ Webview บนอุปกรณ์ของคุณ\nหากติดตั้งแล้วตรวจสอบให้แน่ใจว่าอยู่ใน environment PATH\n\nหลังจากติดตั้งแล้ว ให้รีสตาร์ทแอป", "webview_not_found_description": "ไม่พบ runtime ของ Webview บนอุปกรณ์ของคุณ\nหากติดตั้งแล้วตรวจสอบให้แน่ใจว่าอยู่ใน environment PATH\n\nหลังจากติดตั้งแล้ว ให้รีสตาร์ทแอป",
"unsupported_platform": "แพลตฟอร์มไม่รองรับ" "unsupported_platform": "แพลตฟอร์มไม่รองรับ",
"invidious_instance": "อินสแตนซ์เซิร์ฟเวอร์ Invidious",
"invidious_description": "อินสแตนซ์เซิร์ฟเวอร์ Invidious ที่ใช้สำหรับการจับคู่เพลง",
"invidious_warning": "บางอันอาจใช้งานไม่ดี ใช้ด้วยความเสี่ยงของคุณเอง",
"invidious_source_description": "คล้ายกับ Piped แต่มีความพร้อมใช้งานสูงกว่า",
"cache_music": "แคชเพลง",
"open": "เปิด",
"cache_folder": "โฟลเดอร์แคช",
"export": "ส่งออก",
"clear_cache": "ล้างแคช",
"clear_cache_confirmation": "คุณต้องการล้างแคชหรือไม่?",
"export_cache_files": "ส่งออกไฟล์แคช",
"found_n_files": "พบ {count} ไฟล์",
"export_cache_confirmation": "คุณต้องการส่งออกไฟล์เหล่านี้ไปยัง",
"exported_n_out_of_m_files": "ส่งออก {filesExported} จาก {files} ไฟล์"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir.", "spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir.",
"webview_not_found": "Webview bulunamadı", "webview_not_found": "Webview bulunamadı",
"webview_not_found_description": "Cihazınızda herhangi bir Webview çalışma zamanı yüklü değil.\nEğer kuruluysa, ortam YOLUNDA olduğundan emin olun\n\nKurulumdan sonra uygulamayı yeniden başlatın", "webview_not_found_description": "Cihazınızda herhangi bir Webview çalışma zamanı yüklü değil.\nEğer kuruluysa, ortam YOLUNDA olduğundan emin olun\n\nKurulumdan sonra uygulamayı yeniden başlatın",
"unsupported_platform": "Desteklenmeyen platform" "unsupported_platform": "Desteklenmeyen platform",
"invidious_instance": "Invidious Sunucu Örneği",
"invidious_description": "Parça eşleştirmesi için kullanılacak Invidious sunucu örneği",
"invidious_warning": "Bazıları iyi çalışmayabilir. Kendi riskinizde kullanın",
"invidious_source_description": "Piped'a benzer, ancak daha yüksek kullanılabilirliğe sahip.",
"cache_music": "Müziği önbellekle",
"open": "Aç",
"cache_folder": "Önbellek klasörü",
"export": "Dışa aktar",
"clear_cache": "Önbelleği temizle",
"clear_cache_confirmation": "Önbelleği temizlemek istiyor musunuz?",
"export_cache_files": "Önbelleğe Alınmış Dosyaları Dışa Aktar",
"found_n_files": "{count} dosya bulundu",
"export_cache_confirmation": "Bu dosyaları dışa aktarmak istiyor musunuz",
"exported_n_out_of_m_files": "{filesExported} / {files} dosya dışa aktarıldı"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify.", "spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify.",
"webview_not_found": "Webview не знайдено", "webview_not_found": "Webview не знайдено",
"webview_not_found_description": "На вашому пристрої не встановлено виконуване середовище Webview.\nЯкщо воно встановлено, переконайтеся, що воно знаходиться в environment PATH\n\nПісля встановлення перезапустіть програму", "webview_not_found_description": "На вашому пристрої не встановлено виконуване середовище Webview.\nЯкщо воно встановлено, переконайтеся, що воно знаходиться в environment PATH\n\nПісля встановлення перезапустіть програму",
"unsupported_platform": "Непідтримувана платформа" "unsupported_platform": "Непідтримувана платформа",
"invidious_instance": "Екземпляр сервера Invidious",
"invidious_description": "Екземпляр сервера Invidious для зіставлення треків",
"invidious_warning": "Деякі можуть працювати не дуже добре. Використовуйте на власний ризик",
"invidious_source_description": "Подібний до Piped, але з вищою доступністю.",
"cache_music": "Кешувати музику",
"open": "Відкрити",
"cache_folder": "Тека кешу",
"export": "Експорт",
"clear_cache": "Очистити кеш",
"clear_cache_confirmation": "Ви хочете очистити кеш?",
"export_cache_files": "Експортувати кешовані файли",
"found_n_files": "Знайдено {count} файлів",
"export_cache_confirmation": "Ви хочете експортувати ці файли до",
"exported_n_out_of_m_files": "Експортовано {filesExported} з {files} файлів"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify.", "spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify.",
"webview_not_found": "Không tìm thấy Webview", "webview_not_found": "Không tìm thấy Webview",
"webview_not_found_description": "Không có runtime Webview nào được cài đặt trên thiết bị của bạn.\nNếu đã cài đặt, hãy đảm bảo rằng nó nằm trong environment PATH\n\nSau khi cài đặt, hãy khởi động lại ứng dụng", "webview_not_found_description": "Không có runtime Webview nào được cài đặt trên thiết bị của bạn.\nNếu đã cài đặt, hãy đảm bảo rằng nó nằm trong environment PATH\n\nSau khi cài đặt, hãy khởi động lại ứng dụng",
"unsupported_platform": "Nền tảng không được hỗ trợ" "unsupported_platform": "Nền tảng không được hỗ trợ",
"invidious_instance": "Phiên bản máy chủ Invidious",
"invidious_description": "Phiên bản máy chủ Invidious để sử dụng để so khớp bản nhạc",
"invidious_warning": "Một số có thể sẽ không hoạt động tốt. Vì vậy hãy sử dụng với rủi ro của riêng bạn",
"invidious_source_description": "Tương tự như Piped nhưng có tính khả dụng cao hơn.",
"cache_music": "Lưu nhạc vào bộ nhớ đệm",
"open": "Mở",
"cache_folder": "Thư mục bộ nhớ đệm",
"export": "Xuất",
"clear_cache": "Xóa bộ nhớ đệm",
"clear_cache_confirmation": "Bạn có muốn xóa bộ nhớ đệm không?",
"export_cache_files": "Xuất các tệp được lưu trong bộ nhớ đệm",
"found_n_files": "Tìm thấy {count} tệp",
"export_cache_confirmation": "Bạn có muốn xuất các tệp này đến",
"exported_n_out_of_m_files": "Đã xuất {filesExported} trên {files} tệp"
} }

View File

@ -387,5 +387,19 @@
"spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。", "spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。",
"webview_not_found": "未找到 Webview", "webview_not_found": "未找到 Webview",
"webview_not_found_description": "您的设备中未安装 Webview 运行时。\n如果已安装请确保它在 environment PATH 中\n\n安装后重新启动应用程序", "webview_not_found_description": "您的设备中未安装 Webview 运行时。\n如果已安装请确保它在 environment PATH 中\n\n安装后重新启动应用程序",
"unsupported_platform": "不支持的平台" "unsupported_platform": "不支持的平台",
"invidious_instance": "Invidious服务器实例",
"invidious_description": "用于音轨匹配的Invidious服务器实例",
"invidious_warning": "有些可能无法正常工作。请自行承担风险",
"invidious_source_description": "类似于Piped但可用性更高。",
"cache_music": "缓存音乐",
"open": "打开",
"cache_folder": "缓存文件夹",
"export": "导出",
"clear_cache": "清除缓存",
"clear_cache_confirmation": "您要清除缓存吗?",
"export_cache_files": "导出缓存文件",
"found_n_files": "找到 {count} 个文件",
"export_cache_confirmation": "您要导出这些文件到",
"exported_n_out_of_m_files": "导出了 {filesExported} / {files} 个文件"
} }

View File

@ -99,8 +99,13 @@ mixin _$WebSocketLoadEventData {
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this WebSocketLoadEventData to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> get copyWith => $WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -127,6 +132,8 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -171,6 +178,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
$Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then) $Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -247,12 +256,14 @@ class _$WebSocketLoadEventDataPlaylistImpl
other.initialIndex == initialIndex)); other.initialIndex == initialIndex));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_tracks), collection, initialIndex); const DeepCollectionEquality().hash(_tracks), collection, initialIndex);
@JsonKey(ignore: true) /// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$WebSocketLoadEventDataPlaylistImplCopyWith< _$$WebSocketLoadEventDataPlaylistImplCopyWith<
@ -372,8 +383,11 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
PlaylistSimple? get collection; PlaylistSimple? get collection;
@override @override
int? get initialIndex; int? get initialIndex;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$WebSocketLoadEventDataPlaylistImplCopyWith< _$$WebSocketLoadEventDataPlaylistImplCopyWith<
_$WebSocketLoadEventDataPlaylistImpl> _$WebSocketLoadEventDataPlaylistImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
@ -404,6 +418,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
$Res Function(_$WebSocketLoadEventDataAlbumImpl) _then) $Res Function(_$WebSocketLoadEventDataAlbumImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -479,12 +495,14 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
other.initialIndex == initialIndex)); other.initialIndex == initialIndex));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_tracks), collection, initialIndex); const DeepCollectionEquality().hash(_tracks), collection, initialIndex);
@JsonKey(ignore: true) /// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
@ -603,8 +621,11 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
AlbumSimple? get collection; AlbumSimple? get collection;
@override @override
int? get initialIndex; int? get initialIndex;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }

View File

@ -16,7 +16,7 @@ _$WebSocketLoadEventDataPlaylistImpl
? null ? null
: PlaylistSimple.fromJson( : PlaylistSimple.fromJson(
Map<String, dynamic>.from(json['collection'] as Map)), Map<String, dynamic>.from(json['collection'] as Map)),
initialIndex: json['initialIndex'] as int?, initialIndex: (json['initialIndex'] as num?)?.toInt(),
$type: json['runtimeType'] as String?, $type: json['runtimeType'] as String?,
); );
@ -39,7 +39,7 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
? null ? null
: AlbumSimple.fromJson( : AlbumSimple.fromJson(
Map<String, dynamic>.from(json['collection'] as Map)), Map<String, dynamic>.from(json['collection'] as Map)),
initialIndex: json['initialIndex'] as int?, initialIndex: (json['initialIndex'] as num?)?.toInt(),
$type: json['runtimeType'] as String?, $type: json['runtimeType'] as String?,
); );

View File

@ -9,6 +9,7 @@ import 'package:media_kit/media_kit.dart' hide Track;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/database/database.steps.dart';
import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/lyrics.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
@ -57,7 +58,28 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 1; int get schemaVersion => 3;
@override
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: stepByStep(
from1To2: (m, schema) async {
// Add invidiousInstance column to preferences table
await m.addColumn(
schema.preferencesTable,
schema.preferencesTable.invidiousInstance,
);
},
from2To3: (m, schema) async {
await m.addColumn(
schema.preferencesTable,
schema.preferencesTable.cacheMusic,
);
},
),
);
}
} }
LazyDatabase _openConnection() { LazyDatabase _openConnection() {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,940 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/migrations/adapters.dart';
// GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema {
Schema2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
playlistTable,
playlistMediaTable,
historyTable,
lyricsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 preferencesTable = Shape2(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_25,
_column_26,
_column_27,
_column_28,
_column_29,
_column_30,
_column_31,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 sourceMatchTable = Shape5(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_38,
_column_39,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 audioPlayerStateTable = Shape6(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 playlistTable = Shape7(
source: i0.VersionedTable(
entityName: 'playlist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_44,
_column_45,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 playlistMediaTable = Shape8(
source: i0.VersionedTable(
entityName: 'playlist_media_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_46,
_column_47,
_column_48,
_column_49,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
}
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get cookie =>
columnsByName['cookie']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get accessToken =>
columnsByName['access_token']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get expiration =>
columnsByName['expiration']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
i1.GeneratedColumn<int>('id', aliasedName, false,
hasAutoIncrement: true,
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('cookie', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
i1.GeneratedColumn<String>('access_token', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_3(String aliasedName) =>
i1.GeneratedColumn<DateTime>('expiration', aliasedName, false,
type: i1.DriftSqlType.dateTime);
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get elementType =>
columnsByName['element_type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get elementId =>
columnsByName['element_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_5(String aliasedName) =>
i1.GeneratedColumn<String>('element_type', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
i1.GeneratedColumn<String>('element_id', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get audioQuality =>
columnsByName['audio_quality']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get albumColorSync =>
columnsByName['album_color_sync']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get amoledDarkTheme =>
columnsByName['amoled_dark_theme']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get checkUpdate =>
columnsByName['check_update']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get normalizeAudio =>
columnsByName['normalize_audio']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get showSystemTrayIcon =>
columnsByName['show_system_tray_icon']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get systemTitleBar =>
columnsByName['system_title_bar']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get skipNonMusic =>
columnsByName['skip_non_music']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get closeBehavior =>
columnsByName['close_behavior']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get accentColorScheme =>
columnsByName['accent_color_scheme']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get layoutMode =>
columnsByName['layout_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get locale =>
columnsByName['locale']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get market =>
columnsByName['market']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get searchMode =>
columnsByName['search_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadLocation =>
columnsByName['download_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get localLibraryLocation =>
columnsByName['local_library_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get pipedInstance =>
columnsByName['piped_instance']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get invidiousInstance =>
columnsByName['invidious_instance']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get themeMode =>
columnsByName['theme_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get audioSource =>
columnsByName['audio_source']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get streamMusicCodec =>
columnsByName['stream_music_codec']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadMusicCodec =>
columnsByName['download_music_codec']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get discordPresence =>
columnsByName['discord_presence']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get endlessPlayback =>
columnsByName['endless_playback']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get enableConnect =>
columnsByName['enable_connect']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
i1.GeneratedColumn<String>('audio_quality', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(SourceQualities.high.name));
i1.GeneratedColumn<bool> _column_8(String aliasedName) =>
i1.GeneratedColumn<bool>('album_color_sync', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("album_color_sync" IN (0, 1))'),
defaultValue: const Constant(true));
i1.GeneratedColumn<bool> _column_9(String aliasedName) =>
i1.GeneratedColumn<bool>('amoled_dark_theme', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("amoled_dark_theme" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<bool> _column_10(String aliasedName) =>
i1.GeneratedColumn<bool>('check_update', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("check_update" IN (0, 1))'),
defaultValue: const Constant(true));
i1.GeneratedColumn<bool> _column_11(String aliasedName) =>
i1.GeneratedColumn<bool>('normalize_audio', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("normalize_audio" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<bool> _column_12(String aliasedName) =>
i1.GeneratedColumn<bool>('show_system_tray_icon', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("show_system_tray_icon" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<bool> _column_13(String aliasedName) =>
i1.GeneratedColumn<bool>('system_title_bar', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("system_title_bar" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<bool> _column_14(String aliasedName) =>
i1.GeneratedColumn<bool>('skip_non_music', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("skip_non_music" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
i1.GeneratedColumn<String>('close_behavior', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(CloseBehavior.close.name));
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const Constant("Blue:0xFF2196F3"));
i1.GeneratedColumn<String> _column_17(String aliasedName) =>
i1.GeneratedColumn<String>('layout_mode', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(LayoutMode.adaptive.name));
i1.GeneratedColumn<String> _column_18(String aliasedName) =>
i1.GeneratedColumn<String>('locale', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue:
const Constant('{"languageCode":"system","countryCode":"system"}'));
i1.GeneratedColumn<String> _column_19(String aliasedName) =>
i1.GeneratedColumn<String>('market', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: Constant(Market.US.name));
i1.GeneratedColumn<String> _column_20(String aliasedName) =>
i1.GeneratedColumn<String>('search_mode', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(SearchMode.youtube.name));
i1.GeneratedColumn<String> _column_21(String aliasedName) =>
i1.GeneratedColumn<String>('download_location', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant(""));
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
i1.GeneratedColumn<String>('local_library_location', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant(""));
i1.GeneratedColumn<String> _column_23(String aliasedName) =>
i1.GeneratedColumn<String>('piped_instance', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const Constant("https://pipedapi.kavin.rocks"));
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
i1.GeneratedColumn<String>('invidious_instance', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const Constant("https://inv.nadeko.net"));
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
i1.GeneratedColumn<String>('theme_mode', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(ThemeMode.system.name));
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
i1.GeneratedColumn<String>('audio_source', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(AudioSource.youtube.name));
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
i1.GeneratedColumn<String>('stream_music_codec', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(SourceCodecs.weba.name));
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
i1.GeneratedColumn<String>('download_music_codec', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(SourceCodecs.m4a.name));
i1.GeneratedColumn<bool> _column_29(String aliasedName) =>
i1.GeneratedColumn<bool>('discord_presence', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("discord_presence" IN (0, 1))'),
defaultValue: const Constant(true));
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
i1.GeneratedColumn<bool>('endless_playback', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("endless_playback" IN (0, 1))'),
defaultValue: const Constant(true));
i1.GeneratedColumn<bool> _column_31(String aliasedName) =>
i1.GeneratedColumn<bool>('enable_connect', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("enable_connect" IN (0, 1))'),
defaultValue: const Constant(false));
class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get username =>
columnsByName['username']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get passwordHash =>
columnsByName['password_hash']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<DateTime> _column_32(String aliasedName) =>
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i1.DriftSqlType.dateTime, defaultValue: currentDateAndTime);
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
i1.GeneratedColumn<String>('username', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
i1.GeneratedColumn<String>('password_hash', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get start =>
columnsByName['start']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get end =>
columnsByName['end']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get trackId =>
columnsByName['track_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_35(String aliasedName) =>
i1.GeneratedColumn<int>('start', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_36(String aliasedName) =>
i1.GeneratedColumn<int>('end', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
i1.GeneratedColumn<String>('track_id', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape5 extends i0.VersionedTable {
Shape5({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get trackId =>
columnsByName['track_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sourceId =>
columnsByName['source_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sourceType =>
columnsByName['source_type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
i1.GeneratedColumn<String>('source_id', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
i1.GeneratedColumn<String>('source_type', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(SourceType.youtube.name));
class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get playing =>
columnsByName['playing']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get loopMode =>
columnsByName['loop_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get shuffled =>
columnsByName['shuffled']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get collections =>
columnsByName['collections']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<bool> _column_40(String aliasedName) =>
i1.GeneratedColumn<bool>('playing', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("playing" IN (0, 1))'));
i1.GeneratedColumn<String> _column_41(String aliasedName) =>
i1.GeneratedColumn<String>('loop_mode', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_42(String aliasedName) =>
i1.GeneratedColumn<bool>('shuffled', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("shuffled" IN (0, 1))'));
i1.GeneratedColumn<String> _column_43(String aliasedName) =>
i1.GeneratedColumn<String>('collections', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape7 extends i0.VersionedTable {
Shape7({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get audioPlayerStateId =>
columnsByName['audio_player_state_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get index =>
columnsByName['index']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_44(String aliasedName) =>
i1.GeneratedColumn<int>('audio_player_state_id', aliasedName, false,
type: i1.DriftSqlType.int,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES audio_player_state_table (id)'));
i1.GeneratedColumn<int> _column_45(String aliasedName) =>
i1.GeneratedColumn<int>('index', aliasedName, false,
type: i1.DriftSqlType.int);
class Shape8 extends i0.VersionedTable {
Shape8({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get playlistId =>
columnsByName['playlist_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get uri =>
columnsByName['uri']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get extras =>
columnsByName['extras']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get httpHeaders =>
columnsByName['http_headers']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_46(String aliasedName) =>
i1.GeneratedColumn<int>('playlist_id', aliasedName, false,
type: i1.DriftSqlType.int,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES playlist_table (id)'));
i1.GeneratedColumn<String> _column_47(String aliasedName) =>
i1.GeneratedColumn<String>('uri', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_48(String aliasedName) =>
i1.GeneratedColumn<String>('extras', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_49(String aliasedName) =>
i1.GeneratedColumn<String>('http_headers', aliasedName, true,
type: i1.DriftSqlType.string);
class Shape9 extends i0.VersionedTable {
Shape9({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get itemId =>
columnsByName['item_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get data =>
columnsByName['data']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_50(String aliasedName) =>
i1.GeneratedColumn<String>('type', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_51(String aliasedName) =>
i1.GeneratedColumn<String>('item_id', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_52(String aliasedName) =>
i1.GeneratedColumn<String>('data', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape10 extends i0.VersionedTable {
Shape10({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get trackId =>
columnsByName['track_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get data =>
columnsByName['data']! as i1.GeneratedColumn<String>;
}
final class Schema3 extends i0.VersionedSchema {
Schema3({required super.database}) : super(version: 3);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
playlistTable,
playlistMediaTable,
historyTable,
lyricsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 preferencesTable = Shape11(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_25,
_column_26,
_column_27,
_column_28,
_column_29,
_column_30,
_column_31,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 sourceMatchTable = Shape5(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_38,
_column_39,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 audioPlayerStateTable = Shape6(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 playlistTable = Shape7(
source: i0.VersionedTable(
entityName: 'playlist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_44,
_column_45,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 playlistMediaTable = Shape8(
source: i0.VersionedTable(
entityName: 'playlist_media_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_46,
_column_47,
_column_48,
_column_49,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
}
class Shape11 extends i0.VersionedTable {
Shape11({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get audioQuality =>
columnsByName['audio_quality']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get albumColorSync =>
columnsByName['album_color_sync']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get amoledDarkTheme =>
columnsByName['amoled_dark_theme']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get checkUpdate =>
columnsByName['check_update']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get normalizeAudio =>
columnsByName['normalize_audio']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get showSystemTrayIcon =>
columnsByName['show_system_tray_icon']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get systemTitleBar =>
columnsByName['system_title_bar']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get skipNonMusic =>
columnsByName['skip_non_music']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get closeBehavior =>
columnsByName['close_behavior']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get accentColorScheme =>
columnsByName['accent_color_scheme']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get layoutMode =>
columnsByName['layout_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get locale =>
columnsByName['locale']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get market =>
columnsByName['market']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get searchMode =>
columnsByName['search_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadLocation =>
columnsByName['download_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get localLibraryLocation =>
columnsByName['local_library_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get pipedInstance =>
columnsByName['piped_instance']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get invidiousInstance =>
columnsByName['invidious_instance']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get themeMode =>
columnsByName['theme_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get audioSource =>
columnsByName['audio_source']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get streamMusicCodec =>
columnsByName['stream_music_codec']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadMusicCodec =>
columnsByName['download_music_codec']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get discordPresence =>
columnsByName['discord_presence']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get endlessPlayback =>
columnsByName['endless_playback']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get enableConnect =>
columnsByName['enable_connect']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get cacheMusic =>
columnsByName['cache_music']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<bool> _column_53(String aliasedName) =>
i1.GeneratedColumn<bool>('cache_music', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("cache_music" IN (0, 1))'),
defaultValue: const Constant(true));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
case 1:
final schema = Schema2(database: database);
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
case 2:
final schema = Schema3(database: database);
final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema);
return 3;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
};
}
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
));

View File

@ -14,7 +14,8 @@ enum CloseBehavior {
enum AudioSource { enum AudioSource {
youtube, youtube,
piped, piped,
jiosaavn; jiosaavn,
invidious;
String get label => name[0].toUpperCase() + name.substring(1); String get label => name[0].toUpperCase() + name.substring(1);
} }
@ -77,6 +78,8 @@ class PreferencesTable extends Table {
text().withDefault(const Constant("")).map(const StringListConverter())(); text().withDefault(const Constant("")).map(const StringListConverter())();
TextColumn get pipedInstance => TextColumn get pipedInstance =>
text().withDefault(const Constant("https://pipedapi.kavin.rocks"))(); text().withDefault(const Constant("https://pipedapi.kavin.rocks"))();
TextColumn get invidiousInstance =>
text().withDefault(const Constant("https://inv.nadeko.net"))();
TextColumn get themeMode => TextColumn get themeMode =>
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))(); textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
TextColumn get audioSource => TextColumn get audioSource =>
@ -91,6 +94,7 @@ class PreferencesTable extends Table {
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
BoolColumn get enableConnect => BoolColumn get enableConnect =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
BoolColumn get cacheMusic => boolean().withDefault(const Constant(true))();
// Default values as PreferencesTableData // Default values as PreferencesTableData
static PreferencesTableData defaults() { static PreferencesTableData defaults() {
@ -113,13 +117,15 @@ class PreferencesTable extends Table {
downloadLocation: "", downloadLocation: "",
localLibraryLocation: [], localLibraryLocation: [],
pipedInstance: "https://pipedapi.kavin.rocks", pipedInstance: "https://pipedapi.kavin.rocks",
invidiousInstance: "https://inv.nadeko.net",
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
audioSource: AudioSource.youtube, audioSource: AudioSource.youtube,
streamMusicCodec: SourceCodecs.weba, streamMusicCodec: SourceCodecs.m4a,
downloadMusicCodec: SourceCodecs.m4a, downloadMusicCodec: SourceCodecs.m4a,
discordPresence: true, discordPresence: true,
endlessPlayback: true, endlessPlayback: true,
enableConnect: false, enableConnect: false,
cacheMusic: true,
); );
} }
} }

View File

@ -0,0 +1,71 @@
class ContentRangeHeader {
final int start;
final int end;
final int total;
ContentRangeHeader(this.start, this.end, this.total);
factory ContentRangeHeader.parse(String value) {
if (value.isEmpty) {
throw FormatException('Invalid Content-Range header: $value');
}
final parts = value.split(' ');
if (parts.length != 2) {
throw FormatException('Invalid Content-Range header: $value');
}
final rangeParts = parts[1].split('/');
if (rangeParts.length != 2) {
throw FormatException('Invalid Content-Range header: $value');
}
final range = rangeParts[0].split('-');
if (range.length != 2) {
throw FormatException('Invalid Content-Range header: $value');
}
return ContentRangeHeader(
int.parse(range[0]),
int.parse(range[1]),
int.parse(rangeParts[1]),
);
}
@override
String toString() {
return 'bytes $start-$end/$total';
}
}
class RangeHeader {
final int start;
final int? end;
RangeHeader(this.start, this.end);
factory RangeHeader.parse(String value) {
if (value.isEmpty) {
return RangeHeader(0, null);
}
final parts = value.split('=');
if (parts.length != 2) {
throw FormatException('Invalid Range header: $value');
}
final ranges = parts[1].split('-');
return RangeHeader(
int.parse(ranges[0]),
ranges.elementAtOrNull(1) != null && ranges[1].isNotEmpty
? int.parse(ranges[1])
: null,
);
}
@override
String toString() {
return 'bytes=$start-${end ?? ""}';
}
}

View File

@ -29,8 +29,12 @@ mixin _$SpotifySectionPlaylist {
String get owner => throw _privateConstructorUsedError; String get owner => throw _privateConstructorUsedError;
String get uri => throw _privateConstructorUsedError; String get uri => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionPlaylist to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifySectionPlaylistCopyWith<SpotifySectionPlaylist> get copyWith => $SpotifySectionPlaylistCopyWith<SpotifySectionPlaylist> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -61,6 +65,8 @@ class _$SpotifySectionPlaylistCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -128,6 +134,8 @@ class __$$SpotifySectionPlaylistImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionPlaylistImpl) _then) $Res Function(_$SpotifySectionPlaylistImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -221,12 +229,14 @@ class _$SpotifySectionPlaylistImpl extends _SpotifySectionPlaylist {
(identical(other.uri, uri) || other.uri == uri)); (identical(other.uri, uri) || other.uri == uri));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, description, format, int get hashCode => Object.hash(runtimeType, description, format,
const DeepCollectionEquality().hash(_images), name, owner, uri); const DeepCollectionEquality().hash(_images), name, owner, uri);
@JsonKey(ignore: true) /// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl> _$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl>
@ -266,8 +276,11 @@ abstract class _SpotifySectionPlaylist extends SpotifySectionPlaylist {
String get owner; String get owner;
@override @override
String get uri; String get uri;
/// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl> _$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -283,8 +296,12 @@ mixin _$SpotifySectionArtist {
List<SpotifySectionItemImage> get images => List<SpotifySectionItemImage> get images =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this SpotifySectionArtist to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifySectionArtistCopyWith<SpotifySectionArtist> get copyWith => $SpotifySectionArtistCopyWith<SpotifySectionArtist> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -309,6 +326,8 @@ class _$SpotifySectionArtistCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -352,6 +371,8 @@ class __$$SpotifySectionArtistImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionArtistImpl) _then) $Res Function(_$SpotifySectionArtistImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -416,12 +437,14 @@ class _$SpotifySectionArtistImpl extends _SpotifySectionArtist {
const DeepCollectionEquality().equals(other._images, _images)); const DeepCollectionEquality().equals(other._images, _images));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, name, uri, const DeepCollectionEquality().hash(_images)); runtimeType, name, uri, const DeepCollectionEquality().hash(_images));
@JsonKey(ignore: true) /// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl> _$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl>
@ -454,8 +477,11 @@ abstract class _SpotifySectionArtist extends SpotifySectionArtist {
String get uri; String get uri;
@override @override
List<SpotifySectionItemImage> get images; List<SpotifySectionItemImage> get images;
/// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl> _$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -473,8 +499,12 @@ mixin _$SpotifySectionAlbum {
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
String get uri => throw _privateConstructorUsedError; String get uri => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionAlbum to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifySectionAlbumCopyWith<SpotifySectionAlbum> get copyWith => $SpotifySectionAlbumCopyWith<SpotifySectionAlbum> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -502,6 +532,8 @@ class _$SpotifySectionAlbumCopyWithImpl<$Res, $Val extends SpotifySectionAlbum>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -554,6 +586,8 @@ class __$$SpotifySectionAlbumImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionAlbumImpl) _then) $Res Function(_$SpotifySectionAlbumImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -635,7 +669,7 @@ class _$SpotifySectionAlbumImpl extends _SpotifySectionAlbum {
(identical(other.uri, uri) || other.uri == uri)); (identical(other.uri, uri) || other.uri == uri));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
@ -644,7 +678,9 @@ class _$SpotifySectionAlbumImpl extends _SpotifySectionAlbum {
name, name,
uri); uri);
@JsonKey(ignore: true) /// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith => _$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith =>
@ -678,8 +714,11 @@ abstract class _SpotifySectionAlbum extends SpotifySectionAlbum {
String get name; String get name;
@override @override
String get uri; String get uri;
/// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith => _$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -694,8 +733,12 @@ mixin _$SpotifySectionAlbumArtist {
String get name => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError;
String get uri => throw _privateConstructorUsedError; String get uri => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionAlbumArtist to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifySectionAlbumArtistCopyWith<SpotifySectionAlbumArtist> get copyWith => $SpotifySectionAlbumArtistCopyWith<SpotifySectionAlbumArtist> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -720,6 +763,8 @@ class _$SpotifySectionAlbumArtistCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -761,6 +806,8 @@ class __$$SpotifySectionAlbumArtistImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionAlbumArtistImpl) _then) $Res Function(_$SpotifySectionAlbumArtistImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -808,11 +855,13 @@ class _$SpotifySectionAlbumArtistImpl extends _SpotifySectionAlbumArtist {
(identical(other.uri, uri) || other.uri == uri)); (identical(other.uri, uri) || other.uri == uri));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, name, uri); int get hashCode => Object.hash(runtimeType, name, uri);
@JsonKey(ignore: true) /// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl> _$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl>
@ -840,8 +889,11 @@ abstract class _SpotifySectionAlbumArtist extends SpotifySectionAlbumArtist {
String get name; String get name;
@override @override
String get uri; String get uri;
/// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl> _$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -857,8 +909,12 @@ mixin _$SpotifySectionItemImage {
String get url => throw _privateConstructorUsedError; String get url => throw _privateConstructorUsedError;
num? get width => throw _privateConstructorUsedError; num? get width => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionItemImage to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifySectionItemImageCopyWith<SpotifySectionItemImage> get copyWith => $SpotifySectionItemImageCopyWith<SpotifySectionItemImage> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -883,6 +939,8 @@ class _$SpotifySectionItemImageCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -929,6 +987,8 @@ class __$$SpotifySectionItemImageImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionItemImageImpl) _then) $Res Function(_$SpotifySectionItemImageImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -985,11 +1045,13 @@ class _$SpotifySectionItemImageImpl extends _SpotifySectionItemImage {
(identical(other.width, width) || other.width == width)); (identical(other.width, width) || other.width == width));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, height, url, width); int get hashCode => Object.hash(runtimeType, height, url, width);
@JsonKey(ignore: true) /// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl> _$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl>
@ -1020,8 +1082,11 @@ abstract class _SpotifySectionItemImage extends SpotifySectionItemImage {
String get url; String get url;
@override @override
num? get width; num? get width;
/// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl> _$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1038,8 +1103,12 @@ mixin _$SpotifyHomeFeedSectionItem {
SpotifySectionArtist? get artist => throw _privateConstructorUsedError; SpotifySectionArtist? get artist => throw _privateConstructorUsedError;
SpotifySectionAlbum? get album => throw _privateConstructorUsedError; SpotifySectionAlbum? get album => throw _privateConstructorUsedError;
/// Serializes this SpotifyHomeFeedSectionItem to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifyHomeFeedSectionItemCopyWith<SpotifyHomeFeedSectionItem> $SpotifyHomeFeedSectionItemCopyWith<SpotifyHomeFeedSectionItem>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1073,6 +1142,8 @@ class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1101,6 +1172,8 @@ class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res,
) as $Val); ) as $Val);
} }
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SpotifySectionPlaylistCopyWith<$Res>? get playlist { $SpotifySectionPlaylistCopyWith<$Res>? get playlist {
@ -1113,6 +1186,8 @@ class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res,
}); });
} }
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SpotifySectionArtistCopyWith<$Res>? get artist { $SpotifySectionArtistCopyWith<$Res>? get artist {
@ -1125,6 +1200,8 @@ class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res,
}); });
} }
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SpotifySectionAlbumCopyWith<$Res>? get album { $SpotifySectionAlbumCopyWith<$Res>? get album {
@ -1171,6 +1248,8 @@ class __$$SpotifyHomeFeedSectionItemImplCopyWithImpl<$Res>
$Res Function(_$SpotifyHomeFeedSectionItemImpl) _then) $Res Function(_$SpotifyHomeFeedSectionItemImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1237,12 +1316,14 @@ class _$SpotifyHomeFeedSectionItemImpl implements _SpotifyHomeFeedSectionItem {
(identical(other.album, album) || other.album == album)); (identical(other.album, album) || other.album == album));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, typename, playlist, artist, album); Object.hash(runtimeType, typename, playlist, artist, album);
@JsonKey(ignore: true) /// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl> _$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl>
@ -1276,8 +1357,11 @@ abstract class _SpotifyHomeFeedSectionItem
SpotifySectionArtist? get artist; SpotifySectionArtist? get artist;
@override @override
SpotifySectionAlbum? get album; SpotifySectionAlbum? get album;
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl> _$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1295,8 +1379,12 @@ mixin _$SpotifyHomeFeedSection {
List<SpotifyHomeFeedSectionItem> get items => List<SpotifyHomeFeedSectionItem> get items =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this SpotifyHomeFeedSection to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifyHomeFeedSectionCopyWith<SpotifyHomeFeedSection> get copyWith => $SpotifyHomeFeedSectionCopyWith<SpotifyHomeFeedSection> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -1325,6 +1413,8 @@ class _$SpotifyHomeFeedSectionCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1380,6 +1470,8 @@ class __$$SpotifyHomeFeedSectionImplCopyWithImpl<$Res>
$Res Function(_$SpotifyHomeFeedSectionImpl) _then) $Res Function(_$SpotifyHomeFeedSectionImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1453,12 +1545,14 @@ class _$SpotifyHomeFeedSectionImpl implements _SpotifyHomeFeedSection {
const DeepCollectionEquality().equals(other._items, _items)); const DeepCollectionEquality().equals(other._items, _items));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, typename, title, uri, int get hashCode => Object.hash(runtimeType, typename, title, uri,
const DeepCollectionEquality().hash(_items)); const DeepCollectionEquality().hash(_items));
@JsonKey(ignore: true) /// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl> _$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl>
@ -1492,8 +1586,11 @@ abstract class _SpotifyHomeFeedSection implements SpotifyHomeFeedSection {
String get uri; String get uri;
@override @override
List<SpotifyHomeFeedSectionItem> get items; List<SpotifyHomeFeedSectionItem> get items;
/// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl> _$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1508,8 +1605,12 @@ mixin _$SpotifyHomeFeed {
List<SpotifyHomeFeedSection> get sections => List<SpotifyHomeFeedSection> get sections =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this SpotifyHomeFeed to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpotifyHomeFeedCopyWith<SpotifyHomeFeed> get copyWith => $SpotifyHomeFeedCopyWith<SpotifyHomeFeed> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -1533,6 +1634,8 @@ class _$SpotifyHomeFeedCopyWithImpl<$Res, $Val extends SpotifyHomeFeed>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1571,6 +1674,8 @@ class __$$SpotifyHomeFeedImplCopyWithImpl<$Res>
_$SpotifyHomeFeedImpl _value, $Res Function(_$SpotifyHomeFeedImpl) _then) _$SpotifyHomeFeedImpl _value, $Res Function(_$SpotifyHomeFeedImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1626,12 +1731,14 @@ class _$SpotifyHomeFeedImpl implements _SpotifyHomeFeed {
const DeepCollectionEquality().equals(other._sections, _sections)); const DeepCollectionEquality().equals(other._sections, _sections));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, greeting, const DeepCollectionEquality().hash(_sections)); runtimeType, greeting, const DeepCollectionEquality().hash(_sections));
@JsonKey(ignore: true) /// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith => _$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith =>
@ -1659,8 +1766,11 @@ abstract class _SpotifyHomeFeed implements SpotifyHomeFeed {
String get greeting; String get greeting;
@override @override
List<SpotifyHomeFeedSection> get sections; List<SpotifyHomeFeedSection> get sections;
/// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith => _$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View File

@ -24,7 +24,9 @@ mixin _$GeneratePlaylistProviderInput {
RecommendationSeeds? get min => throw _privateConstructorUsedError; RecommendationSeeds? get min => throw _privateConstructorUsedError;
RecommendationSeeds? get target => throw _privateConstructorUsedError; RecommendationSeeds? get target => throw _privateConstructorUsedError;
@JsonKey(ignore: true) /// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$GeneratePlaylistProviderInputCopyWith<GeneratePlaylistProviderInput> $GeneratePlaylistProviderInputCopyWith<GeneratePlaylistProviderInput>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -62,6 +64,8 @@ class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -105,6 +109,8 @@ class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
) as $Val); ) as $Val);
} }
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get max { $RecommendationSeedsCopyWith<$Res>? get max {
@ -117,6 +123,8 @@ class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
}); });
} }
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get min { $RecommendationSeedsCopyWith<$Res>? get min {
@ -129,6 +137,8 @@ class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
}); });
} }
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get target { $RecommendationSeedsCopyWith<$Res>? get target {
@ -178,6 +188,8 @@ class __$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>
$Res Function(_$GeneratePlaylistProviderInputImpl) _then) $Res Function(_$GeneratePlaylistProviderInputImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -283,7 +295,9 @@ class _$GeneratePlaylistProviderInputImpl
min, min,
target); target);
@JsonKey(ignore: true) /// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$GeneratePlaylistProviderInputImplCopyWith< _$$GeneratePlaylistProviderInputImplCopyWith<
@ -317,8 +331,11 @@ abstract class _GeneratePlaylistProviderInput
RecommendationSeeds? get min; RecommendationSeeds? get min;
@override @override
RecommendationSeeds? get target; RecommendationSeeds? get target;
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$GeneratePlaylistProviderInputImplCopyWith< _$$GeneratePlaylistProviderInputImplCopyWith<
_$GeneratePlaylistProviderInputImpl> _$GeneratePlaylistProviderInputImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
@ -347,8 +364,12 @@ mixin _$RecommendationSeeds {
num? get timeSignature => throw _privateConstructorUsedError; num? get timeSignature => throw _privateConstructorUsedError;
num? get valence => throw _privateConstructorUsedError; num? get valence => throw _privateConstructorUsedError;
/// Serializes this RecommendationSeeds to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$RecommendationSeedsCopyWith<RecommendationSeeds> get copyWith => $RecommendationSeedsCopyWith<RecommendationSeeds> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -386,6 +407,8 @@ class _$RecommendationSeedsCopyWithImpl<$Res, $Val extends RecommendationSeeds>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -498,6 +521,8 @@ class __$$RecommendationSeedsImplCopyWithImpl<$Res>
$Res Function(_$RecommendationSeedsImpl) _then) $Res Function(_$RecommendationSeedsImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -665,7 +690,7 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
(identical(other.valence, valence) || other.valence == valence)); (identical(other.valence, valence) || other.valence == valence));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
@ -684,7 +709,9 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
timeSignature, timeSignature,
valence); valence);
@JsonKey(ignore: true) /// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith => _$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
@ -749,8 +776,11 @@ abstract class _RecommendationSeeds implements RecommendationSeeds {
num? get timeSignature; num? get timeSignature;
@override @override
num? get valence; num? get valence;
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith => _$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View File

@ -0,0 +1,139 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
final codecs = SourceCodecs.values.map((s) => s.name);
class LocalFolderCacheExportDialog extends HookConsumerWidget {
final Directory exportDir;
final Directory cacheDir;
const LocalFolderCacheExportDialog({
super.key,
required this.exportDir,
required this.cacheDir,
});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final files = useState<List<File>>([]);
final filesExported = useState<int>(0);
useEffect(() {
final stream = cacheDir.list().where(
(event) =>
event is File &&
codecs.contains(extension(event.path).replaceAll(".", "")),
);
stream.listen(
(event) {
files.value = [...files.value, event as File];
},
onError: (e, stack) {
AppLogger.reportError(e, stack);
},
);
return null;
}, []);
useEffect(() {
if (filesExported.value == files.value.length &&
filesExported.value > 0) {
Navigator.of(context).pop();
}
return null;
}, [filesExported.value, files.value]);
final isExportInProgress =
filesExported.value > 0 && filesExported.value != files.value.length;
return AlertDialog(
title: Text(context.l10n.export_cache_files),
content: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: filesExported.value == 0
? Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.found_n_files(files.value.length.toString()),
),
const Gap(10),
Text.rich(
TextSpan(
children: [
TextSpan(
text: context.l10n.export_cache_confirmation,
),
TextSpan(
text: "\n${exportDir.path}?",
style: textTheme.labelMedium!.copyWith(
color: colorScheme.secondary,
),
),
],
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.exported_n_out_of_m_files(
files.value.length.toString(),
filesExported.value.toString(),
),
),
const Gap(10),
LinearProgressIndicator(
value: filesExported.value / files.value.length,
),
],
),
),
actions: [
TextButton(
onPressed: isExportInProgress
? null
: () {
Navigator.of(context).pop();
},
child: Text(context.l10n.cancel),
),
TextButton(
onPressed: isExportInProgress
? null
: () async {
for (final file in files.value) {
try {
final destinationFile = File(
join(exportDir.path, basename(file.path)),
);
if (await destinationFile.exists()) {
await destinationFile.delete();
}
await file.copy(destinationFile.path);
filesExported.value++;
} catch (e, stack) {
AppLogger.reportError(e, stack);
continue;
}
}
},
child: Text(context.l10n.export),
),
],
);
}
}

View File

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -10,6 +11,7 @@ import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/pages/library/local_folder.dart'; import 'package:spotube/pages/library/local_folder.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
@ -28,8 +30,10 @@ class LocalFolderItem extends HookConsumerWidget {
final downloadFolder = final downloadFolder =
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)); ref.watch(userPreferencesProvider.select((s) => s.downloadLocation));
final cacheFolder = useFuture(UserPreferencesNotifier.getMusicCacheDir());
final isDownloadFolder = folder == downloadFolder; final isDownloadFolder = folder == downloadFolder;
final isCacheFolder = folder == cacheFolder.data;
final Uri(:pathSegments) = Uri.parse( final Uri(:pathSegments) = Uri.parse(
folder folder
@ -62,6 +66,7 @@ class LocalFolderItem extends HookConsumerWidget {
LocalLibraryPage.name, LocalLibraryPage.name,
queryParameters: { queryParameters: {
if (isDownloadFolder) "downloads": "true", if (isDownloadFolder) "downloads": "true",
if (isCacheFolder) "cache": "true",
}, },
extra: folder, extra: folder,
); );
@ -123,7 +128,9 @@ class LocalFolderItem extends HookConsumerWidget {
child: Text( child: Text(
isDownloadFolder isDownloadFolder
? context.l10n.downloads ? context.l10n.downloads
: basename(folder), : isCacheFolder
? context.l10n.cache_folder.capitalize()
: basename(folder),
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),

View File

@ -73,41 +73,41 @@ class UserAlbums extends HookConsumerWidget {
), ),
), ),
const SliverGap(10), const SliverGap(10),
Skeletonizer.sliver( SliverLayoutBuilder(builder: (context, constrains) {
enabled: albumsQuery.isLoading, return SliverGrid.builder(
child: SliverLayoutBuilder(builder: (context, constrains) { itemCount: albums.isEmpty ? 6 : albums.length + 1,
return SliverGrid.builder( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
itemCount: albums.isEmpty ? 6 : albums.length + 1, maxCrossAxisExtent: 200,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( mainAxisExtent: constrains.smAndDown ? 225 : 250,
maxCrossAxisExtent: 200, crossAxisSpacing: 8,
mainAxisExtent: constrains.smAndDown ? 225 : 250, mainAxisSpacing: 8,
crossAxisSpacing: 8, ),
mainAxisSpacing: 8, itemBuilder: (context, index) {
), if (albums.isNotEmpty && index == albums.length) {
itemBuilder: (context, index) { if (albumsQuery.asData?.value.hasMore != true) {
if (albums.isNotEmpty && index == albums.length) { return const SizedBox.shrink();
if (albumsQuery.asData?.value.hasMore != true) {
return const SizedBox.shrink();
}
return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: albumsQueryNotifier.fetchMore,
child: Skeletonizer(
enabled: true,
child: AlbumCard(FakeData.albumSimple),
),
);
} }
return AlbumCard( return Waypoint(
albums.elementAtOrNull(index) ?? FakeData.albumSimple, controller: controller,
isGrid: true,
onTouchEdge: albumsQueryNotifier.fetchMore,
child: Skeletonizer(
enabled: true,
child: AlbumCard(FakeData.albumSimple),
),
); );
}, }
);
}), return Skeletonizer(
), enabled: albumsQuery.isLoading,
child: AlbumCard(
albums.elementAtOrNull(index) ?? FakeData.albumSimple,
),
);
},
);
}),
], ],
), ),
), ),

View File

@ -74,45 +74,45 @@ class UserArtists extends HookConsumerWidget {
), ),
), ),
const SliverGap(10), const SliverGap(10),
Skeletonizer.sliver( SliverLayoutBuilder(builder: (context, constrains) {
enabled: artistQuery.isLoading, return SliverGrid.builder(
child: SliverLayoutBuilder(builder: (context, constrains) { itemCount: filteredArtists.isEmpty
return SliverGrid.builder( ? 6
itemCount: filteredArtists.isEmpty : filteredArtists.length + 1,
? 6 gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
: filteredArtists.length + 1, maxCrossAxisExtent: 200,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( mainAxisExtent: constrains.smAndDown ? 225 : 250,
maxCrossAxisExtent: 200, crossAxisSpacing: 8,
mainAxisExtent: constrains.smAndDown ? 225 : 250, mainAxisSpacing: 8,
crossAxisSpacing: 8, ),
mainAxisSpacing: 8, itemBuilder: (context, index) {
), if (filteredArtists.isNotEmpty &&
itemBuilder: (context, index) { index == filteredArtists.length) {
if (filteredArtists.isNotEmpty && if (artistQuery.asData?.value.hasMore != true) {
index == filteredArtists.length) { return const SizedBox.shrink();
if (artistQuery.asData?.value.hasMore != true) {
return const SizedBox.shrink();
}
return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: artistQueryNotifier.fetchMore,
child: Skeletonizer(
enabled: true,
child: ArtistCard(FakeData.artist),
),
);
} }
return ArtistCard( return Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: artistQueryNotifier.fetchMore,
child: Skeletonizer(
enabled: true,
child: ArtistCard(FakeData.artist),
),
);
}
return Skeletonizer(
enabled: artistQuery.isLoading,
child: ArtistCard(
filteredArtists.elementAtOrNull(index) ?? filteredArtists.elementAtOrNull(index) ??
FakeData.artist, FakeData.artist,
); ),
}, );
); },
}), );
), }),
], ],
), ),
), ),

View File

@ -30,6 +30,7 @@ class UserLocalTracks extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final cacheDir = useFuture(UserPreferencesNotifier.getMusicCacheDir());
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
@ -83,12 +84,16 @@ class UserLocalTracks extends HookConsumerWidget {
crossAxisSpacing: 10, crossAxisSpacing: 10,
mainAxisSpacing: 10, mainAxisSpacing: 10,
), ),
itemCount: preferences.localLibraryLocation.length + 1, itemCount: preferences.localLibraryLocation.length +
1 +
(cacheDir.hasData ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return LocalFolderItem( return LocalFolderItem(
folder: index == 0 folder: index == 0
? preferences.downloadLocation ? preferences.downloadLocation
: preferences.localLibraryLocation[index - 1], : index == 1 && cacheDir.hasData
? cacheDir.data!
: preferences.localLibraryLocation[index - 1],
); );
}, },
), ),

View File

@ -23,6 +23,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -42,6 +43,17 @@ final sourceInfoToIconMap = {
), ),
), ),
PipedSourceInfo: const Icon(SpotubeIcons.piped), PipedSourceInfo: const Icon(SpotubeIcons.piped),
InvidiousSourceInfo: Container(
height: 18,
width: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
image: DecorationImage(
image: Assets.invidious.provider(),
fit: BoxFit.cover,
),
),
),
}; };
class SiblingTracksSheet extends HookConsumerWidget { class SiblingTracksSheet extends HookConsumerWidget {

View File

@ -17,6 +17,10 @@ final audioSourceToIconMap = {
size: 30, size: 30,
), ),
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30), AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30),
AudioSource.invidious: ClipRRect(
borderRadius: BorderRadius.circular(48),
child: Assets.invidious.image(width: 48, height: 48),
),
AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48), AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48),
}; };
@ -45,6 +49,7 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
AudioSource.jiosaavn: AudioSource.jiosaavn:
"${context.l10n.jiosaavn_source_description}\n" "${context.l10n.jiosaavn_source_description}\n"
"${context.l10n.highest_quality("320kbps mp")}", "${context.l10n.highest_quality("320kbps mp")}",
AudioSource.invidious: context.l10n.invidious_source_description,
}, },
[]); []);
@ -104,7 +109,9 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
title: Align( title: Align(
alignment: switch (preferences.audioSource) { alignment: switch (preferences.audioSource) {
AudioSource.youtube => Alignment.centerLeft, AudioSource.youtube => Alignment.centerLeft,
AudioSource.piped => Alignment.center, AudioSource.piped ||
AudioSource.invidious =>
Alignment.center,
AudioSource.jiosaavn => Alignment.centerRight, AudioSource.jiosaavn => Alignment.centerRight,
}, },
child: Text( child: Text(

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart' hide Offset; import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -27,6 +29,14 @@ class GenrePlaylistsPage extends HookConsumerWidget {
final playlistsNotifier = final playlistsNotifier =
ref.read(categoryPlaylistsProvider(category.id!).notifier); ref.read(categoryPlaylistsProvider(category.id!).notifier);
final scrollController = useScrollController(); final scrollController = useScrollController();
final routeName = GoRouterState.of(context).name;
useCustomStatusBarColor(
Colors.black,
routeName == GenrePlaylistsPage.name,
noSetBGColor: true,
automaticSystemUiAdjustment: false,
);
return Scaffold( return Scaffold(
appBar: kIsDesktop appBar: kIsDesktop

View File

@ -1,4 +1,8 @@
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
@ -6,6 +10,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/modules/library/user_local_tracks.dart';
import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart';
import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/fallbacks/not_found.dart';
@ -18,6 +24,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/local_tracks/local_tracks_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/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class LocalLibraryPage extends HookConsumerWidget { class LocalLibraryPage extends HookConsumerWidget {
@ -25,7 +32,13 @@ class LocalLibraryPage extends HookConsumerWidget {
final String location; final String location;
final bool isDownloads; final bool isDownloads;
const LocalLibraryPage(this.location, {super.key, this.isDownloads = false}); final bool isCache;
const LocalLibraryPage(
this.location, {
super.key,
this.isDownloads = false,
this.isCache = false,
});
Future<void> playLocalTracks( Future<void> playLocalTracks(
WidgetRef ref, WidgetRef ref,
@ -52,6 +65,8 @@ class LocalLibraryPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context);
final sortBy = useState<SortBy>(SortBy.none); final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
final trackSnapshot = ref.watch(localTracksProvider); final trackSnapshot = ref.watch(localTracksProvider);
@ -65,14 +80,133 @@ class LocalLibraryPage extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
final directorySize = useMemoized(() async {
final dir = Directory(location);
final files = await dir.list(recursive: true).toList();
final filesLength =
await Future.wait(files.whereType<File>().map((e) => e.length()));
return (filesLength.sum.toInt() / pow(10, 9)).toStringAsFixed(2);
}, [location]);
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: const BackButton(), leading: const BackButton(),
centerTitle: true, centerTitle: true,
title: Text(isDownloads ? context.l10n.downloads : location), title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isDownloads
? context.l10n.downloads
: isCache
? context.l10n.cache_folder.capitalize()
: location,
style: textTheme.titleLarge,
),
FutureBuilder<String>(
future: directorySize,
builder: (context, snapshot) {
return Text(
"${(snapshot.data ?? 0)} GB",
style: textTheme.labelSmall,
);
},
)
],
),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
actions: [
if (isCache) ...[
IconButton(
iconSize: 16,
icon: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.delete),
Text(
context.l10n.clear_cache,
style: textTheme.labelSmall,
)
],
),
onPressed: () async {
final accepted = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog.adaptive(
title: Text(context.l10n.clear_cache_confirmation),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(context.l10n.decline),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
child: Text(context.l10n.accept),
),
],
),
);
if (accepted ?? false) return;
final cacheDir = Directory(
await UserPreferencesNotifier.getMusicCacheDir(),
);
if (cacheDir.existsSync()) {
await cacheDir.delete(recursive: true);
}
},
),
IconButton(
iconSize: 16,
icon: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.export),
Text(
context.l10n.export,
style: textTheme.labelSmall,
)
],
),
onPressed: () async {
final exportPath =
await FilePicker.platform.getDirectoryPath();
if (exportPath == null) return;
final exportDirectory = Directory(exportPath);
if (!exportDirectory.existsSync()) {
await exportDirectory.create(recursive: true);
}
final cacheDir = Directory(
await UserPreferencesNotifier.getMusicCacheDir());
if (!context.mounted) return;
await showDialog(
context: context,
builder: (context) {
return LocalFolderCacheExportDialog(
cacheDir: cacheDir,
exportDir: exportDirectory,
);
},
);
},
),
]
],
), ),
body: Column( body: Column(
children: [ children: [

View File

@ -38,10 +38,11 @@ class LyricsPage extends HookConsumerWidget {
); );
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final route = ModalRoute.of(context);
useCustomStatusBarColor( final resetStatusBar = useCustomStatusBarColor(
palette.color, palette.color,
true, route?.isCurrent ?? false,
noSetBGColor: true, noSetBGColor: true,
); );
@ -81,53 +82,57 @@ class LyricsPage extends HookConsumerWidget {
); );
if (isModal) { if (isModal) {
return DefaultTabController( return PopScope(
length: 2, canPop: true,
child: SafeArea( onPopInvokedWithResult: (_, __) => resetStatusBar(),
child: BackdropFilter( child: DefaultTabController(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), length: 2,
child: Container( child: SafeArea(
clipBehavior: Clip.hardEdge, child: BackdropFilter(
decoration: BoxDecoration( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
color: Theme.of(context).colorScheme.surface.withOpacity(.4), child: Container(
borderRadius: const BorderRadius.only( clipBehavior: Clip.hardEdge,
topLeft: Radius.circular(10), decoration: BoxDecoration(
topRight: Radius.circular(10), color: Theme.of(context).colorScheme.surface.withOpacity(.4),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
), ),
), child: Column(
child: Column( children: [
children: [ const SizedBox(height: 5),
const SizedBox(height: 5), Container(
Container( height: 7,
height: 7, width: 150,
width: 150, decoration: BoxDecoration(
decoration: BoxDecoration( color: palette.titleTextColor,
color: palette.titleTextColor, borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10),
),
),
AppBar(
leadingWidth: double.infinity,
leading: tabbar,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(SpotubeIcons.minimize),
onPressed: () => Navigator.of(context).pop(),
), ),
const SizedBox(width: 5), ),
], AppBar(
), leadingWidth: double.infinity,
Expanded( leading: tabbar,
child: TabBarView( backgroundColor: Colors.transparent,
children: [ automaticallyImplyLeading: false,
SyncedLyrics(palette: palette, isModal: isModal), actions: [
PlainLyrics(palette: palette, isModal: isModal), IconButton(
icon: const Icon(SpotubeIcons.minimize),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: 5),
], ],
), ),
), Expanded(
], child: TabBarView(
children: [
SyncedLyrics(palette: palette, isModal: isModal),
PlainLyrics(palette: palette, isModal: isModal),
],
),
),
],
),
), ),
), ),
), ),

View File

@ -1,4 +1,5 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -10,10 +11,12 @@ import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/piped_instances_provider.dart'; import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart';
import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart';
class SettingsPlaybackSection extends HookConsumerWidget { class SettingsPlaybackSection extends HookConsumerWidget {
const SettingsPlaybackSection({super.key}); const SettingsPlaybackSection({super.key});
@ -135,6 +138,73 @@ class SettingsPlaybackSection extends HookConsumerWidget {
); );
}), }),
), ),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.audioSource != AudioSource.invidious
? const SizedBox.shrink()
: Consumer(builder: (context, ref, child) {
final instanceList = ref.watch(invidiousInstancesProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary: const Icon(SpotubeIcons.piped),
title: Text(context.l10n.invidious_instance),
subtitle: RichText(
text: TextSpan(
children: [
TextSpan(
text: context.l10n.invidious_description,
style: theme.textTheme.bodyMedium,
),
const TextSpan(text: "\n"),
TextSpan(
text: context.l10n.invidious_warning,
style: theme.textTheme.labelMedium,
)
],
),
),
value: preferences.invidiousInstance,
showValueWhenUnfolded: false,
options: data
.sortedBy((e) => e.name)
.map(
(e) => DropdownMenuItem(
value: e.details.uri,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: "${e.name.trim()}\n",
style: theme.textTheme.labelLarge,
),
TextSpan(
text: countryCodeToEmoji(
e.details.region,
),
style: GoogleFonts.notoColorEmoji(),
),
],
),
),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
preferencesNotifier.setInvidiousInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Text(error.toString()),
);
}),
),
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: preferences.audioSource != AudioSource.piped child: preferences.audioSource != AudioSource.piped
@ -159,7 +229,8 @@ class SettingsPlaybackSection extends HookConsumerWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: preferences.searchMode == SearchMode.youtube && child: preferences.searchMode == SearchMode.youtube &&
(preferences.audioSource == AudioSource.piped || (preferences.audioSource == AudioSource.piped ||
preferences.audioSource == AudioSource.youtube) preferences.audioSource == AudioSource.youtube ||
preferences.audioSource == AudioSource.invidious)
? SwitchListTile( ? SwitchListTile(
secondary: const Icon(SpotubeIcons.skip), secondary: const Icon(SpotubeIcons.skip),
title: Text(context.l10n.skip_non_music), title: Text(context.l10n.skip_non_music),
@ -170,6 +241,30 @@ class SettingsPlaybackSection extends HookConsumerWidget {
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
SwitchListTile(
title: Text(context.l10n.cache_music),
subtitle: kIsMobile
? null
: Text.rich(
TextSpan(
children: [
TextSpan(text: "${context.l10n.open} "),
TextSpan(
text: context.l10n.cache_folder.toLowerCase(),
recognizer: TapGestureRecognizer()
..onTap = preferencesNotifier.openCacheFolder,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
)
],
),
),
secondary: const Icon(SpotubeIcons.cache),
value: preferences.cacheMusic,
onChanged: preferencesNotifier.setCacheMusic,
),
ListTile( ListTile(
leading: const Icon(SpotubeIcons.playlistRemove), leading: const Icon(SpotubeIcons.playlistRemove),
title: Text(context.l10n.blacklist), title: Text(context.l10n.blacklist),

View File

@ -247,7 +247,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
// Tracks related methods // Tracks related methods
Future<void> addTracksAtFirst(Iterable<Track> tracks) async { Future<void> addTracksAtFirst(
Iterable<Track> tracks, {
bool allowDuplicates = false,
}) async {
if (state.tracks.length == 1) { if (state.tracks.length == 1) {
return addTracks(tracks); return addTracks(tracks);
} }
@ -257,7 +260,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
for (int i = 0; i < tracks.length; i++) { for (int i = 0; i < tracks.length; i++) {
final track = tracks.elementAt(i); final track = tracks.elementAt(i);
if (state.tracks.any((element) => _compareTracks(element, track))) { if (!allowDuplicates &&
state.tracks.any((element) => _compareTracks(element, track))) {
continue; continue;
} }

View File

@ -74,6 +74,7 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPlaylist() { StreamSubscription subscribeToPlaylist() {
return audioPlayer.playlistStream.listen((mpvPlaylist) { return audioPlayer.playlistStream.listen((mpvPlaylist) {
try { try {
if (audioPlayerState.activeTrack == null) return;
notificationService.addTrack(audioPlayerState.activeTrack!); notificationService.addTrack(audioPlayerState.activeTrack!);
discord.updatePresence(audioPlayerState.activeTrack!); discord.updatePresence(audioPlayerState.activeTrack!);
updatePalette(); updatePalette();

View File

@ -0,0 +1,12 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart';
final invidiousInstancesProvider = FutureProvider((ref) async {
final invidious = ref.watch(invidiousProvider);
final instances = await invidious.instances();
return instances
.where((instance) => instance.details.type == "https")
.toList();
});

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -16,6 +16,7 @@ import 'package:spotube/services/download_manager/download_manager.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart';
class DownloadManagerProvider extends ChangeNotifier { class DownloadManagerProvider extends ChangeNotifier {
DownloadManagerProvider({required this.ref}) DownloadManagerProvider({required this.ref})
@ -53,33 +54,16 @@ class DownloadManagerProvider extends ChangeNotifier {
await oldFile.delete(); await oldFile.delete();
} }
final imageBytes = await downloadImage( final imageBytes = await ServiceUtils.downloadImage(
(track.album?.images).asUrlString( (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
index: 1, index: 1,
), ),
); );
final metadata = Metadata( final metadata = track.toMetadata(
title: track.name, fileLength: await file.length(),
artist: track.artists?.map((a) => a.name).join(", "), imageBytes: imageBytes,
album: track.album?.name,
albumArtist: track.artists?.map((a) => a.name).join(", "),
year: track.album?.releaseDate != null
? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969
: 1969,
trackNumber: track.trackNumber,
discNumber: track.discNumber,
durationMs: track.durationMs?.toDouble() ?? 0.0,
fileSize: BigInt.from(await file.length()),
trackTotal: track.album?.tracks?.length ?? 0,
picture: imageBytes != null
? Picture(
data: imageBytes,
// Spotify images are always JPEGs
mimeType: 'image/jpeg',
)
: null,
); );
await MetadataGod.writeMetadata( await MetadataGod.writeMetadata(
@ -116,29 +100,6 @@ class DownloadManagerProvider extends ChangeNotifier {
final Set<Track> $backHistory; final Set<Track> $backHistory;
final DownloadManager dl; final DownloadManager dl;
/// Spotify Images are always JPEGs
Future<Uint8List?> downloadImage(
String imageUrl,
) async {
try {
final fileStream = DefaultCacheManager().getImageFile(imageUrl);
final bytes = List<int>.empty(growable: true);
await for (final data in fileStream) {
if (data is FileInfo) {
bytes.addAll(data.file.readAsBytesSync());
break;
}
}
return Uint8List.fromList(bytes);
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
return null;
}
}
String getTrackFileUrl(Track track) { String getTrackFileUrl(Track track) {
final name = final name =
"${track.name} - ${track.artists?.asString() ?? ""}.${downloadCodec.name}"; "${track.name} - ${track.artists?.asString() ?? ""}.${downloadCodec.name}";

View File

@ -44,14 +44,23 @@ final localTracksProvider =
userPreferencesProvider.select((s) => s.downloadLocation), userPreferencesProvider.select((s) => s.downloadLocation),
); );
final downloadDir = Directory(downloadLocation); final downloadDir = Directory(downloadLocation);
final cacheDir =
Directory(await UserPreferencesNotifier.getMusicCacheDir());
if (!await downloadDir.exists()) { if (!await downloadDir.exists()) {
await downloadDir.create(recursive: true); await downloadDir.create(recursive: true);
} }
if (!await cacheDir.exists()) {
await cacheDir.create(recursive: true);
}
final localLibraryLocations = ref.watch( final localLibraryLocations = ref.watch(
userPreferencesProvider.select((s) => s.localLibraryLocation), userPreferencesProvider.select((s) => s.localLibraryLocation),
); );
for (final location in [downloadLocation, ...localLibraryLocations]) { for (final location in [
downloadLocation,
cacheDir.path,
...localLibraryLocations
]) {
if (location.isEmpty) continue; if (location.isEmpty) continue;
final entities = <File>[]; final entities = <File>[];
if (await Directory(location).exists()) { if (await Directory(location).exists()) {

View File

@ -31,7 +31,7 @@ class ActiveSourcedTrackNotifier extends Notifier<SourcedTrack?> {
final playbackNotifier = ref.read(audioPlayerProvider.notifier); final playbackNotifier = ref.read(audioPlayerProvider.notifier);
final oldActiveIndex = audioPlayer.currentIndex; final oldActiveIndex = audioPlayer.currentIndex;
await playbackNotifier.addTracksAtFirst([newTrack]); await playbackNotifier.addTracksAtFirst([newTrack], allowDuplicates: true);
await Future.delayed(const Duration(milliseconds: 50)); await Future.delayed(const Duration(milliseconds: 50));
await playbackNotifier.jumpToTrack(newTrack); await playbackNotifier.jumpToTrack(newTrack);

View File

@ -1,14 +1,27 @@
import 'dart:io';
import 'package:dio/dio.dart' hide Response; import 'package:dio/dio.dart' hide Response;
import 'package:dio/dio.dart' as dio_lib;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/parser/range_headers.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/service_utils.dart';
class ServerPlaybackRoutes { class ServerPlaybackRoutes {
final Ref ref; final Ref ref;
@ -18,6 +31,137 @@ class ServerPlaybackRoutes {
ServerPlaybackRoutes(this.ref) : dio = Dio(); ServerPlaybackRoutes(this.ref) : dio = Dio();
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})>
streamTrack(
SourcedTrack track,
Map<String, dynamic> headers,
) async {
final trackCacheFile = File(
join(
await UserPreferencesNotifier.getMusicCacheDir(),
'${track.name} - ${track.artists?.asString()} (${track.sourceInfo.id}).${track.codec.name}',
),
);
final trackPartialCacheFile = File("${trackCacheFile.path}.part");
var options = Options(
headers: {
...headers,
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
"host": Uri.parse(track.url).host,
},
responseType: ResponseType.bytes,
validateStatus: (status) => status! < 400,
);
final headersRes = await Future<dio_lib.Response?>.value(
dio.head(
track.url,
options: options,
),
).catchError((_) async => null);
final contentLength = headersRes?.headers.value("content-length");
if (await trackCacheFile.exists() && userPreferences.cacheMusic) {
final bytes = await trackCacheFile.readAsBytes();
final cachedFileLength = bytes.length;
return (
response: dio_lib.Response<Uint8List>(
statusCode: 200,
headers: Headers.fromMap({
"content-type": ["audio/${track.codec.name}"],
"content-length": ["$cachedFileLength"],
"accept-ranges": ["bytes"],
"content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"],
}),
requestOptions: RequestOptions(path: track.url),
),
bytes: bytes,
);
}
/// Forcing partial content range as mpv sometimes greedily wants
/// everything at one go. Slows down overall streaming.
final range = RangeHeader.parse(headers["range"] ?? "");
final contentPartialLength = int.tryParse(contentLength ?? "");
if ((range.end == null) &&
contentPartialLength != null &&
range.start == 0) {
options = options.copyWith(
headers: {
...?options.headers,
"range": "$range${(contentPartialLength * 0.3).ceil()}",
},
);
}
final res =
await dio.get<Uint8List>(track.url, options: options).catchError(
(e, stack) async {
final sourcedTrack = await ref
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
.switchToAlternativeSources();
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
return await dio.get<Uint8List>(sourcedTrack!.url, options: options);
},
);
final bytes = res.data;
if (bytes == null || !userPreferences.cacheMusic) {
return (response: res, bytes: bytes);
}
final contentRange =
ContentRangeHeader.parse(res.headers.value("content-range") ?? "");
if (!await trackPartialCacheFile.exists()) {
await trackPartialCacheFile.create(recursive: true);
}
// Write the stream to the file based on the range
final partialCacheFile =
await trackPartialCacheFile.open(mode: FileMode.writeOnlyAppend);
int fileLength = 0;
try {
await partialCacheFile.setPosition(contentRange.start);
await partialCacheFile.writeFrom(bytes);
fileLength = await partialCacheFile.length();
} finally {
await partialCacheFile.close();
}
if (fileLength == contentRange.total) {
await trackPartialCacheFile.rename(trackCacheFile.path);
}
if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) {
final imageBytes = await ServiceUtils.downloadImage(
(track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
index: 1,
),
);
await MetadataGod.writeMetadata(
file: trackCacheFile.path,
metadata: track.toMetadata(
fileLength: fileLength,
imageBytes: imageBytes,
),
);
}
return (bytes: bytes, response: res);
}
/// @get('/stream/<trackId>') /// @get('/stream/<trackId>')
Future<Response> getStreamTrackId(Request request, String trackId) async { Future<Response> getStreamTrackId(Request request, String trackId) async {
try { try {
@ -31,36 +175,12 @@ class ServerPlaybackRoutes {
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
final res = await dio.get( final (bytes: audioBytes, response: res) =
sourcedTrack!.url, await streamTrack(sourcedTrack!, request.headers);
options: Options(
headers: {
...request.headers,
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"host": Uri.parse(sourcedTrack.url).host,
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
},
responseType: ResponseType.stream,
validateStatus: (status) => status! < 500,
),
);
final audioStream =
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();
audioStream!.listen(
(event) {},
cancelOnError: true,
);
return Response( return Response(
res.statusCode!, res.statusCode!,
body: audioStream, body: audioBytes,
context: {
"shelf.io.buffer_output": false,
},
headers: res.headers.map, headers: res.headers.map,
); );
} catch (e, stack) { } catch (e, stack) {

View File

@ -5,24 +5,44 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
final sourcedTrackProvider = class SourcedTrackNotifier
FutureProvider.family<SourcedTrack?, SpotubeMedia?>((ref, media) async { extends FamilyAsyncNotifier<SourcedTrack?, SpotubeMedia?> {
final track = media?.track; @override
if (track == null || track is LocalTrack) { build(media) async {
return null; final track = media?.track;
if (track == null || track is LocalTrack) {
return null;
}
ref.listen(
audioPlayerProvider.select((value) => value.tracks),
(old, next) {
if (next.isEmpty || next.none((element) => element.id == track.id)) {
ref.invalidateSelf();
}
},
);
final sourcedTrack =
await SourcedTrack.fetchFromTrack(track: track, ref: ref);
return sourcedTrack;
} }
ref.listen( Future<SourcedTrack?> switchToAlternativeSources() async {
audioPlayerProvider.select((value) => value.tracks), if (arg == null) {
(old, next) { return null;
if (next.isEmpty || next.none((element) => element.id == track.id)) { }
ref.invalidateSelf(); return await update((prev) async {
} return await SourcedTrack.fetchFromTrackAltSource(
}, track: arg!.track,
); ref: ref,
);
});
}
}
final sourcedTrack = final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
await SourcedTrack.fetchFromTrack(track: track, ref: ref); SourcedTrack?, SpotubeMedia?>(
() => SourcedTrackNotifier(),
return sourcedTrack; );
});

View File

@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart' as paths;
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
@ -14,6 +14,7 @@ import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:open_file/open_file.dart';
typedef UserPreferences = PreferencesTableData; typedef UserPreferences = PreferencesTableData;
@ -71,10 +72,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
if (kIsMacOS) { if (kIsMacOS) {
return join((await getLibraryDirectory()).path, "Caches"); return join((await paths.getLibraryDirectory()).path, "Caches");
} }
return getDownloadsDirectory().then((dir) { return paths.getDownloadsDirectory().then((dir) {
return join(dir!.path, "Spotube"); return join(dir!.path, "Spotube");
}); });
} }
@ -95,6 +96,30 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
await query.replace(PreferencesTableCompanion.insert()); await query.replace(PreferencesTableCompanion.insert());
} }
static Future<String> getMusicCacheDir() async {
if (kIsAndroid) {
final dir =
await paths.getExternalCacheDirectories().then((dirs) => dirs!.first);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
return join(dir.path, 'Cached Tracks');
}
final dir = await paths.getApplicationCacheDirectory();
return join(dir.path, 'cached_tracks');
}
Future<void> openCacheFolder() async {
try {
final filePath = await getMusicCacheDir();
await OpenFile.open(filePath);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}
void setStreamMusicCodec(SourceCodecs codec) { void setStreamMusicCodec(SourceCodecs codec) {
setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); setData(PreferencesTableCompanion(streamMusicCodec: Value(codec)));
} }
@ -167,6 +192,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
setData(PreferencesTableCompanion(pipedInstance: Value(instance))); setData(PreferencesTableCompanion(pipedInstance: Value(instance)));
} }
void setInvidiousInstance(String instance) {
setData(PreferencesTableCompanion(invidiousInstance: Value(instance)));
}
void setSearchMode(SearchMode mode) { void setSearchMode(SearchMode mode) {
setData(PreferencesTableCompanion(searchMode: Value(mode))); setData(PreferencesTableCompanion(searchMode: Value(mode)));
} }
@ -207,6 +236,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
void setEnableConnect(bool enable) { void setEnableConnect(bool enable) {
setData(PreferencesTableCompanion(enableConnect: Value(enable))); setData(PreferencesTableCompanion(enableConnect: Value(enable)));
} }
void setCacheMusic(bool cache) {
setData(PreferencesTableCompanion(cacheMusic: Value(cache)));
}
} }
final userPreferencesProvider = final userPreferencesProvider =

View File

@ -7,6 +7,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -59,6 +60,9 @@ class MobileAudioService extends BaseAudioHandler {
}); });
}); });
audioPlayer.playerStateStream.listen((state) async { audioPlayer.playerStateStream.listen((state) async {
if (state == AudioPlaybackState.playing) {
await session?.setActive(true);
}
playbackState.add(await _transformEvent()); playbackState.add(await _transformEvent());
}); });

View File

@ -30,8 +30,12 @@ mixin _$SongLink {
String? get nativeAppUriMobile => throw _privateConstructorUsedError; String? get nativeAppUriMobile => throw _privateConstructorUsedError;
String? get nativeAppUriDesktop => throw _privateConstructorUsedError; String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
/// Serializes this SongLink to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SongLinkCopyWith<SongLink> get copyWith => $SongLinkCopyWith<SongLink> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -63,6 +67,8 @@ class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -145,6 +151,8 @@ class __$$SongLinkImplCopyWithImpl<$Res>
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then) _$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -261,12 +269,14 @@ class _$SongLinkImpl implements _SongLink {
other.nativeAppUriDesktop == nativeAppUriDesktop)); other.nativeAppUriDesktop == nativeAppUriDesktop));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform, int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop); show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
@JsonKey(ignore: true) /// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
@ -313,8 +323,11 @@ abstract class _SongLink implements SongLink {
String? get nativeAppUriMobile; String? get nativeAppUriMobile;
@override @override
String? get nativeAppUriDesktop; String? get nativeAppUriDesktop;
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View File

@ -12,7 +12,7 @@ SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo(
artist: json['artist'] as String, artist: json['artist'] as String,
thumbnail: json['thumbnail'] as String, thumbnail: json['thumbnail'] as String,
pageUrl: json['pageUrl'] as String, pageUrl: json['pageUrl'] as String,
duration: Duration(microseconds: json['duration'] as int), duration: Duration(microseconds: (json['duration'] as num).toInt()),
artistUrl: json['artistUrl'] as String, artistUrl: json['artistUrl'] as String,
album: json['album'] as String?, album: json['album'] as String?,
); );

View File

@ -1,3 +1,4 @@
import 'package:invidious/invidious.dart';
import 'package:piped_client/piped_client.dart'; import 'package:piped_client/piped_client.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
@ -112,4 +113,24 @@ class YoutubeVideoInfo {
channelId: stream.uploaderUrl, channelId: stream.uploaderUrl,
); );
} }
factory YoutubeVideoInfo.fromSearchResponse(
InvidiousSearchResponseVideo searchResponse,
SearchMode searchMode,
) {
return YoutubeVideoInfo(
searchMode: searchMode,
title: searchResponse.title,
duration: Duration(seconds: searchResponse.lengthSeconds),
thumbnailUrl: searchResponse.videoThumbnails.first.url,
id: searchResponse.videoId,
likes: 0,
dislikes: 0,
views: searchResponse.viewCount,
channelName: searchResponse.author,
channelId: searchResponse.authorId,
publishedAt:
DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000),
);
}
} }

View File

@ -12,6 +12,7 @@ import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/source_map.dart'; import 'package:spotube/services/sourced_track/models/source_map.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -85,6 +86,13 @@ abstract class SourcedTrack extends Track {
sourceInfo: sourceInfo, sourceInfo: sourceInfo,
track: track, track: track,
), ),
AudioSource.invidious => InvidiousSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
sourceInfo: sourceInfo,
track: track,
),
}; };
} }
@ -104,6 +112,49 @@ abstract class SourcedTrack extends Track {
return "$title - ${artists.join(", ")}"; return "$title - ${artists.join(", ")}";
} }
static fetchFromTrackAltSource({
required Track track,
required Ref ref,
}) async {
final preferences = ref.read(userPreferencesProvider);
try {
return switch (preferences.audioSource) {
AudioSource.piped ||
AudioSource.invidious ||
AudioSource.jiosaavn =>
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.youtube =>
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
};
} on TrackNotFoundError catch (_) {
return switch (preferences.audioSource) {
AudioSource.piped ||
AudioSource.youtube ||
AudioSource.invidious =>
await JioSaavnSourcedTrack.fetchFromTrack(
track: track,
ref: ref,
weakMatch: true,
),
AudioSource.jiosaavn =>
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
};
} on HttpClientClosedException catch (_) {
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
} on VideoUnplayableException catch (_) {
return await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref);
} catch (e) {
if (e is DioException || e is ClientException || e is SocketException) {
return await JioSaavnSourcedTrack.fetchFromTrack(
track: track,
ref: ref,
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
);
}
rethrow;
}
}
static Future<SourcedTrack> fetchFromTrack({ static Future<SourcedTrack> fetchFromTrack({
required Track track, required Track track,
required Ref ref, required Ref ref,
@ -117,11 +168,14 @@ abstract class SourcedTrack extends Track {
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref), await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.jiosaavn => AudioSource.jiosaavn =>
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.invidious =>
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
}; };
} on TrackNotFoundError catch (_) { } on TrackNotFoundError catch (_) {
return switch (preferences.audioSource) { return switch (preferences.audioSource) {
AudioSource.piped || AudioSource.piped ||
AudioSource.youtube => AudioSource.youtube ||
AudioSource.invidious =>
await JioSaavnSourcedTrack.fetchFromTrack( await JioSaavnSourcedTrack.fetchFromTrack(
track: track, track: track,
ref: ref, ref: ref,
@ -136,11 +190,19 @@ abstract class SourcedTrack extends Track {
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref); return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
} catch (e) { } catch (e) {
if (e is DioException || e is ClientException || e is SocketException) { if (e is DioException || e is ClientException || e is SocketException) {
return await JioSaavnSourcedTrack.fetchFromTrack( return switch (preferences.audioSource) {
track: track, AudioSource.piped ||
ref: ref, AudioSource.invidious =>
weakMatch: preferences.audioSource == AudioSource.jiosaavn, await YoutubeSourcedTrack.fetchFromTrack(
); track: track,
ref: ref,
),
_ => await JioSaavnSourcedTrack.fetchFromTrack(
track: track,
ref: ref,
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
)
};
} }
rethrow; rethrow;
} }
@ -159,6 +221,8 @@ abstract class SourcedTrack extends Track {
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref), YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
AudioSource.jiosaavn => AudioSource.jiosaavn =>
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref), JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
AudioSource.invidious =>
InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref),
}; };
} }

View File

@ -0,0 +1,266 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/source_map.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:invidious/invidious.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
final invidiousProvider = Provider<InvidiousClient>(
(ref) {
final invidiousInstance = ref.watch(
userPreferencesProvider.select((s) => s.invidiousInstance),
);
return InvidiousClient(server: invidiousInstance);
},
);
class InvidiousSourceInfo extends SourceInfo {
InvidiousSourceInfo({
required super.id,
required super.title,
required super.artist,
required super.thumbnail,
required super.pageUrl,
required super.duration,
required super.artistUrl,
required super.album,
});
}
class InvidiousSourcedTrack extends SourcedTrack {
InvidiousSourcedTrack({
required super.ref,
required super.source,
required super.siblings,
required super.sourceInfo,
required super.track,
});
static Future<SourcedTrack> fetchFromTrack({
required Track track,
required Ref ref,
}) async {
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(track.id!))
..limit(1)
..orderBy([
(s) =>
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
]))
.getSingleOrNull();
final invidiousClient = ref.read(invidiousProvider);
if (cachedSource == null) {
final siblings = await fetchSiblings(ref: ref, track: track);
if (siblings.isEmpty) {
throw TrackNotFoundError(track);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: track.id!,
sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.youtube),
),
);
return InvidiousSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
track: track,
);
} else {
final manifest =
await invidiousClient.videos.get(cachedSource.sourceId, local: true);
return InvidiousSourcedTrack(
ref: ref,
siblings: [],
source: toSourceMap(manifest),
sourceInfo: InvidiousSourceInfo(
id: manifest.videoId,
artist: manifest.author,
artistUrl: manifest.authorUrl,
pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}",
thumbnail: manifest.videoThumbnails.first.url,
title: manifest.title,
duration: Duration(seconds: manifest.lengthSeconds),
album: null,
),
track: track,
);
}
}
static SourceMap toSourceMap(InvidiousVideoResponse manifest) {
final m4a = manifest.adaptiveFormats
.where((audio) => audio.type.contains("audio/mp4"))
.sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate)));
final weba = manifest.adaptiveFormats
.where((audio) => audio.type.contains("audio/webm"))
.sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate)));
return SourceMap(
m4a: SourceQualityMap(
high: m4a.first.url.toString(),
medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(),
low: m4a.last.url.toString(),
),
weba: SourceQualityMap(
high: weba.first.url.toString(),
medium:
(weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(),
low: weba.last.url.toString(),
),
);
}
static Future<SiblingType> toSiblingType(
int index,
YoutubeVideoInfo item,
InvidiousClient invidiousClient,
) async {
SourceMap? sourceMap;
if (index == 0) {
final manifest = await invidiousClient.videos.get(item.id, local: true);
sourceMap = toSourceMap(manifest);
}
final SiblingType sibling = (
info: InvidiousSourceInfo(
id: item.id,
artist: item.channelName,
artistUrl: "https://www.youtube.com/${item.channelId}",
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
thumbnail: item.thumbnailUrl,
title: item.title,
duration: item.duration,
album: null,
),
source: sourceMap,
);
return sibling;
}
static Future<List<SiblingType>> fetchSiblings({
required Track track,
required Ref ref,
}) async {
final invidiousClient = ref.read(invidiousProvider);
final preference = ref.read(userPreferencesProvider);
final query = SourcedTrack.getSearchTerm(track);
final searchResults = await invidiousClient.search.list(
query,
type: InvidiousSearchType.video,
);
if (ServiceUtils.onlyContainsEnglish(query)) {
return await Future.wait(
searchResults
.whereType<InvidiousSearchResponseVideo>()
.map(
(result) => YoutubeVideoInfo.fromSearchResponse(
result,
preference.searchMode,
),
)
.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)),
);
}
final rankedSiblings = YoutubeSourcedTrack.rankResults(
searchResults
.whereType<InvidiousSearchResponseVideo>()
.map(
(result) => YoutubeVideoInfo.fromSearchResponse(
result,
preference.searchMode,
),
)
.toList(),
track,
);
return await Future.wait(
rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)),
);
}
@override
Future<SourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
return InvidiousSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != sourceInfo.id)
.map((s) => s.info)
.toList(),
source: source,
sourceInfo: sourceInfo,
track: this,
);
}
@override
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
if (sibling.id == sourceInfo.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, sourceInfo);
final pipedClient = ref.read(invidiousProvider);
final manifest =
await pipedClient.videos.get(newSourceInfo.id, local: true);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: id!,
sourceId: newSourceInfo.id,
sourceType: const Value(SourceType.youtube),
// Because we're sorting by createdAt in the query
// we have to update it to indicate priority
createdAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
return InvidiousSourcedTrack(
ref: ref,
siblings: newSiblings,
source: toSourceMap(manifest),
sourceInfo: newSourceInfo,
track: this,
);
}
}

View File

@ -53,8 +53,12 @@ mixin _$UserPreferences {
bool get endlessPlayback => throw _privateConstructorUsedError; bool get endlessPlayback => throw _privateConstructorUsedError;
bool get enableConnect => throw _privateConstructorUsedError; bool get enableConnect => throw _privateConstructorUsedError;
/// Serializes this UserPreferences to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserPreferencesCopyWith<UserPreferences> get copyWith => $UserPreferencesCopyWith<UserPreferences> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -110,6 +114,8 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -290,6 +296,8 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
_$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then) _$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -605,7 +613,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
other.enableConnect == enableConnect)); other.enableConnect == enableConnect));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hashAll([ int get hashCode => Object.hashAll([
runtimeType, runtimeType,
@ -635,7 +643,9 @@ class _$UserPreferencesImpl implements _UserPreferences {
enableConnect enableConnect
]); ]);
@JsonKey(ignore: true) /// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
@ -744,8 +754,11 @@ abstract class _UserPreferences implements UserPreferences {
bool get endlessPlayback; bool get endlessPlayback;
@override @override
bool get enableConnect; bool get enableConnect;
/// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -812,8 +825,13 @@ mixin _$PlaybackHistoryItem {
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this PlaybackHistoryItem to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$PlaybackHistoryItemCopyWith<PlaybackHistoryItem> get copyWith => $PlaybackHistoryItemCopyWith<PlaybackHistoryItem> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -837,6 +855,8 @@ class _$PlaybackHistoryItemCopyWithImpl<$Res, $Val extends PlaybackHistoryItem>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -873,6 +893,8 @@ class __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>
$Res Function(_$PlaybackHistoryPlaylistImpl) _then) $Res Function(_$PlaybackHistoryPlaylistImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -925,11 +947,13 @@ class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist {
other.playlist == playlist)); other.playlist == playlist));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, date, playlist); int get hashCode => Object.hash(runtimeType, date, playlist);
@JsonKey(ignore: true) /// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl>
@ -1023,8 +1047,11 @@ abstract class PlaybackHistoryPlaylist implements PlaybackHistoryItem {
@override @override
DateTime get date; DateTime get date;
PlaylistSimple get playlist; PlaylistSimple get playlist;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1048,6 +1075,8 @@ class __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>
$Res Function(_$PlaybackHistoryAlbumImpl) _then) $Res Function(_$PlaybackHistoryAlbumImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1099,11 +1128,13 @@ class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum {
(identical(other.album, album) || other.album == album)); (identical(other.album, album) || other.album == album));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, date, album); int get hashCode => Object.hash(runtimeType, date, album);
@JsonKey(ignore: true) /// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl>
@ -1198,8 +1229,11 @@ abstract class PlaybackHistoryAlbum implements PlaybackHistoryItem {
@override @override
DateTime get date; DateTime get date;
AlbumSimple get album; AlbumSimple get album;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }
@ -1223,6 +1257,8 @@ class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>
$Res Function(_$PlaybackHistoryTrackImpl) _then) $Res Function(_$PlaybackHistoryTrackImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -1274,11 +1310,13 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
(identical(other.track, track) || other.track == track)); (identical(other.track, track) || other.track == track));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, date, track); int get hashCode => Object.hash(runtimeType, date, track);
@JsonKey(ignore: true) /// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
@ -1373,8 +1411,11 @@ abstract class PlaybackHistoryTrack implements PlaybackHistoryItem {
@override @override
DateTime get date; DateTime get date;
Track get track; Track get track;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }

View File

@ -1,4 +1,7 @@
import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:html/dom.dart' hide Text; import 'package:html/dom.dart' hide Text;
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -8,6 +11,7 @@ import 'package:spotube/modules/root/update_dialog.dart';
import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/lyrics.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
@ -449,4 +453,27 @@ abstract class ServiceUtils {
); );
} }
} }
/// Spotify Images are always JPEGs
static Future<Uint8List?> downloadImage(
String imageUrl,
) async {
try {
final fileStream = DefaultCacheManager().getImageFile(imageUrl);
final bytes = List<int>.empty(growable: true);
await for (final data in fileStream) {
if (data is FileInfo) {
bytes.addAll(data.file.readAsBytesSync());
break;
}
}
return Uint8List.fromList(bytes);
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
return null;
}
}
} }

View File

@ -12,7 +12,8 @@
#include <gtk/gtk_plugin.h> #include <gtk/gtk_plugin.h>
#include <local_notifier/local_notifier_plugin.h> #include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <open_file_linux/open_file_linux_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <system_theme/system_theme_plugin.h> #include <system_theme/system_theme_plugin.h>
#include <tray_manager/tray_manager_plugin.h> #include <tray_manager/tray_manager_plugin.h>
@ -38,9 +39,12 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar = g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

@ -9,7 +9,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
gtk gtk
local_notifier local_notifier
media_kit_libs_linux media_kit_libs_linux
screen_retriever open_file_linux
screen_retriever_linux
sqlite3_flutter_libs sqlite3_flutter_libs
system_theme system_theme
tray_manager tray_manager

View File

@ -16,11 +16,12 @@ import flutter_inappwebview_macos
import flutter_secure_storage_macos import flutter_secure_storage_macos
import local_notifier import local_notifier
import media_kit_libs_macos_audio import media_kit_libs_macos_audio
import open_file_mac
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
import screen_retriever import screen_retriever_macos
import shared_preferences_foundation import shared_preferences_foundation
import sqflite import sqflite_darwin
import sqlite3_flutter_libs import sqlite3_flutter_libs
import system_theme import system_theme
import tray_manager import tray_manager
@ -39,9 +40,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))

View File

@ -30,32 +30,37 @@ PODS:
- FlutterMacOS - FlutterMacOS
- metadata_god (0.0.1): - metadata_god (0.0.1):
- FlutterMacOS - FlutterMacOS
- open_file_mac (0.0.1):
- FlutterMacOS
- OrderedSet (6.0.3) - OrderedSet (6.0.3)
- package_info_plus (0.0.1): - package_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- screen_retriever (0.0.1): - screen_retriever_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.3): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- "sqlite3 (3.46.0+1)": - sqlite3 (3.47.0):
- "sqlite3/common (= 3.46.0+1)" - sqlite3/common (= 3.47.0)
- "sqlite3/common (3.46.0+1)" - sqlite3/common (3.47.0)
- "sqlite3/fts5 (3.46.0+1)": - sqlite3/dbstatvtab (3.47.0):
- sqlite3/common - sqlite3/common
- "sqlite3/perf-threadsafe (3.46.0+1)": - sqlite3/fts5 (3.47.0):
- sqlite3/common - sqlite3/common
- "sqlite3/rtree (3.46.0+1)": - sqlite3/perf-threadsafe (3.47.0):
- sqlite3/common
- sqlite3/rtree (3.47.0):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.46.0) - sqlite3 (~> 3.47.0)
- sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
- sqlite3/rtree - sqlite3/rtree
@ -84,11 +89,12 @@ DEPENDENCIES:
- media_kit_libs_macos_audio (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos`) - media_kit_libs_macos_audio (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`) - media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- metadata_god (from `Flutter/ephemeral/.symlinks/plugins/metadata_god/macos`) - metadata_god (from `Flutter/ephemeral/.symlinks/plugins/metadata_god/macos`)
- open_file_mac (from `Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
@ -131,16 +137,18 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos :path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
metadata_god: metadata_god:
:path: Flutter/ephemeral/.symlinks/plugins/metadata_god/macos :path: Flutter/ephemeral/.symlinks/plugins/metadata_god/macos
open_file_mac:
:path: Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos
package_info_plus: package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_retriever: screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
shared_preferences_foundation: shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite: sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
sqlite3_flutter_libs: sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
system_theme: system_theme:
@ -158,29 +166,30 @@ SPEC CHECKSUMS:
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
flutter_discord_rpc: 67a7c10ea24d9d3bf35d01af643f48fbcfa7c24f flutter_discord_rpc: 67a7c10ea24d9d3bf35d01af643f48fbcfa7c24f
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
media_kit_libs_macos_audio: 3871782a4f3f84c77f04d7666c87800a781c24da media_kit_libs_macos_audio: 3871782a4f3f84c77f04d7666c87800a781c24da
media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5 media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5
metadata_god: 829f61208b44ac1173e7cd32ab740d8776be5435 metadata_god: 829f61208b44ac1173e7cd32ab740d8776be5435
open_file_mac: 0e554648e2a87ce59e9438e3e5ca3e552e90d89a
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 sqlite3: 0aa20658a9b238a3b1ff7175eb7bdd863b0ab4fd
sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 sqlite3_flutter_libs: f0b7a85544d8bac7b8bac12eac7d05bcfdd786d0
system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0
COCOAPODS: 1.15.2 COCOAPODS: 1.16.2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Some files were not shown because too many files have changed in this diff Show More