Merge branch 'website-ads-integration' into website

This commit is contained in:
Kingkor Roy Tirtho 2024-12-13 00:17:38 +06:00
commit efa2b77ac3
128 changed files with 16532 additions and 2089 deletions

View File

@ -1,16 +1,16 @@
# The format:
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
SPOTIFY_SECRETS=
SPOTIFY_SECRETS=$SPOTIFY_SECRETS
# 0 or 1
# 0 = disable
# 1 = enable
ENABLE_UPDATE_CHECK=
ENABLE_UPDATE_CHECK=$ENABLE_UPDATE_CHECK
LASTFM_API_KEY=
LASTFM_API_SECRET=
LASTFM_API_KEY=$LASTFM_API_KEY
LASTFM_API_SECRET=$LASTFM_API_SECRET
# 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": {}
}

View File

@ -4,7 +4,7 @@ on:
pull_request:
env:
FLUTTER_VERSION: 3.22.2
FLUTTER_VERSION: 3.24.5
jobs:
lint:
@ -17,18 +17,23 @@ jobs:
with:
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
run: |
flutter pub get
echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
dart run build_runner build --delete-conflicting-outputs
- name: Lint Dart files
run: |
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
env:
FLUTTER_VERSION: 3.24.3
FLUTTER_VERSION: 3.24.5
permissions:
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.
## [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)
## Changes

View File

@ -43,3 +43,6 @@ apk:
gensums:
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>
</td>
</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>
<td>Flatpak</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. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
1. [audio_service_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_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
1. [auto_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. [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. [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. [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. [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. [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. [device_info_plus](https://github.com/fluttercommunity/plus_plugins) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on.
1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.
1. [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. [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. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
1. [flutter_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_discord_rpc](https://pub.dev/packages/flutter_discord_rpc) - Discord RPC support for Flutter desktop platforms
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
1. [flutter_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_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_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.
@ -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. [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](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. [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. [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_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. [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_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
1. [introduction_screen](https://pub.dev/packages/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
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. [invidious](https://pub.dev/packages/invidious) - Invidious API client for Dart and Flutter.
1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
1. [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. [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. [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. [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. [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. [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_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
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. [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. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
1. [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. [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. [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. [sqlite3](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3) - Provides lightweight yet convenient bindings to SQLite by using dart:ffi
1. [sqlite3_flutter_libs](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs) - Flutter plugin to include native sqlite3 libraries with your app
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
1. [system_theme](https://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. [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.
@ -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. [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. [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. [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>
<div align="center"><h4>© Copyright Spotube 2024</h4></div>

View File

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

View File

@ -1,8 +1,18 @@
{
"title": "Spotube",
"icon": "assets/spotube-logo.png",
"icon": "assets/spotube-logo-macos.png",
"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
drift_dev:
options:
databases:
app_db: lib/models/database/database.dart
sql:
dialect: sqlite
options:

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

View File

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

View File

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

View File

@ -128,9 +128,12 @@ final routerProvider = Provider((ref) {
pageBuilder: (context, state) {
assert(state.extra is String);
return SpotubePage(
child: LocalLibraryPage(state.extra as String,
child: LocalLibraryPage(
state.extra as String,
isDownloads:
state.uri.queryParameters["downloads"] != null),
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 folderAdd = FeatherIcons.folderPlus;
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:typed_data';
import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart';
@ -37,6 +38,33 @@ extension TrackExtensions on Track {
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 {

View File

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

View File

@ -387,5 +387,19 @@
"total_money": "المجموع {money}",
"webview_not_found": "لم يتم العثور على Webview",
"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}",
"webview_not_found": "ওয়েবভিউ পাওয়া যায়নি",
"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}",
"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ó",
"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}",
"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",
"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}",
"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",
"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_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",
"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",
"track_exists": "Track {track} already exists",
"replace_downloaded_tracks": "Replace all downloaded tracks",
@ -307,6 +310,7 @@
"youtube_source_description": "Recommended and works best.",
"piped_source_description": "Feeling free? Same as YouTube but a lot free.",
"jiosaavn_source_description": "Best for South Asian region.",
"invidious_source_description": "Similar to Piped but with higher availability.",
"highest_quality": "Highest Quality: {quality}",
"select_audio_source": "Select Audio Source",
"endless_playback_description": "Automatically append new songs\nto the end of the queue",
@ -387,5 +391,15 @@
"total_money": "Total {money}",
"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",
"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}",
"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",
"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}",
"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",
"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}",
"webview_not_found": "وب‌ویو پیدا نشد",
"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}",
"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",
"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}",
"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",
"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भुक्तान गर्नुपर्ने थियो।",
"webview_not_found": "वेबव्यू नहीं मिला",
"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}",
"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",
"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}",
"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",
"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どれくらい支払ったかをユーザーに示すためのものです。",
"webview_not_found": "Webviewが見つかりません",
"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-ზე.",
"webview_not_found": "ვებვიუ ვერ მოიძებნა",
"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알려주기 위한 가상의 계산입니다.",
"webview_not_found": "웹뷰를 찾을 수 없음",
"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भुक्तान गर्नुपर्ने थियो।",
"webview_not_found": "वेबभ्यू फेला परेन",
"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.",
"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",
"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.",
"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ę",
"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.",
"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",
"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.",
"webview_not_found": "Webview не найден",
"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.",
"webview_not_found": "ไม่พบ Webview",
"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.",
"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",
"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.",
"webview_not_found": "Webview не знайдено",
"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.",
"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",
"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收听歌曲会支付给艺术家的金额。",
"webview_not_found": "未找到 Webview",
"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(),
}) =>
throw _privateConstructorUsedError;
/// Serializes this WebSocketLoadEventData to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -127,6 +132,8 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -171,6 +178,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
$Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then)
: super(_value, _then);
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -247,12 +256,14 @@ class _$WebSocketLoadEventDataPlaylistImpl
other.initialIndex == initialIndex));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,
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
@pragma('vm:prefer-inline')
_$$WebSocketLoadEventDataPlaylistImplCopyWith<
@ -372,8 +383,11 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
PlaylistSimple? get collection;
@override
int? get initialIndex;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$WebSocketLoadEventDataPlaylistImplCopyWith<
_$WebSocketLoadEventDataPlaylistImpl>
get copyWith => throw _privateConstructorUsedError;
@ -404,6 +418,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
$Res Function(_$WebSocketLoadEventDataAlbumImpl) _then)
: super(_value, _then);
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -479,12 +495,14 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
other.initialIndex == initialIndex));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,
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
@pragma('vm:prefer-inline')
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
@ -603,8 +621,11 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
AlbumSimple? get collection;
@override
int? get initialIndex;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -16,7 +16,7 @@ _$WebSocketLoadEventDataPlaylistImpl
? null
: PlaylistSimple.fromJson(
Map<String, dynamic>.from(json['collection'] as Map)),
initialIndex: json['initialIndex'] as int?,
initialIndex: (json['initialIndex'] as num?)?.toInt(),
$type: json['runtimeType'] as String?,
);
@ -39,7 +39,7 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
? null
: AlbumSimple.fromJson(
Map<String, dynamic>.from(json['collection'] as Map)),
initialIndex: json['initialIndex'] as int?,
initialIndex: (json['initialIndex'] as num?)?.toInt(),
$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_provider/path_provider.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/database/database.steps.dart';
import 'package:spotube/models/lyrics.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
@ -57,7 +58,28 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@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() {

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 {
youtube,
piped,
jiosaavn;
jiosaavn,
invidious;
String get label => name[0].toUpperCase() + name.substring(1);
}
@ -77,6 +78,8 @@ class PreferencesTable extends Table {
text().withDefault(const Constant("")).map(const StringListConverter())();
TextColumn get pipedInstance =>
text().withDefault(const Constant("https://pipedapi.kavin.rocks"))();
TextColumn get invidiousInstance =>
text().withDefault(const Constant("https://inv.nadeko.net"))();
TextColumn get themeMode =>
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
TextColumn get audioSource =>
@ -91,6 +94,7 @@ class PreferencesTable extends Table {
boolean().withDefault(const Constant(true))();
BoolColumn get enableConnect =>
boolean().withDefault(const Constant(false))();
BoolColumn get cacheMusic => boolean().withDefault(const Constant(true))();
// Default values as PreferencesTableData
static PreferencesTableData defaults() {
@ -113,13 +117,15 @@ class PreferencesTable extends Table {
downloadLocation: "",
localLibraryLocation: [],
pipedInstance: "https://pipedapi.kavin.rocks",
invidiousInstance: "https://inv.nadeko.net",
themeMode: ThemeMode.system,
audioSource: AudioSource.youtube,
streamMusicCodec: SourceCodecs.weba,
streamMusicCodec: SourceCodecs.m4a,
downloadMusicCodec: SourceCodecs.m4a,
discordPresence: true,
endlessPlayback: true,
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 uri => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionPlaylist to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -61,6 +65,8 @@ class _$SpotifySectionPlaylistCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -128,6 +134,8 @@ class __$$SpotifySectionPlaylistImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionPlaylistImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -221,12 +229,14 @@ class _$SpotifySectionPlaylistImpl extends _SpotifySectionPlaylist {
(identical(other.uri, uri) || other.uri == uri));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, description, format,
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
@pragma('vm:prefer-inline')
_$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl>
@ -266,8 +276,11 @@ abstract class _SpotifySectionPlaylist extends SpotifySectionPlaylist {
String get owner;
@override
String get uri;
/// Create a copy of SpotifySectionPlaylist
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionPlaylistImplCopyWith<_$SpotifySectionPlaylistImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -283,8 +296,12 @@ mixin _$SpotifySectionArtist {
List<SpotifySectionItemImage> get images =>
throw _privateConstructorUsedError;
/// Serializes this SpotifySectionArtist to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -309,6 +326,8 @@ class _$SpotifySectionArtistCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -352,6 +371,8 @@ class __$$SpotifySectionArtistImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionArtistImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -416,12 +437,14 @@ class _$SpotifySectionArtistImpl extends _SpotifySectionArtist {
const DeepCollectionEquality().equals(other._images, _images));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
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
@pragma('vm:prefer-inline')
_$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl>
@ -454,8 +477,11 @@ abstract class _SpotifySectionArtist extends SpotifySectionArtist {
String get uri;
@override
List<SpotifySectionItemImage> get images;
/// Create a copy of SpotifySectionArtist
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionArtistImplCopyWith<_$SpotifySectionArtistImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -473,8 +499,12 @@ mixin _$SpotifySectionAlbum {
String get name => throw _privateConstructorUsedError;
String get uri => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionAlbum to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -502,6 +532,8 @@ class _$SpotifySectionAlbumCopyWithImpl<$Res, $Val extends SpotifySectionAlbum>
// ignore: unused_field
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')
@override
$Res call({
@ -554,6 +586,8 @@ class __$$SpotifySectionAlbumImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionAlbumImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -635,7 +669,7 @@ class _$SpotifySectionAlbumImpl extends _SpotifySectionAlbum {
(identical(other.uri, uri) || other.uri == uri));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
@ -644,7 +678,9 @@ class _$SpotifySectionAlbumImpl extends _SpotifySectionAlbum {
name,
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
@pragma('vm:prefer-inline')
_$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith =>
@ -678,8 +714,11 @@ abstract class _SpotifySectionAlbum extends SpotifySectionAlbum {
String get name;
@override
String get uri;
/// Create a copy of SpotifySectionAlbum
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionAlbumImplCopyWith<_$SpotifySectionAlbumImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@ -694,8 +733,12 @@ mixin _$SpotifySectionAlbumArtist {
String get name => throw _privateConstructorUsedError;
String get uri => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionAlbumArtist to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -720,6 +763,8 @@ class _$SpotifySectionAlbumArtistCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -761,6 +806,8 @@ class __$$SpotifySectionAlbumArtistImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionAlbumArtistImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -808,11 +855,13 @@ class _$SpotifySectionAlbumArtistImpl extends _SpotifySectionAlbumArtist {
(identical(other.uri, uri) || other.uri == uri));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
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
@pragma('vm:prefer-inline')
_$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl>
@ -840,8 +889,11 @@ abstract class _SpotifySectionAlbumArtist extends SpotifySectionAlbumArtist {
String get name;
@override
String get uri;
/// Create a copy of SpotifySectionAlbumArtist
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionAlbumArtistImplCopyWith<_$SpotifySectionAlbumArtistImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -857,8 +909,12 @@ mixin _$SpotifySectionItemImage {
String get url => throw _privateConstructorUsedError;
num? get width => throw _privateConstructorUsedError;
/// Serializes this SpotifySectionItemImage to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -883,6 +939,8 @@ class _$SpotifySectionItemImageCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -929,6 +987,8 @@ class __$$SpotifySectionItemImageImplCopyWithImpl<$Res>
$Res Function(_$SpotifySectionItemImageImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -985,11 +1045,13 @@ class _$SpotifySectionItemImageImpl extends _SpotifySectionItemImage {
(identical(other.width, width) || other.width == width));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
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
@pragma('vm:prefer-inline')
_$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl>
@ -1020,8 +1082,11 @@ abstract class _SpotifySectionItemImage extends SpotifySectionItemImage {
String get url;
@override
num? get width;
/// Create a copy of SpotifySectionItemImage
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifySectionItemImageImplCopyWith<_$SpotifySectionItemImageImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -1038,8 +1103,12 @@ mixin _$SpotifyHomeFeedSectionItem {
SpotifySectionArtist? get artist => throw _privateConstructorUsedError;
SpotifySectionAlbum? get album => throw _privateConstructorUsedError;
/// Serializes this SpotifyHomeFeedSectionItem to a JSON map.
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>
get copyWith => throw _privateConstructorUsedError;
}
@ -1073,6 +1142,8 @@ class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -1101,6 +1172,8 @@ class _$SpotifyHomeFeedSectionItemCopyWithImpl<$Res,
) as $Val);
}
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$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
@pragma('vm:prefer-inline')
$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
@pragma('vm:prefer-inline')
$SpotifySectionAlbumCopyWith<$Res>? get album {
@ -1171,6 +1248,8 @@ class __$$SpotifyHomeFeedSectionItemImplCopyWithImpl<$Res>
$Res Function(_$SpotifyHomeFeedSectionItemImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1237,12 +1316,14 @@ class _$SpotifyHomeFeedSectionItemImpl implements _SpotifyHomeFeedSectionItem {
(identical(other.album, album) || other.album == album));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
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
@pragma('vm:prefer-inline')
_$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl>
@ -1276,8 +1357,11 @@ abstract class _SpotifyHomeFeedSectionItem
SpotifySectionArtist? get artist;
@override
SpotifySectionAlbum? get album;
/// Create a copy of SpotifyHomeFeedSectionItem
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifyHomeFeedSectionItemImplCopyWith<_$SpotifyHomeFeedSectionItemImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -1295,8 +1379,12 @@ mixin _$SpotifyHomeFeedSection {
List<SpotifyHomeFeedSectionItem> get items =>
throw _privateConstructorUsedError;
/// Serializes this SpotifyHomeFeedSection to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -1325,6 +1413,8 @@ class _$SpotifyHomeFeedSectionCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -1380,6 +1470,8 @@ class __$$SpotifyHomeFeedSectionImplCopyWithImpl<$Res>
$Res Function(_$SpotifyHomeFeedSectionImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1453,12 +1545,14 @@ class _$SpotifyHomeFeedSectionImpl implements _SpotifyHomeFeedSection {
const DeepCollectionEquality().equals(other._items, _items));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, typename, title, uri,
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
@pragma('vm:prefer-inline')
_$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl>
@ -1492,8 +1586,11 @@ abstract class _SpotifyHomeFeedSection implements SpotifyHomeFeedSection {
String get uri;
@override
List<SpotifyHomeFeedSectionItem> get items;
/// Create a copy of SpotifyHomeFeedSection
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifyHomeFeedSectionImplCopyWith<_$SpotifyHomeFeedSectionImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -1508,8 +1605,12 @@ mixin _$SpotifyHomeFeed {
List<SpotifyHomeFeedSection> get sections =>
throw _privateConstructorUsedError;
/// Serializes this SpotifyHomeFeed to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -1533,6 +1634,8 @@ class _$SpotifyHomeFeedCopyWithImpl<$Res, $Val extends SpotifyHomeFeed>
// ignore: unused_field
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')
@override
$Res call({
@ -1571,6 +1674,8 @@ class __$$SpotifyHomeFeedImplCopyWithImpl<$Res>
_$SpotifyHomeFeedImpl _value, $Res Function(_$SpotifyHomeFeedImpl) _then)
: super(_value, _then);
/// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1626,12 +1731,14 @@ class _$SpotifyHomeFeedImpl implements _SpotifyHomeFeed {
const DeepCollectionEquality().equals(other._sections, _sections));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
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
@pragma('vm:prefer-inline')
_$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith =>
@ -1659,8 +1766,11 @@ abstract class _SpotifyHomeFeed implements SpotifyHomeFeed {
String get greeting;
@override
List<SpotifyHomeFeedSection> get sections;
/// Create a copy of SpotifyHomeFeed
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpotifyHomeFeedImplCopyWith<_$SpotifyHomeFeedImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -24,7 +24,9 @@ mixin _$GeneratePlaylistProviderInput {
RecommendationSeeds? get min => 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>
get copyWith => throw _privateConstructorUsedError;
}
@ -62,6 +64,8 @@ class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
// ignore: unused_field
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')
@override
$Res call({
@ -105,6 +109,8 @@ class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
) as $Val);
}
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$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
@pragma('vm:prefer-inline')
$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
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get target {
@ -178,6 +188,8 @@ class __$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>
$Res Function(_$GeneratePlaylistProviderInputImpl) _then)
: super(_value, _then);
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -283,7 +295,9 @@ class _$GeneratePlaylistProviderInputImpl
min,
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
@pragma('vm:prefer-inline')
_$$GeneratePlaylistProviderInputImplCopyWith<
@ -317,8 +331,11 @@ abstract class _GeneratePlaylistProviderInput
RecommendationSeeds? get min;
@override
RecommendationSeeds? get target;
/// Create a copy of GeneratePlaylistProviderInput
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$GeneratePlaylistProviderInputImplCopyWith<
_$GeneratePlaylistProviderInputImpl>
get copyWith => throw _privateConstructorUsedError;
@ -347,8 +364,12 @@ mixin _$RecommendationSeeds {
num? get timeSignature => throw _privateConstructorUsedError;
num? get valence => throw _privateConstructorUsedError;
/// Serializes this RecommendationSeeds to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -386,6 +407,8 @@ class _$RecommendationSeedsCopyWithImpl<$Res, $Val extends RecommendationSeeds>
// ignore: unused_field
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')
@override
$Res call({
@ -498,6 +521,8 @@ class __$$RecommendationSeedsImplCopyWithImpl<$Res>
$Res Function(_$RecommendationSeedsImpl) _then)
: super(_value, _then);
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -665,7 +690,7 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
(identical(other.valence, valence) || other.valence == valence));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
@ -684,7 +709,9 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
timeSignature,
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
@pragma('vm:prefer-inline')
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
@ -749,8 +776,11 @@ abstract class _RecommendationSeeds implements RecommendationSeeds {
num? get timeSignature;
@override
num? get valence;
/// Create a copy of RecommendationSeeds
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
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 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.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/context.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/pages/library/local_folder.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
@ -28,8 +30,10 @@ class LocalFolderItem extends HookConsumerWidget {
final downloadFolder =
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation));
final cacheFolder = useFuture(UserPreferencesNotifier.getMusicCacheDir());
final isDownloadFolder = folder == downloadFolder;
final isCacheFolder = folder == cacheFolder.data;
final Uri(:pathSegments) = Uri.parse(
folder
@ -62,6 +66,7 @@ class LocalFolderItem extends HookConsumerWidget {
LocalLibraryPage.name,
queryParameters: {
if (isDownloadFolder) "downloads": "true",
if (isCacheFolder) "cache": "true",
},
extra: folder,
);
@ -123,6 +128,8 @@ class LocalFolderItem extends HookConsumerWidget {
child: Text(
isDownloadFolder
? context.l10n.downloads
: isCacheFolder
? context.l10n.cache_folder.capitalize()
: basename(folder),
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,

View File

@ -73,9 +73,7 @@ class UserAlbums extends HookConsumerWidget {
),
),
const SliverGap(10),
Skeletonizer.sliver(
enabled: albumsQuery.isLoading,
child: SliverLayoutBuilder(builder: (context, constrains) {
SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder(
itemCount: albums.isEmpty ? 6 : albums.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
@ -101,13 +99,15 @@ class UserAlbums extends HookConsumerWidget {
);
}
return AlbumCard(
return Skeletonizer(
enabled: albumsQuery.isLoading,
child: AlbumCard(
albums.elementAtOrNull(index) ?? FakeData.albumSimple,
),
);
},
);
}),
),
],
),
),

View File

@ -74,9 +74,7 @@ class UserArtists extends HookConsumerWidget {
),
),
const SliverGap(10),
Skeletonizer.sliver(
enabled: artistQuery.isLoading,
child: SliverLayoutBuilder(builder: (context, constrains) {
SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder(
itemCount: filteredArtists.isEmpty
? 6
@ -105,14 +103,16 @@ class UserArtists extends HookConsumerWidget {
);
}
return ArtistCard(
return Skeletonizer(
enabled: artistQuery.isLoading,
child: ArtistCard(
filteredArtists.elementAtOrNull(index) ??
FakeData.artist,
),
);
},
);
}),
),
],
),
),

View File

@ -30,6 +30,7 @@ class UserLocalTracks extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final cacheDir = useFuture(UserPreferencesNotifier.getMusicCacheDir());
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final preferences = ref.watch(userPreferencesProvider);
@ -83,11 +84,15 @@ class UserLocalTracks extends HookConsumerWidget {
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: preferences.localLibraryLocation.length + 1,
itemCount: preferences.localLibraryLocation.length +
1 +
(cacheDir.hasData ? 1 : 0),
itemBuilder: (context, index) {
return LocalFolderItem(
folder: index == 0
? preferences.downloadLocation
: 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/video_info.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/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -42,6 +43,17 @@ final sourceInfoToIconMap = {
),
),
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 {

View File

@ -17,6 +17,10 @@ final audioSourceToIconMap = {
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),
};
@ -45,6 +49,7 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
AudioSource.jiosaavn:
"${context.l10n.jiosaavn_source_description}\n"
"${context.l10n.highest_quality("320kbps mp")}",
AudioSource.invidious: context.l10n.invidious_source_description,
},
[]);
@ -104,7 +109,9 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
title: Align(
alignment: switch (preferences.audioSource) {
AudioSource.youtube => Alignment.centerLeft,
AudioSource.piped => Alignment.center,
AudioSource.piped ||
AudioSource.invidious =>
Alignment.center,
AudioSource.jiosaavn => Alignment.centerRight,
},
child: Text(

View File

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

View File

@ -1,4 +1,8 @@
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
@ -6,6 +10,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.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/components/expandable_search/expandable_search.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/provider/local_tracks/local_tracks_provider.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';
class LocalLibraryPage extends HookConsumerWidget {
@ -25,7 +32,13 @@ class LocalLibraryPage extends HookConsumerWidget {
final String location;
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(
WidgetRef ref,
@ -52,6 +65,8 @@ class LocalLibraryPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context);
final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(audioPlayerProvider);
final trackSnapshot = ref.watch(localTracksProvider);
@ -65,14 +80,133 @@ class LocalLibraryPage extends HookConsumerWidget {
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(
bottom: false,
child: Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
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,
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(
children: [

View File

@ -38,10 +38,11 @@ class LyricsPage extends HookConsumerWidget {
);
final palette = usePaletteColor(albumArt, ref);
final mediaQuery = MediaQuery.of(context);
final route = ModalRoute.of(context);
useCustomStatusBarColor(
final resetStatusBar = useCustomStatusBarColor(
palette.color,
true,
route?.isCurrent ?? false,
noSetBGColor: true,
);
@ -81,7 +82,10 @@ class LyricsPage extends HookConsumerWidget {
);
if (isModal) {
return DefaultTabController(
return PopScope(
canPop: true,
onPopInvokedWithResult: (_, __) => resetStatusBar(),
child: DefaultTabController(
length: 2,
child: SafeArea(
child: BackdropFilter(
@ -132,6 +136,7 @@ class LyricsPage extends HookConsumerWidget {
),
),
),
),
);
}
return DefaultTabController(

View File

@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.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/components/adaptive/adaptive_select_tile.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/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart';
class SettingsPlaybackSection extends HookConsumerWidget {
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(
duration: const Duration(milliseconds: 300),
child: preferences.audioSource != AudioSource.piped
@ -159,7 +229,8 @@ class SettingsPlaybackSection extends HookConsumerWidget {
duration: const Duration(milliseconds: 300),
child: preferences.searchMode == SearchMode.youtube &&
(preferences.audioSource == AudioSource.piped ||
preferences.audioSource == AudioSource.youtube)
preferences.audioSource == AudioSource.youtube ||
preferences.audioSource == AudioSource.invidious)
? SwitchListTile(
secondary: const Icon(SpotubeIcons.skip),
title: Text(context.l10n.skip_non_music),
@ -170,6 +241,30 @@ class SettingsPlaybackSection extends HookConsumerWidget {
)
: 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(
leading: const Icon(SpotubeIcons.playlistRemove),
title: Text(context.l10n.blacklist),

View File

@ -247,7 +247,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
// 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) {
return addTracks(tracks);
}
@ -257,7 +260,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
for (int i = 0; i < tracks.length; i++) {
final track = tracks.elementAt(i);
if (state.tracks.any((element) => _compareTracks(element, track))) {
if (!allowDuplicates &&
state.tracks.any((element) => _compareTracks(element, track))) {
continue;
}

View File

@ -74,6 +74,7 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPlaylist() {
return audioPlayer.playlistStream.listen((mpvPlaylist) {
try {
if (audioPlayerState.activeTrack == null) return;
notificationService.addTrack(audioPlayerState.activeTrack!);
discord.updatePresence(audioPlayerState.activeTrack!);
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:io';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:metadata_god/metadata_god.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/sourced_track.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart';
class DownloadManagerProvider extends ChangeNotifier {
DownloadManagerProvider({required this.ref})
@ -53,33 +54,16 @@ class DownloadManagerProvider extends ChangeNotifier {
await oldFile.delete();
}
final imageBytes = await downloadImage(
final imageBytes = await ServiceUtils.downloadImage(
(track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
index: 1,
),
);
final metadata = Metadata(
title: track.name,
artist: track.artists?.map((a) => a.name).join(", "),
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,
final metadata = track.toMetadata(
fileLength: await file.length(),
imageBytes: imageBytes,
);
await MetadataGod.writeMetadata(
@ -116,29 +100,6 @@ class DownloadManagerProvider extends ChangeNotifier {
final Set<Track> $backHistory;
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) {
final name =
"${track.name} - ${track.artists?.asString() ?? ""}.${downloadCodec.name}";

View File

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

View File

@ -31,7 +31,7 @@ class ActiveSourcedTrackNotifier extends Notifier<SourcedTrack?> {
final playbackNotifier = ref.read(audioPlayerProvider.notifier);
final oldActiveIndex = audioPlayer.currentIndex;
await playbackNotifier.addTracksAtFirst([newTrack]);
await playbackNotifier.addTracksAtFirst([newTrack], allowDuplicates: true);
await Future.delayed(const Duration(milliseconds: 50));
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' as dio_lib;
import 'package:flutter/foundation.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: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/state.dart';
import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.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 {
final Ref ref;
@ -18,6 +31,137 @@ class ServerPlaybackRoutes {
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>')
Future<Response> getStreamTrackId(Request request, String trackId) async {
try {
@ -31,36 +175,12 @@ class ServerPlaybackRoutes {
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
final res = await dio.get(
sourcedTrack!.url,
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,
);
final (bytes: audioBytes, response: res) =
await streamTrack(sourcedTrack!, request.headers);
return Response(
res.statusCode!,
body: audioStream,
context: {
"shelf.io.buffer_output": false,
},
body: audioBytes,
headers: res.headers.map,
);
} catch (e, stack) {

View File

@ -5,8 +5,10 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
final sourcedTrackProvider =
FutureProvider.family<SourcedTrack?, SpotubeMedia?>((ref, media) async {
class SourcedTrackNotifier
extends FamilyAsyncNotifier<SourcedTrack?, SpotubeMedia?> {
@override
build(media) async {
final track = media?.track;
if (track == null || track is LocalTrack) {
return null;
@ -25,4 +27,22 @@ final sourcedTrackProvider =
await SourcedTrack.fetchFromTrack(track: track, ref: ref);
return sourcedTrack;
}
Future<SourcedTrack?> switchToAlternativeSources() async {
if (arg == null) {
return null;
}
return await update((prev) async {
return await SourcedTrack.fetchFromTrackAltSource(
track: arg!.track,
ref: ref,
);
});
}
}
final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
SourcedTrack?, SpotubeMedia?>(
() => SourcedTrackNotifier(),
);

View File

@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:spotube/models/database/database.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/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
import 'package:open_file/open_file.dart';
typedef UserPreferences = PreferencesTableData;
@ -71,10 +72,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
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");
});
}
@ -95,6 +96,30 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
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) {
setData(PreferencesTableCompanion(streamMusicCodec: Value(codec)));
}
@ -167,6 +192,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
setData(PreferencesTableCompanion(pipedInstance: Value(instance)));
}
void setInvidiousInstance(String instance) {
setData(PreferencesTableCompanion(invidiousInstance: Value(instance)));
}
void setSearchMode(SearchMode mode) {
setData(PreferencesTableCompanion(searchMode: Value(mode)));
}
@ -207,6 +236,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
void setEnableConnect(bool enable) {
setData(PreferencesTableCompanion(enableConnect: Value(enable)));
}
void setCacheMusic(bool cache) {
setData(PreferencesTableCompanion(cacheMusic: Value(cache)));
}
}
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/services/audio_player/audio_player.dart';
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/utils/platform.dart';
@ -59,6 +60,9 @@ class MobileAudioService extends BaseAudioHandler {
});
});
audioPlayer.playerStateStream.listen((state) async {
if (state == AudioPlaybackState.playing) {
await session?.setActive(true);
}
playbackState.add(await _transformEvent());
});

View File

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

View File

@ -12,7 +12,7 @@ SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo(
artist: json['artist'] as String,
thumbnail: json['thumbnail'] 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,
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:spotube/models/database/database.dart';
@ -112,4 +113,24 @@ class YoutubeVideoInfo {
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/models/source_info.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/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -85,6 +86,13 @@ abstract class SourcedTrack extends Track {
sourceInfo: sourceInfo,
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(", ")}";
}
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({
required Track track,
required Ref ref,
@ -117,11 +168,14 @@ abstract class SourcedTrack extends Track {
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.jiosaavn =>
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.invidious =>
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
};
} on TrackNotFoundError catch (_) {
return switch (preferences.audioSource) {
AudioSource.piped ||
AudioSource.youtube =>
AudioSource.youtube ||
AudioSource.invidious =>
await JioSaavnSourcedTrack.fetchFromTrack(
track: track,
ref: ref,
@ -136,11 +190,19 @@ abstract class SourcedTrack extends Track {
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
} catch (e) {
if (e is DioException || e is ClientException || e is SocketException) {
return await JioSaavnSourcedTrack.fetchFromTrack(
return switch (preferences.audioSource) {
AudioSource.piped ||
AudioSource.invidious =>
await YoutubeSourcedTrack.fetchFromTrack(
track: track,
ref: ref,
),
_ => await JioSaavnSourcedTrack.fetchFromTrack(
track: track,
ref: ref,
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
);
)
};
}
rethrow;
}
@ -159,6 +221,8 @@ abstract class SourcedTrack extends Track {
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
AudioSource.jiosaavn =>
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 enableConnect => throw _privateConstructorUsedError;
/// Serializes this UserPreferences to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -110,6 +114,8 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
// ignore: unused_field
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')
@override
$Res call({
@ -290,6 +296,8 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
_$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then)
: super(_value, _then);
/// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -605,7 +613,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
other.enableConnect == enableConnect));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([
runtimeType,
@ -635,7 +643,9 @@ class _$UserPreferencesImpl implements _UserPreferences {
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
@pragma('vm:prefer-inline')
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
@ -744,8 +754,11 @@ abstract class _UserPreferences implements UserPreferences {
bool get endlessPlayback;
@override
bool get enableConnect;
/// Create a copy of UserPreferences
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@ -812,8 +825,13 @@ mixin _$PlaybackHistoryItem {
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
/// Serializes this PlaybackHistoryItem to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -837,6 +855,8 @@ class _$PlaybackHistoryItemCopyWithImpl<$Res, $Val extends PlaybackHistoryItem>
// ignore: unused_field
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')
@override
$Res call({
@ -873,6 +893,8 @@ class __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>
$Res Function(_$PlaybackHistoryPlaylistImpl) _then)
: super(_value, _then);
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -925,11 +947,13 @@ class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist {
other.playlist == playlist));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
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
@pragma('vm:prefer-inline')
_$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl>
@ -1023,8 +1047,11 @@ abstract class PlaybackHistoryPlaylist implements PlaybackHistoryItem {
@override
DateTime get date;
PlaylistSimple get playlist;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -1048,6 +1075,8 @@ class __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>
$Res Function(_$PlaybackHistoryAlbumImpl) _then)
: super(_value, _then);
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1099,11 +1128,13 @@ class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum {
(identical(other.album, album) || other.album == album));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
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
@pragma('vm:prefer-inline')
_$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl>
@ -1198,8 +1229,11 @@ abstract class PlaybackHistoryAlbum implements PlaybackHistoryItem {
@override
DateTime get date;
AlbumSimple get album;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl>
get copyWith => throw _privateConstructorUsedError;
}
@ -1223,6 +1257,8 @@ class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>
$Res Function(_$PlaybackHistoryTrackImpl) _then)
: super(_value, _then);
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -1274,11 +1310,13 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
(identical(other.track, track) || other.track == track));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
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
@pragma('vm:prefer-inline')
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
@ -1373,8 +1411,11 @@ abstract class PlaybackHistoryTrack implements PlaybackHistoryItem {
@override
DateTime get date;
Track get track;
/// Create a copy of PlaybackHistoryItem
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -1,4 +1,7 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:go_router/go_router.dart';
import 'package:html/dom.dart' hide Text;
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/provider/database/database.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/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 <local_notifier/local_notifier_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 <system_theme/system_theme_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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

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