Compare commits
29 Commits
e0c32a5722
...
eb2e3ae777
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2e3ae777 | ||
|
|
bbe3394e9e | ||
|
|
7cde803bee | ||
|
|
cd475e93d0 | ||
|
|
3d334d96fd | ||
|
|
bd4cd22e4e | ||
|
|
c709de6bf1 | ||
|
|
ccbac85171 | ||
|
|
50123b235c | ||
|
|
4072531c62 | ||
|
|
4db9a95a91 | ||
|
|
3cce2868de | ||
|
|
894b0d7e5e | ||
|
|
7befbca8e5 | ||
|
|
180d07a1be | ||
|
|
91871d0d26 | ||
|
|
c3bbc129ad | ||
|
|
a9586a64f2 | ||
|
|
677f95f266 | ||
|
|
f4b1e550bf | ||
|
|
bb71fc0eea | ||
|
|
7eb0e69dd7 | ||
|
|
30bf0bed62 | ||
|
|
c82b68a513 | ||
|
|
ccf84c568e | ||
|
|
fdb5ed8f56 | ||
|
|
55871e3cdd | ||
|
|
0aa44520ac | ||
|
|
7571f880ec |
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutterSdkVersion": "3.29.0"
|
"flutterSdkVersion": "3.29.1"
|
||||||
}
|
}
|
||||||
2
.fvmrc
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.29.0",
|
"flutter": "3.29.1",
|
||||||
"flavors": {}
|
"flavors": {}
|
||||||
}
|
}
|
||||||
2
.github/workflows/pr-lint.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: 3.29.0
|
FLUTTER_VERSION: 3.29.1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
2
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Version to publish (x.x.x)
|
description: Version to publish (x.x.x)
|
||||||
default: 3.8.3
|
default: 4.0.0
|
||||||
required: true
|
required: true
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Dry run
|
description: Dry run
|
||||||
|
|||||||
2
.github/workflows/spotube-release-binary.yml
vendored
@ -20,7 +20,7 @@ on:
|
|||||||
description: Dry run without uploading to release
|
description: Dry run without uploading to release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: 3.29.0
|
FLUTTER_VERSION: 3.29.1
|
||||||
FLUTTER_CHANNEL: master
|
FLUTTER_CHANNEL: master
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
@ -28,5 +28,5 @@
|
|||||||
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
||||||
"*.dart": "${capture}.g.dart,${capture}.freezed.dart"
|
"*.dart": "${capture}.g.dart,${capture}.freezed.dart"
|
||||||
},
|
},
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.29.0"
|
"dart.flutterSdkPath": ".fvm/versions/3.29.1"
|
||||||
}
|
}
|
||||||
1142
CHANGELOG.md
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
BSD-4-Clause License
|
BSD-4-Clause License
|
||||||
|
|
||||||
Copyright (c) 2023 Kingkor Roy Tirtho. All rights reserved.
|
Copyright (c) 2025 Kingkor Roy Tirtho. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|||||||
38
README.md
@ -110,7 +110,7 @@ This handy table lists all the methods you can use to install Spotube:
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>AppImage</td>
|
<td>AppImage</td>
|
||||||
<td>AppImage's lacking stability led to it's temporal removal. More information at https://github.com/KRTirtho/spotube/issues/1082</td>
|
<td>AppImage's lacking stability led to it's temporary removal. More information at https://github.com/KRTirtho/spotube/issues/1082</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Debian/Ubuntu</td>
|
<td>Debian/Ubuntu</td>
|
||||||
@ -207,10 +207,15 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
### Services
|
### Services
|
||||||
|
|
||||||
1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
|
1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
|
||||||
|
1. [MPV](https://mpv.io) - mpv is a free (as in freedom) media player for the command line. It supports a wide variety of media file formats, audio and video codecs, and subtitle types.
|
||||||
1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data
|
1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data
|
||||||
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
|
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
|
||||||
|
1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube.
|
||||||
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
|
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
|
||||||
|
1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader
|
||||||
|
1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites
|
||||||
1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
|
1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
|
||||||
1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
|
1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
|
||||||
1. [LRCLib](https://lrclib.net/) - A public synced lyric API
|
1. [LRCLib](https://lrclib.net/) - A public synced lyric API
|
||||||
@ -223,21 +228,20 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
||||||
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
|
1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.
|
||||||
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
|
1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library.
|
||||||
1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off.
|
1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off.
|
||||||
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
||||||
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
|
1. [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
|
||||||
|
1. [auto_route](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you.
|
||||||
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
|
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
|
||||||
1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
|
1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
|
||||||
1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
|
|
||||||
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
|
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
|
||||||
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
1. [connectivity_plus](https://github.com/fluttercommunity/plus_plugins) - Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS.
|
||||||
1. [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. [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. [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. [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. [drift](https://drift.simonbinder.eu/) - Drift is a reactive library to store relational data in Dart and Flutter applications.
|
1. [drift](https://drift.simonbinder.eu/) - Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||||
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
|
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
|
||||||
1. [encrypt](https://pub.dev/packages/encrypt) - A set of high-level APIs over PointyCastle for two-way cryptography.
|
1. [encrypt](https://pub.dev/packages/encrypt) - A set of high-level APIs over PointyCastle for two-way cryptography.
|
||||||
@ -245,26 +249,25 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
|
1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
|
||||||
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
||||||
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
||||||
1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
|
|
||||||
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
||||||
1. [flutter_discord_rpc](https://pub.dev/packages/flutter_discord_rpc) - Discord RPC support for Flutter desktop platforms
|
1. [flutter_discord_rpc](https://pub.dev/packages/flutter_discord_rpc) - Discord RPC support for Flutter desktop platforms
|
||||||
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
||||||
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
|
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
|
||||||
|
1. [flutter_form_builder](https://github.com/flutter-form-builder-ecosystem) - This package helps in creation of forms in Flutter by removing the boilerplate code, reusing validation, react to changes, and collect final user input.
|
||||||
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
|
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
|
||||||
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
|
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
|
||||||
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
|
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
|
||||||
1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
||||||
1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
|
1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
|
||||||
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
|
1. [flutter_undraw](https://github.com/KRTirtho/flutter_undraw) - Undraw.co Illustrations for Flutter with customization options
|
||||||
|
1. [form_builder_validators](https://github.com/flutter-form-builder-ecosystem) - Form Builder Validators set of validators for FlutterFormBuilder. Provides common validators and a way to make your own.
|
||||||
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
||||||
1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
|
1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
|
||||||
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
|
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
|
||||||
1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
|
1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views.
|
||||||
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
|
|
||||||
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
||||||
1. [hive](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. [home_widget](https://pub.dev/packages/home_widget) - A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS.
|
||||||
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
|
|
||||||
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
|
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
|
||||||
1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
|
1. [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.
|
||||||
@ -276,6 +279,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
|
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
|
||||||
1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications.
|
1. [local_notifier](https://github.com/leanflutter/local_notifier) - This plugin allows Flutter desktop apps to displaying local notifications.
|
||||||
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
|
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
|
||||||
|
1. [logging](https://pub.dev/packages/logging) - Provides APIs for debugging and error logging, similar to loggers in other languages, such as the Closure JS Logger and java.util.logging.Logger.
|
||||||
1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
|
1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
|
||||||
1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
||||||
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
|
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
|
||||||
@ -288,16 +292,16 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
||||||
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
||||||
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
||||||
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
|
|
||||||
1. [riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
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. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
|
||||||
|
1. [shadcn_flutter](https://github.com/sunarya-thito/shadcn_flutter) - Beautifully designed components from Shadcn/UI is now available for Flutter
|
||||||
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
|
1. [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](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
|
||||||
1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
|
1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
|
||||||
1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
|
1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
|
||||||
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
|
|
||||||
1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
|
1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands.
|
||||||
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
||||||
|
1. [sliding_up_panel](https://github.com/akshathjain/sliding_up_panel) - A draggable Flutter widget that makes implementing a SlidingUpPanel much easier!
|
||||||
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
||||||
1. [smtc_windows](https://pub.dev/packages/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
1. [smtc_windows](https://pub.dev/packages/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
||||||
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
|
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
|
||||||
@ -319,26 +323,30 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
||||||
1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
|
1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window.
|
||||||
1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
||||||
|
1. [http_parser](https://pub.dev/packages/http_parser) - A platform-independent package for parsing and serializing HTTP formats.
|
||||||
|
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
||||||
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
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. [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. [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_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_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_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. [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. [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. [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. [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. [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. [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. [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. [drift_dev](https://drift.simonbinder.eu/) - Dev-dependency for users of drift. Contains the generator and development tools.
|
||||||
|
1. [auto_route_generator](https://github.com/Milad-Akarie/auto_route_library) - AutoRoute is a declarative routing solution, where everything needed for navigation is automatically generated for you.
|
||||||
1. [desktop_webview_window](https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window) - Show a webview window on your flutter desktop application.
|
1. [desktop_webview_window](https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_webview_window) - Show a webview window on your flutter desktop application.
|
||||||
|
1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc
|
||||||
1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
|
1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
|
||||||
|
1. [flutter_broadcasts](https://github.com/KRTirtho/flutter_broadcasts.git) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
|
||||||
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
|
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
|
||||||
|
1. [yt_dlp_dart](https://github.com/KRTirtho/yt_dlp_dart.git) - yt-dlp binding in Dart
|
||||||
|
1. [flutter_new_pipe_extractor](https://github.com/KRTirtho/flutter_new_pipe_extractor) - NewPipeExtractor binding for Flutter (Android only)
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
<div align="center"><h4>© Copyright Spotube 2024</h4></div>
|
||||||
|
|||||||
@ -25,9 +25,9 @@
|
|||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
<!-- Enable Impeller -->
|
<!-- Enable Impeller -->
|
||||||
<!-- <meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
android:value="false" /> -->
|
android:value="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 39 KiB |
BIN
assets/mobile-screenshots/android-6.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 750 KiB After Width: | Height: | Size: 1006 KiB |
@ -1,8 +1,8 @@
|
|||||||
pkgbase = spotube-bin
|
pkgbase = spotube-bin
|
||||||
pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
|
pkgdesc = Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!
|
||||||
pkgver = 3.7.1
|
pkgver = 4.0.0
|
||||||
pkgrel = 2
|
pkgrel = 1
|
||||||
url = https://github.com/KRTirtho/spotube/
|
url = https://spotube.krtirtho.dev
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
license = BSD-4-Clause
|
license = BSD-4-Clause
|
||||||
depends = mpv
|
depends = mpv
|
||||||
|
|||||||
@ -5,7 +5,7 @@ pkgrel=%{{PKGREL}}%
|
|||||||
epoch=
|
epoch=
|
||||||
pkgdesc="Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!"
|
pkgdesc="Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile!"
|
||||||
arch=(x86_64)
|
arch=(x86_64)
|
||||||
url="https://github.com/KRTirtho/spotube/"
|
url="https://spotube.krtirtho.dev"
|
||||||
license=('BSD-4-Clause')
|
license=('BSD-4-Clause')
|
||||||
groups=()
|
groups=()
|
||||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter
|
||||||
|
enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<!-- == PACKAGE SPECIFIC SECTION == -->
|
<!-- == PACKAGE SPECIFIC SECTION == -->
|
||||||
@ -12,17 +13,19 @@
|
|||||||
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
||||||
<title>spotube (Install)</title>
|
<title>spotube (Install)</title>
|
||||||
<authors>Kingkor Roy Tirtho</authors>
|
<authors>Kingkor Roy Tirtho</authors>
|
||||||
<projectUrl>https://github.com/KRTirtho/spotube/</projectUrl>
|
<projectUrl>https://spotube.krtirtho.dev</projectUrl>
|
||||||
<iconUrl>https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
|
<iconUrl>
|
||||||
|
https://rawcdn.githack.com/KRTirtho/spotube/7edb0bb834eb18c05551e30a891720a6abf53dbe/assets/spotube-logo.png</iconUrl>
|
||||||
<copyright>2022 Spotube</copyright>
|
<copyright>2022 Spotube</copyright>
|
||||||
<!-- If there is a license Url available, it is required for the community feed -->
|
<!-- If there is a license Url available, it is required for the community feed -->
|
||||||
<licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl>
|
<licenseUrl>https://github.com/KRTirtho/spotube/blob/master/LICENSE</licenseUrl>
|
||||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||||
<projectSourceUrl>https://github.com/KRTirtho/spotube</projectSourceUrl>
|
<projectSourceUrl>https://github.com/KRTirtho/spotube</projectSourceUrl>
|
||||||
<docsUrl>https://github.com/KRTirtho/spotube#readme</docsUrl>
|
<docsUrl>https://spotube.krtirtho.dev</docsUrl>
|
||||||
<bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl>
|
<bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl>
|
||||||
<tags>spotube music audio spotify youtube flutter</tags>
|
<tags>spotube music audio spotify youtube flutter</tags>
|
||||||
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available for both desktop & mobile! </summary>
|
<summary>🎧 Open source Spotify client that doesn't require Premium nor uses Electron! Available
|
||||||
|
for both desktop & mobile! </summary>
|
||||||
<description>
|
<description>
|
||||||
Spotube is a Flutter based lightweight spotify client. It utilizes the power
|
Spotube is a Flutter based lightweight spotify client. It utilizes the power
|
||||||
of Spotify & Youtube's public API & creates a hazardless, performant & resource
|
of Spotify & Youtube's public API & creates a hazardless, performant & resource
|
||||||
@ -37,7 +40,10 @@
|
|||||||
- Native performance (Thanks to Flutter+Skia)
|
- Native performance (Thanks to Flutter+Skia)
|
||||||
- Playback control is done locally instead of on the server
|
- Playback control is done locally instead of on the server
|
||||||
- Small size & less data usage
|
- Small size & less data usage
|
||||||
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support their valuable creations.)
|
- No Spotify or YouTube ads since it uses all public & free APIs (It is still recommended
|
||||||
|
to support the creators by watching/liking/subscribing to the artists' YouTube channels or
|
||||||
|
liking their tracks on Spotify. Purchasing Spotify Premium is usually the best way to support
|
||||||
|
their valuable creations.)
|
||||||
- Time synced lyrics
|
- Time synced lyrics
|
||||||
- Downloadable tracks
|
- Downloadable tracks
|
||||||
</description>
|
</description>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
BSD 4-Clause License
|
BSD 4-Clause License
|
||||||
|
|
||||||
Copyright (c) 2022 Kingkor Roy Tirtho. All rights reserved.
|
Copyright (c) 2025 Kingkor Roy Tirtho. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|||||||
@ -75,17 +75,17 @@ class AppRouter extends RootStackRouter {
|
|||||||
path: "local",
|
path: "local",
|
||||||
page: UserLocalLibraryRoute.page,
|
page: UserLocalLibraryRoute.page,
|
||||||
),
|
),
|
||||||
AutoRoute(
|
|
||||||
path: "local/folder",
|
|
||||||
page: LocalLibraryRoute.page,
|
|
||||||
// parentNavigatorKey: shellRouteNavigatorKey,
|
|
||||||
),
|
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "downloads",
|
path: "downloads",
|
||||||
page: UserDownloadsRoute.page,
|
page: UserDownloadsRoute.page,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "local/folder",
|
||||||
|
page: LocalLibraryRoute.page,
|
||||||
|
// parentNavigatorKey: shellRouteNavigatorKey,
|
||||||
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "library/generate",
|
path: "library/generate",
|
||||||
page: PlaylistGeneratorRoute.page,
|
page: PlaylistGeneratorRoute.page,
|
||||||
@ -190,6 +190,27 @@ class AppRouter extends RootStackRouter {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
CustomRoute(
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
path: "/player/queue",
|
||||||
|
page: PlayerQueueRoute.page,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
path: "/player/sources",
|
||||||
|
page: PlayerTrackSourcesRoute.page,
|
||||||
|
),
|
||||||
|
CustomRoute(
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
durationInMilliseconds: 200,
|
||||||
|
reverseDurationInMilliseconds: 200,
|
||||||
|
path: "/player/lyrics",
|
||||||
|
page: PlayerLyricsRoute.page,
|
||||||
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "/mini-player",
|
path: "/mini-player",
|
||||||
page: MiniLyricsRoute.page,
|
page: MiniLyricsRoute.page,
|
||||||
|
|||||||
@ -28,7 +28,7 @@ class AdaptiveMenuButton<T> extends MenuButton {
|
|||||||
/// or equal to 640px
|
/// or equal to 640px
|
||||||
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
|
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
|
||||||
class AdaptivePopSheetList<T> extends StatelessWidget {
|
class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||||
final List<AdaptiveMenuButton<T>> children;
|
final List<AdaptiveMenuButton<T>> Function(BuildContext context) items;
|
||||||
final Widget? icon;
|
final Widget? icon;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final bool useRootNavigator;
|
final bool useRootNavigator;
|
||||||
@ -43,7 +43,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
|
|
||||||
const AdaptivePopSheetList({
|
const AdaptivePopSheetList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.children,
|
required this.items,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.child,
|
this.child,
|
||||||
this.useRootNavigator = true,
|
this.useRootNavigator = true,
|
||||||
@ -59,7 +59,8 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
|
|
||||||
Future<void> showDropdownMenu(BuildContext context, Offset position) async {
|
Future<void> showDropdownMenu(BuildContext context, Offset position) async {
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final childrenModified = children.map((s) {
|
List<MenuButton> childrenModified(BuildContext context) =>
|
||||||
|
items(context).map((s) {
|
||||||
if (s.onPressed == null) {
|
if (s.onPressed == null) {
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
key: s.key,
|
key: s.key,
|
||||||
@ -92,7 +93,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
position: position,
|
position: position,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return DropdownMenu(
|
return DropdownMenu(
|
||||||
children: childrenModified,
|
children: childrenModified(context),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
@ -109,11 +110,12 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
backgroundColor: context.theme.colorScheme.card,
|
backgroundColor: context.theme.colorScheme.card,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
final children = childrenModified(context);
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: childrenModified.length,
|
itemCount: children.length,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final data = childrenModified[index];
|
final data = children[index];
|
||||||
|
|
||||||
return Button(
|
return Button(
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
final Widget title;
|
final Widget title;
|
||||||
final Widget? subtitle;
|
final Widget? subtitle;
|
||||||
final Widget? secondary;
|
final Widget? secondary;
|
||||||
|
final List<Widget>? trailing;
|
||||||
final ListTileControlAffinity? controlAffinity;
|
final ListTileControlAffinity? controlAffinity;
|
||||||
final T value;
|
final T value;
|
||||||
final ValueChanged<T?>? onChanged;
|
final ValueChanged<T?>? onChanged;
|
||||||
@ -34,6 +35,7 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
this.controlAffinity = ListTileControlAffinity.trailing,
|
this.controlAffinity = ListTileControlAffinity.trailing,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.secondary,
|
this.secondary,
|
||||||
|
this.trailing,
|
||||||
this.breakLayout,
|
this.breakLayout,
|
||||||
this.showValueWhenUnfolded = true,
|
this.showValueWhenUnfolded = true,
|
||||||
super.key,
|
super.key,
|
||||||
@ -54,7 +56,18 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
popupConstraints: popupConstraints ?? const BoxConstraints(maxWidth: 200),
|
popupConstraints: popupConstraints ?? const BoxConstraints(maxWidth: 200),
|
||||||
popupWidthConstraint: popupWidthConstraint ?? PopoverConstraint.flexible,
|
popupWidthConstraint: popupWidthConstraint ?? PopoverConstraint.flexible,
|
||||||
children: options,
|
autoClosePopover: true,
|
||||||
|
popup: (context) {
|
||||||
|
return SelectPopup(
|
||||||
|
autoClose: true,
|
||||||
|
items: SelectItemBuilder(
|
||||||
|
childCount: options.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
return options[index];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mediaQuery.smAndDown) {
|
if (mediaQuery.smAndDown) {
|
||||||
@ -73,9 +86,20 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
leading: controlAffinity != ListTileControlAffinity.leading
|
leading: controlAffinity != ListTileControlAffinity.leading
|
||||||
? secondary
|
? secondary
|
||||||
: control,
|
: control,
|
||||||
trailing: controlAffinity == ListTileControlAffinity.leading
|
trailing: Row(
|
||||||
? secondary
|
mainAxisSize: MainAxisSize.min,
|
||||||
: control,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
...?trailing,
|
||||||
|
if (controlAffinity == ListTileControlAffinity.leading &&
|
||||||
|
secondary != null)
|
||||||
|
secondary!
|
||||||
|
else if (controlAffinity == ListTileControlAffinity.trailing &&
|
||||||
|
control != null)
|
||||||
|
control,
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: breakLayout ?? mediaQuery.mdAndUp
|
onTap: breakLayout ?? mediaQuery.mdAndUp
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
|
|
||||||
class AnimateGradient extends HookWidget {
|
|
||||||
const AnimateGradient({
|
|
||||||
super.key,
|
|
||||||
required this.primaryColors,
|
|
||||||
required this.secondaryColors,
|
|
||||||
this.child,
|
|
||||||
this.primaryBegin,
|
|
||||||
this.primaryEnd,
|
|
||||||
this.secondaryBegin,
|
|
||||||
this.secondaryEnd,
|
|
||||||
AnimationController? controller,
|
|
||||||
this.duration = const Duration(seconds: 4),
|
|
||||||
this.animateAlignments = true,
|
|
||||||
this.reverse = true,
|
|
||||||
}) : assert(primaryColors.length >= 2),
|
|
||||||
assert(primaryColors.length == secondaryColors.length),
|
|
||||||
_controller = controller;
|
|
||||||
|
|
||||||
/// [controller]: pass this to have a fine control over the [Animation]
|
|
||||||
final AnimationController? _controller;
|
|
||||||
|
|
||||||
/// [duration]: Time to switch between [Gradient].
|
|
||||||
/// By default its value is [Duration(seconds:4)]
|
|
||||||
final Duration duration;
|
|
||||||
|
|
||||||
/// [primaryColors]: These will be the starting colors of the [Animation].
|
|
||||||
final List<Color> primaryColors;
|
|
||||||
|
|
||||||
/// [secondaryColors]: These Colors are those in which the [primaryColors] will transition into.
|
|
||||||
final List<Color> secondaryColors;
|
|
||||||
|
|
||||||
/// [primaryBegin]: This is begin [Alignment] for [primaryColors].
|
|
||||||
/// By default its value is [Alignment.topLeft]
|
|
||||||
final Alignment? primaryBegin;
|
|
||||||
|
|
||||||
/// [primaryBegin]: This is end [Alignment] for [primaryColors].
|
|
||||||
/// By default its value is [Alignment.topRight]
|
|
||||||
final Alignment? primaryEnd;
|
|
||||||
|
|
||||||
/// [secondaryBegin]: This is begin [Alignment] for [secondaryColors].
|
|
||||||
/// By default its value is [Alignment.bottomLeft]
|
|
||||||
final Alignment? secondaryBegin;
|
|
||||||
|
|
||||||
/// [secondaryEnd]: This is end [Alignment] for [secondaryColors].
|
|
||||||
/// By default its value is [Alignment.bottomRight]
|
|
||||||
final Alignment? secondaryEnd;
|
|
||||||
|
|
||||||
/// [animateAlignments]: set to false if you don't want to animate the alignments.
|
|
||||||
/// This can provide you way cooler animations
|
|
||||||
final bool animateAlignments;
|
|
||||||
|
|
||||||
/// [reverse]: set it to false if you don't want to reverse the animation.
|
|
||||||
/// using that it will go into one direction only
|
|
||||||
final bool reverse;
|
|
||||||
|
|
||||||
final Widget? child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// ignore: no_leading_underscores_for_local_identifiers
|
|
||||||
final __controller = useAnimationController(
|
|
||||||
duration: duration,
|
|
||||||
)..repeat(reverse: reverse);
|
|
||||||
|
|
||||||
final controller = _controller ?? __controller;
|
|
||||||
|
|
||||||
final animation = useMemoized(
|
|
||||||
() => CurvedAnimation(
|
|
||||||
parent: controller,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
),
|
|
||||||
[controller]);
|
|
||||||
|
|
||||||
final colorTween = useMemoized(
|
|
||||||
() => primaryColors.map((color) {
|
|
||||||
return ColorTween(
|
|
||||||
begin: color,
|
|
||||||
end: color,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
[primaryColors]);
|
|
||||||
final colors = useMemoized(
|
|
||||||
() => colorTween.map((color) {
|
|
||||||
return color.evaluate(animation)!;
|
|
||||||
}).toList(),
|
|
||||||
[colorTween, animation]);
|
|
||||||
|
|
||||||
final begin = useMemoized(
|
|
||||||
() => AlignmentTween(
|
|
||||||
begin: primaryBegin ?? Alignment.topLeft,
|
|
||||||
end: primaryEnd ?? Alignment.topRight,
|
|
||||||
),
|
|
||||||
[primaryBegin, primaryEnd]);
|
|
||||||
|
|
||||||
final end = useMemoized(
|
|
||||||
() => AlignmentTween(
|
|
||||||
begin: secondaryBegin ?? Alignment.bottomLeft,
|
|
||||||
end: secondaryEnd ?? Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
[secondaryBegin, secondaryEnd]);
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: animation,
|
|
||||||
child: useMemoized(() => child, [child]),
|
|
||||||
builder: (BuildContext context, Widget? child) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: animateAlignments
|
|
||||||
? begin.evaluate(animation)
|
|
||||||
: (primaryBegin as Alignment),
|
|
||||||
end: animateAlignments
|
|
||||||
? end.evaluate(animation)
|
|
||||||
: primaryEnd as Alignment,
|
|
||||||
colors: colors,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,18 +3,18 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
|
|
||||||
class BackButton extends StatelessWidget {
|
class BackButton extends StatelessWidget {
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
final IconData icon;
|
||||||
const BackButton({
|
const BackButton({
|
||||||
super.key,
|
super.key,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.icon = SpotubeIcons.angleLeft,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton.ghost(
|
return IconButton.ghost(
|
||||||
size: const ButtonSize(.9),
|
size: const ButtonSize(.9),
|
||||||
icon: color != null
|
icon: Icon(icon, color: color),
|
||||||
? Icon(SpotubeIcons.angleLeft, color: color)
|
|
||||||
: const Icon(SpotubeIcons.angleLeft),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ class TextFormBuilderField extends StatelessWidget {
|
|||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
final bool filled;
|
final bool filled;
|
||||||
final Widget? placeholder;
|
final Widget? placeholder;
|
||||||
final AlignmentGeometry? placeholderAlignment;
|
// final AlignmentGeometry? placeholderAlignment;
|
||||||
final AlignmentGeometry? leadingAlignment;
|
// final AlignmentGeometry? leadingAlignment;
|
||||||
final AlignmentGeometry? trailingAlignment;
|
// final AlignmentGeometry? trailingAlignment;
|
||||||
final bool border;
|
final bool border;
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
@ -41,9 +41,9 @@ class TextFormBuilderField extends StatelessWidget {
|
|||||||
final void Function(PointerDownEvent event)? onTapOutside;
|
final void Function(PointerDownEvent event)? onTapOutside;
|
||||||
final List<TextInputFormatter>? inputFormatters;
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
// final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||||
final bool useNativeContextMenu;
|
// final bool useNativeContextMenu;
|
||||||
final bool? isCollapsed;
|
// final bool? isCollapsed;
|
||||||
final TextInputType? keyboardType;
|
final TextInputType? keyboardType;
|
||||||
final TextInputAction? textInputAction;
|
final TextInputAction? textInputAction;
|
||||||
final Clip clipBehavior;
|
final Clip clipBehavior;
|
||||||
@ -86,15 +86,15 @@ class TextFormBuilderField extends StatelessWidget {
|
|||||||
this.onTapOutside,
|
this.onTapOutside,
|
||||||
this.inputFormatters,
|
this.inputFormatters,
|
||||||
this.style,
|
this.style,
|
||||||
this.contextMenuBuilder = TextField.defaultContextMenuBuilder,
|
// this.contextMenuBuilder = TextField.defaultContextMenuBuilder,
|
||||||
this.useNativeContextMenu = false,
|
// this.useNativeContextMenu = false,
|
||||||
this.isCollapsed,
|
// this.isCollapsed,
|
||||||
this.textInputAction,
|
this.textInputAction,
|
||||||
this.clipBehavior = Clip.hardEdge,
|
this.clipBehavior = Clip.hardEdge,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.placeholderAlignment,
|
// this.placeholderAlignment,
|
||||||
this.leadingAlignment,
|
// this.leadingAlignment,
|
||||||
this.trailingAlignment,
|
// this.trailingAlignment,
|
||||||
this.statesController,
|
this.statesController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -161,16 +161,16 @@ class TextFormBuilderField extends StatelessWidget {
|
|||||||
onTapOutside: onTapOutside,
|
onTapOutside: onTapOutside,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
style: style,
|
style: style,
|
||||||
contextMenuBuilder: contextMenuBuilder,
|
// contextMenuBuilder: contextMenuBuilder,
|
||||||
useNativeContextMenu: useNativeContextMenu,
|
// useNativeContextMenu: useNativeContextMenu,
|
||||||
isCollapsed: isCollapsed,
|
// isCollapsed: isCollapsed,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
clipBehavior: clipBehavior,
|
clipBehavior: clipBehavior,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
placeholderAlignment: placeholderAlignment,
|
// placeholderAlignment: placeholderAlignment,
|
||||||
leadingAlignment: leadingAlignment,
|
// leadingAlignment: leadingAlignment,
|
||||||
trailingAlignment: trailingAlignment,
|
// trailingAlignment: trailingAlignment,
|
||||||
statesController: statesController,
|
statesController: statesController,
|
||||||
),
|
),
|
||||||
if (field.hasError)
|
if (field.hasError)
|
||||||
|
|||||||
@ -40,6 +40,8 @@ class PlaybuttonView extends StatelessWidget {
|
|||||||
final VoidCallback onRequestMore;
|
final VoidCallback onRequestMore;
|
||||||
final ScrollController controller;
|
final ScrollController controller;
|
||||||
|
|
||||||
|
final Widget? leading;
|
||||||
|
|
||||||
const PlaybuttonView({
|
const PlaybuttonView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.itemCount,
|
required this.itemCount,
|
||||||
@ -49,6 +51,7 @@ class PlaybuttonView extends StatelessWidget {
|
|||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.onRequestMore,
|
required this.onRequestMore,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
this.leading,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -74,6 +77,7 @@ class PlaybuttonView extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
if (leading != null) leading!,
|
||||||
Toggle(
|
Toggle(
|
||||||
value: isGrid.value,
|
value: isGrid.value,
|
||||||
style:
|
style:
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|
||||||
|
|
||||||
class SpotubePage<T> extends MaterialPage<T> {
|
|
||||||
const SpotubePage({required super.child});
|
|
||||||
}
|
|
||||||
|
|
||||||
// class SpotubeSlidePage extends CustomTransitionPage {
|
|
||||||
// SpotubeSlidePage({
|
|
||||||
// required super.child,
|
|
||||||
// super.key,
|
|
||||||
// }) : super(
|
|
||||||
// reverseTransitionDuration: const Duration(milliseconds: 150),
|
|
||||||
// transitionDuration: const Duration(milliseconds: 150),
|
|
||||||
// transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
// return SlideTransition(
|
|
||||||
// position: Tween<Offset>(
|
|
||||||
// begin: const Offset(1, 0),
|
|
||||||
// end: Offset.zero,
|
|
||||||
// ).animate(animation),
|
|
||||||
// child: child,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
@ -166,7 +166,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
icon: const Icon(SpotubeIcons.moreVertical),
|
icon: const Icon(SpotubeIcons.moreVertical),
|
||||||
variance: ButtonVariance.outline,
|
variance: ButtonVariance.outline,
|
||||||
children: [
|
items: (context) => [
|
||||||
AdaptiveMenuButton(
|
AdaptiveMenuButton(
|
||||||
value: "download",
|
value: "download",
|
||||||
leading: const Icon(SpotubeIcons.download),
|
leading: const Icon(SpotubeIcons.download),
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
@ -9,6 +8,7 @@ import 'package:spotube/components/track_presentation/presentation_props.dart';
|
|||||||
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
import 'package:spotube/components/track_presentation/presentation_state.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
|
|
||||||
class TrackPresentationModifiersSection extends HookConsumerWidget {
|
class TrackPresentationModifiersSection extends HookConsumerWidget {
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
@ -25,7 +25,7 @@ class TrackPresentationModifiersSection extends HookConsumerWidget {
|
|||||||
presentationStateProvider(options.collection).notifier,
|
presentationStateProvider(options.collection).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
final controller = useTextEditingController();
|
final controller = useShadcnTextEditingController();
|
||||||
final scale = context.theme.scaling;
|
final scale = context.theme.scaling;
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constrains) {
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class SortTracksDropdown extends StatelessWidget {
|
|||||||
onSelected: onChanged,
|
onSelected: onChanged,
|
||||||
tooltip: context.l10n.sort_tracks,
|
tooltip: context.l10n.sort_tracks,
|
||||||
icon: const Icon(SpotubeIcons.sort),
|
icon: const Icon(SpotubeIcons.sort),
|
||||||
children: [
|
items: (context) => [
|
||||||
AdaptiveMenuButton(
|
AdaptiveMenuButton(
|
||||||
value: SortBy.none,
|
value: SortBy.none,
|
||||||
enabled: value != SortBy.none,
|
enabled: value != SortBy.none,
|
||||||
|
|||||||
@ -90,13 +90,27 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
Track track,
|
Track track,
|
||||||
) {
|
) {
|
||||||
showDialog(
|
/// showDialog doesn't work for some reason. So we have to
|
||||||
|
/// manually push a Dialog Route in the Navigator to get it working
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
DialogRoute(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
return FadeTransition(opacity: animation, child: child);
|
||||||
|
},
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PlaylistAddTrackDialog(
|
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||||||
|
builder: (context) {
|
||||||
|
return Center(
|
||||||
|
child: PlaylistAddTrackDialog(
|
||||||
tracks: [track],
|
tracks: [track],
|
||||||
openFromPlaylist: playlistId,
|
openFromPlaylist: playlistId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void actionStartRadio(
|
void actionStartRadio(
|
||||||
@ -352,7 +366,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
children: [
|
items: (context) => [
|
||||||
if (isLocalTrack)
|
if (isLocalTrack)
|
||||||
AdaptiveMenuButton(
|
AdaptiveMenuButton(
|
||||||
value: TrackOptionValue.delete,
|
value: TrackOptionValue.delete,
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
|
class _TextEditingControllerHookCreator {
|
||||||
|
const _TextEditingControllerHookCreator();
|
||||||
|
|
||||||
|
/// Creates a [TextEditingController] that will be disposed automatically.
|
||||||
|
///
|
||||||
|
/// The [text] parameter can be used to set the initial value of the
|
||||||
|
/// controller.
|
||||||
|
TextEditingController call({String? text, List<Object?>? keys}) {
|
||||||
|
return use(_TextEditingControllerHook(text, keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [TextEditingController] from the initial [value] that will
|
||||||
|
/// be disposed automatically.
|
||||||
|
TextEditingController fromValue(
|
||||||
|
TextEditingValue value, [
|
||||||
|
List<Object?>? keys,
|
||||||
|
]) {
|
||||||
|
return use(_TextEditingControllerHook.fromValue(value, keys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [TextEditingController], either via an initial text or an initial
|
||||||
|
/// [TextEditingValue].
|
||||||
|
///
|
||||||
|
/// To use a [TextEditingController] with an optional initial text, use:
|
||||||
|
/// ```dart
|
||||||
|
/// final controller = useTextEditingController(text: 'initial text');
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// To use a [TextEditingController] with an optional initial value, use:
|
||||||
|
/// ```dart
|
||||||
|
/// final controller = useTextEditingController
|
||||||
|
/// .fromValue(TextEditingValue.empty);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Changing the text or initial value after the widget has been built has no
|
||||||
|
/// effect whatsoever. To update the value in a callback, for instance after a
|
||||||
|
/// button was pressed, use the [TextEditingController.text] or
|
||||||
|
/// [TextEditingController.value] setters. To have the [TextEditingController]
|
||||||
|
/// reflect changing values, you can use [useEffect]. This example will update
|
||||||
|
/// the [TextEditingController.text] whenever a provided [ValueListenable]
|
||||||
|
/// changes:
|
||||||
|
/// ```dart
|
||||||
|
/// final controller = useTextEditingController();
|
||||||
|
/// final update = useValueListenable(myTextControllerUpdates);
|
||||||
|
///
|
||||||
|
/// useEffect(() {
|
||||||
|
/// controller.text = update;
|
||||||
|
/// }, [update]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// - [TextEditingController], which this hook creates.
|
||||||
|
const useShadcnTextEditingController = _TextEditingControllerHookCreator();
|
||||||
|
|
||||||
|
class _TextEditingControllerHook extends Hook<TextEditingController> {
|
||||||
|
const _TextEditingControllerHook(
|
||||||
|
this.initialText, [
|
||||||
|
List<Object?>? keys,
|
||||||
|
]) : initialValue = null,
|
||||||
|
super(keys: keys);
|
||||||
|
|
||||||
|
const _TextEditingControllerHook.fromValue(
|
||||||
|
TextEditingValue this.initialValue, [
|
||||||
|
List<Object?>? keys,
|
||||||
|
]) : initialText = null,
|
||||||
|
super(keys: keys);
|
||||||
|
|
||||||
|
final String? initialText;
|
||||||
|
final TextEditingValue? initialValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TextEditingControllerHookState createState() {
|
||||||
|
return _TextEditingControllerHookState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextEditingControllerHookState
|
||||||
|
extends HookState<TextEditingController, _TextEditingControllerHook> {
|
||||||
|
late final _controller = hook.initialValue != null
|
||||||
|
? TextEditingController.fromValue(
|
||||||
|
hook.initialValue ?? TextEditingValue.empty,
|
||||||
|
)
|
||||||
|
: TextEditingController(text: hook.initialText);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEditingController build(BuildContext context) => _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() => _controller.dispose();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugLabel => 'useTextEditingController';
|
||||||
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "تصدير الملفات المخزنة مؤقتًا",
|
"export_cache_files": "تصدير الملفات المخزنة مؤقتًا",
|
||||||
"found_n_files": "تم العثور على {count} ملف",
|
"found_n_files": "تم العثور على {count} ملف",
|
||||||
"export_cache_confirmation": "هل تريد تصدير هذه الملفات إلى",
|
"export_cache_confirmation": "هل تريد تصدير هذه الملفات إلى",
|
||||||
"exported_n_out_of_m_files": "تم تصدير {filesExported} من أصل {files} ملفات"
|
"exported_n_out_of_m_files": "تم تصدير {filesExported} من أصل {files} ملفات",
|
||||||
|
"playlist": "قائمة التشغيل",
|
||||||
|
"no_loop": "بدون تكرار",
|
||||||
|
"generate": "إنشاء",
|
||||||
|
"undo": "تراجع",
|
||||||
|
"download_all": "تنزيل الكل",
|
||||||
|
"add_all_to_playlist": "إضافة الكل إلى قائمة التشغيل",
|
||||||
|
"add_all_to_queue": "إضافة الكل إلى القائمة",
|
||||||
|
"play_all_next": "تشغيل الكل بعد ذلك",
|
||||||
|
"pause": "إيقاف مؤقت",
|
||||||
|
"view_all": "عرض الكل",
|
||||||
|
"no_tracks_added_yet": "يبدو أنك لم تضف أي مسارات بعد",
|
||||||
|
"no_tracks": "يبدو أنه لا يوجد أي مسارات هنا",
|
||||||
|
"no_tracks_listened_yet": "يبدو أنك لم تستمع إلى أي شيء بعد",
|
||||||
|
"not_following_artists": "أنت لا تتابع أي فنانين",
|
||||||
|
"no_favorite_albums_yet": "يبدو أنك لم تضف أي ألبومات إلى المفضلة بعد",
|
||||||
|
"no_logs_found": "لم يتم العثور على سجلات",
|
||||||
|
"youtube_engine": "محرك يوتيوب",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} غير مثبت",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} غير مثبت في نظامك.",
|
||||||
|
"youtube_engine_set_path": "تأكد من أنه متاح في متغير PATH أو\nحدد المسار الكامل للملف القابل للتنفيذ {engine} أدناه",
|
||||||
|
"youtube_engine_unix_issue_message": "في أنظمة macOS/Linux/Unix مثل الأنظمة، لن يعمل تعيين المسار في .zshrc/.bashrc/.bash_profile وما إلى ذلك.\nيجب تعيين المسار في ملف تكوين الصدفة",
|
||||||
|
"download": "تنزيل",
|
||||||
|
"file_not_found": "الملف غير موجود",
|
||||||
|
"custom": "مخصص",
|
||||||
|
"add_custom_url": "إضافة URL مخصص"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "ক্যাশে ফাইল রপ্তানি",
|
"export_cache_files": "ক্যাশে ফাইল রপ্তানি",
|
||||||
"found_n_files": "{count} টি ফাইল পাওয়া গেছে",
|
"found_n_files": "{count} টি ফাইল পাওয়া গেছে",
|
||||||
"export_cache_confirmation": "আপনি কি এই ফাইলগুলি রপ্তানি করতে চান",
|
"export_cache_confirmation": "আপনি কি এই ফাইলগুলি রপ্তানি করতে চান",
|
||||||
"exported_n_out_of_m_files": "{filesExported} টি ফাইল রপ্তানি করা হয়েছে {files} এর মধ্যে"
|
"exported_n_out_of_m_files": "{filesExported} টি ফাইল রপ্তানি করা হয়েছে {files} এর মধ্যে",
|
||||||
|
"playlist": "প্লেলিস্ট",
|
||||||
|
"no_loop": "কোনো লুপ নেই",
|
||||||
|
"generate": "উৎপন্ন করুন",
|
||||||
|
"undo": "পূর্বাবস্থায় ফিরুন",
|
||||||
|
"download_all": "সব ডাউনলোড করুন",
|
||||||
|
"add_all_to_playlist": "সব প্লেলিস্টে যোগ করুন",
|
||||||
|
"add_all_to_queue": "সব কিউতে যোগ করুন",
|
||||||
|
"play_all_next": "সব পরবর্তী খেলুন",
|
||||||
|
"pause": "বিরতি",
|
||||||
|
"view_all": "সব দেখুন",
|
||||||
|
"no_tracks_added_yet": "এখনও কোনো ট্র্যাক যোগ করা হয়নি মনে হচ্ছে",
|
||||||
|
"no_tracks": "এখানে কোনো ট্র্যাক নেই মনে হচ্ছে",
|
||||||
|
"no_tracks_listened_yet": "এখনও কিছু শোনা হয়নি মনে হচ্ছে",
|
||||||
|
"not_following_artists": "আপনি কোনো শিল্পীকে অনুসরণ করছেন না",
|
||||||
|
"no_favorite_albums_yet": "এখনও কোনো অ্যালবাম প্রিয় তালিকায় যোগ করা হয়নি মনে হচ্ছে",
|
||||||
|
"no_logs_found": "কোনো লগ পাওয়া যায়নি",
|
||||||
|
"youtube_engine": "ইউটিউব ইঞ্জিন",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} ইনস্টল করা নেই",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} আপনার সিস্টেমে ইনস্টল করা নেই।",
|
||||||
|
"youtube_engine_set_path": "এটি PATH ভেরিয়েবলে উপলব্ধ কিনা নিশ্চিত করুন অথবা\nনীচে {engine} এক্সিকিউটেবল এর পূর্ণপথ সেট করুন",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/Unix-এর মতো অপারেটিং সিস্টেমে, .zshrc/.bashrc/.bash_profile ইত্যাদিতে পাথ সেট করা কাজ করবে না।\nআপনাকে শেল কনফিগারেশন ফাইলে পাথ সেট করতে হবে",
|
||||||
|
"download": "ডাউনলোড",
|
||||||
|
"file_not_found": "ফাইল পাওয়া যায়নি",
|
||||||
|
"custom": "কাস্টম",
|
||||||
|
"add_custom_url": "কাস্টম URL যোগ করুন"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Exportar arxius en caché",
|
"export_cache_files": "Exportar arxius en caché",
|
||||||
"found_n_files": "S'han trobat {count} arxius",
|
"found_n_files": "S'han trobat {count} arxius",
|
||||||
"export_cache_confirmation": "Voleu exportar aquests arxius a",
|
"export_cache_confirmation": "Voleu exportar aquests arxius a",
|
||||||
"exported_n_out_of_m_files": "S'han exportat {filesExported} de {files} arxius"
|
"exported_n_out_of_m_files": "S'han exportat {filesExported} de {files} arxius",
|
||||||
|
"playlist": "Llista de reproducció",
|
||||||
|
"no_loop": "Sense repetició",
|
||||||
|
"generate": "Generar",
|
||||||
|
"undo": "Desfer",
|
||||||
|
"download_all": "Descarregar tot",
|
||||||
|
"add_all_to_playlist": "Afegir tot a la llista de reproducció",
|
||||||
|
"add_all_to_queue": "Afegir tot a la cua",
|
||||||
|
"play_all_next": "Reproduir tot a continuació",
|
||||||
|
"pause": "Pausa",
|
||||||
|
"view_all": "Veure tot",
|
||||||
|
"no_tracks_added_yet": "Sembla que encara no has afegit cap pista",
|
||||||
|
"no_tracks": "Sembla que no hi ha pistes aquí",
|
||||||
|
"no_tracks_listened_yet": "Sembla que no has escoltat res encara",
|
||||||
|
"not_following_artists": "No estàs seguint cap artista",
|
||||||
|
"no_favorite_albums_yet": "Sembla que encara no has afegit cap àlbum als teus favorits",
|
||||||
|
"no_logs_found": "No s'han trobat registres",
|
||||||
|
"youtube_engine": "Motor de YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} no està instal·lat",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} no està instal·lat al teu sistema.",
|
||||||
|
"youtube_engine_set_path": "Assegura't que estigui disponible a la variable PATH o\nestableix el camí absolut a l'executable de {engine} a continuació",
|
||||||
|
"youtube_engine_unix_issue_message": "En macOS/Linux/Unix com a sistemes operatius, establir el camí a .zshrc/.bashrc/.bash_profile etc. no funcionarà.\nHas de configurar el camí al fitxer de configuració de la shell",
|
||||||
|
"download": "Descarregar",
|
||||||
|
"file_not_found": "Fitxer no trobat",
|
||||||
|
"custom": "Personalitzat",
|
||||||
|
"add_custom_url": "Afegir URL personalitzada"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Exportovat soubory z mezipaměti",
|
"export_cache_files": "Exportovat soubory z mezipaměti",
|
||||||
"found_n_files": "Nalezeno {count} souborů",
|
"found_n_files": "Nalezeno {count} souborů",
|
||||||
"export_cache_confirmation": "Chcete exportovat tyto soubory do",
|
"export_cache_confirmation": "Chcete exportovat tyto soubory do",
|
||||||
"exported_n_out_of_m_files": "Exportováno {filesExported} z {files} souborů"
|
"exported_n_out_of_m_files": "Exportováno {filesExported} z {files} souborů",
|
||||||
|
"playlist": "Seznam skladeb",
|
||||||
|
"no_loop": "Žádné opakování",
|
||||||
|
"generate": "Generovat",
|
||||||
|
"undo": "Zpět",
|
||||||
|
"download_all": "Stáhnout vše",
|
||||||
|
"add_all_to_playlist": "Přidat vše do seznamu skladeb",
|
||||||
|
"add_all_to_queue": "Přidat vše do fronty",
|
||||||
|
"play_all_next": "Přehrát vše následně",
|
||||||
|
"pause": "Pauza",
|
||||||
|
"view_all": "Zobrazit vše",
|
||||||
|
"no_tracks_added_yet": "Zdá se, že jste ještě nepřidali žádné skladby",
|
||||||
|
"no_tracks": "Zdá se, že zde nejsou žádné skladby",
|
||||||
|
"no_tracks_listened_yet": "Zdá se, že jste ještě nic neposlouchali",
|
||||||
|
"not_following_artists": "Nezajímáte se o žádné umělce",
|
||||||
|
"no_favorite_albums_yet": "Zdá se, že jste ještě nepřidali žádné alba mezi oblíbené",
|
||||||
|
"no_logs_found": "Žádné záznamy nenalezeny",
|
||||||
|
"youtube_engine": "YouTube Engine",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} není nainstalován",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} není nainstalován ve vašem systému.",
|
||||||
|
"youtube_engine_set_path": "Ujistěte se, že je k dispozici v proměnné PATH nebo\nnastavte absolutní cestu k {engine} spustitelnému souboru níže",
|
||||||
|
"youtube_engine_unix_issue_message": "V macOS/Linux/Unixových systémech nebude fungovat nastavení cesty v .zshrc/.bashrc/.bash_profile atd.\nMusíte nastavit cestu v konfiguračním souboru shellu",
|
||||||
|
"download": "Stáhnout",
|
||||||
|
"file_not_found": "Soubor nenalezen",
|
||||||
|
"custom": "Vlastní",
|
||||||
|
"add_custom_url": "Přidat vlastní URL"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Cachedateien exportieren",
|
"export_cache_files": "Cachedateien exportieren",
|
||||||
"found_n_files": "{count} Dateien gefunden",
|
"found_n_files": "{count} Dateien gefunden",
|
||||||
"export_cache_confirmation": "Möchten Sie diese Dateien exportieren nach",
|
"export_cache_confirmation": "Möchten Sie diese Dateien exportieren nach",
|
||||||
"exported_n_out_of_m_files": "{filesExported} von {files} Dateien exportiert"
|
"exported_n_out_of_m_files": "{filesExported} von {files} Dateien exportiert",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_loop": "Kein Loop",
|
||||||
|
"generate": "Generieren",
|
||||||
|
"undo": "Rückgängig",
|
||||||
|
"download_all": "Alle herunterladen",
|
||||||
|
"add_all_to_playlist": "Alle zur Playlist hinzufügen",
|
||||||
|
"add_all_to_queue": "Alle zur Warteschlange hinzufügen",
|
||||||
|
"play_all_next": "Alle als Nächstes abspielen",
|
||||||
|
"pause": "Pause",
|
||||||
|
"view_all": "Alle ansehen",
|
||||||
|
"no_tracks_added_yet": "Sie haben noch keine Titel hinzugefügt.",
|
||||||
|
"no_tracks": "Es sieht so aus, als ob hier keine Titel sind.",
|
||||||
|
"no_tracks_listened_yet": "Es scheint, dass Sie noch nichts gehört haben.",
|
||||||
|
"not_following_artists": "Sie folgen noch keinem Künstler.",
|
||||||
|
"no_favorite_albums_yet": "Es sieht so aus, als ob Sie noch keine Alben zu Ihren Favoriten hinzugefügt haben.",
|
||||||
|
"no_logs_found": "Keine Protokolle gefunden",
|
||||||
|
"youtube_engine": "YouTube-Engine",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} ist nicht installiert",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} ist nicht auf Ihrem System installiert.",
|
||||||
|
"youtube_engine_set_path": "Stellen Sie sicher, dass es im PATH verfügbar ist oder\nsetzen Sie den absoluten Pfad zur {engine} ausführbaren Datei unten.",
|
||||||
|
"youtube_engine_unix_issue_message": "In macOS/Linux/unixähnlichen Betriebssystemen funktioniert das Setzen des Pfads in .zshrc/.bashrc/.bash_profile usw. nicht.\nSie müssen den Pfad in der Shell-Konfigurationsdatei festlegen.",
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"file_not_found": "Datei nicht gefunden",
|
||||||
|
"custom": "Benutzerdefiniert",
|
||||||
|
"add_custom_url": "Benutzerdefinierte URL hinzufügen"
|
||||||
}
|
}
|
||||||
@ -422,5 +422,7 @@
|
|||||||
"youtube_engine_set_path": "Make sure it's available in the PATH variable or\nset the absolute path to the {engine} executable below",
|
"youtube_engine_set_path": "Make sure it's available in the PATH variable or\nset the absolute path to the {engine} executable below",
|
||||||
"youtube_engine_unix_issue_message": "In macOS/Linux/unix like OS's, setting path on .zshrc/.bashrc/.bash_profile etc. won't work.\nYou need to set the path in the shell configuration file",
|
"youtube_engine_unix_issue_message": "In macOS/Linux/unix like OS's, setting path on .zshrc/.bashrc/.bash_profile etc. won't work.\nYou need to set the path in the shell configuration file",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"file_not_found": "File not found"
|
"file_not_found": "File not found",
|
||||||
|
"custom": "Custom",
|
||||||
|
"add_custom_url": "Add custom URL"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Exportar archivos en caché",
|
"export_cache_files": "Exportar archivos en caché",
|
||||||
"found_n_files": "Se encontraron {count} archivos",
|
"found_n_files": "Se encontraron {count} archivos",
|
||||||
"export_cache_confirmation": "¿Desea exportar estos archivos a",
|
"export_cache_confirmation": "¿Desea exportar estos archivos a",
|
||||||
"exported_n_out_of_m_files": "Se exportaron {filesExported} de {files} archivos"
|
"exported_n_out_of_m_files": "Se exportaron {filesExported} de {files} archivos",
|
||||||
|
"playlist": "Lista de reproducción",
|
||||||
|
"no_loop": "Sin bucle",
|
||||||
|
"generate": "Generar",
|
||||||
|
"undo": "Deshacer",
|
||||||
|
"download_all": "Descargar todo",
|
||||||
|
"add_all_to_playlist": "Agregar todo a la lista de reproducción",
|
||||||
|
"add_all_to_queue": "Agregar todo a la cola",
|
||||||
|
"play_all_next": "Reproducir todo a continuación",
|
||||||
|
"pause": "Pausa",
|
||||||
|
"view_all": "Ver todo",
|
||||||
|
"no_tracks_added_yet": "Parece que aún no has agregado ninguna canción.",
|
||||||
|
"no_tracks": "Parece que no hay canciones aquí.",
|
||||||
|
"no_tracks_listened_yet": "Parece que no has escuchado nada todavía.",
|
||||||
|
"not_following_artists": "No sigues a ningún artista.",
|
||||||
|
"no_favorite_albums_yet": "Parece que aún no has agregado ningún álbum a tus favoritos.",
|
||||||
|
"no_logs_found": "No se encontraron registros",
|
||||||
|
"youtube_engine": "Motor de YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} no está instalado",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} no está instalado en tu sistema.",
|
||||||
|
"youtube_engine_set_path": "Asegúrate de que esté disponible en la variable PATH o\nestablece la ruta absoluta del ejecutable de {engine} a continuación.",
|
||||||
|
"youtube_engine_unix_issue_message": "En macOS/Linux/sistemas operativos similares a Unix, establecer la ruta en .zshrc/.bashrc/.bash_profile etc. no funcionará.\nNecesitas establecer la ruta en el archivo de configuración del shell.",
|
||||||
|
"download": "Descargar",
|
||||||
|
"file_not_found": "Archivo no encontrado",
|
||||||
|
"custom": "Personalizado",
|
||||||
|
"add_custom_url": "Agregar URL personalizada"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Esportatu cache fitxategiak",
|
"export_cache_files": "Esportatu cache fitxategiak",
|
||||||
"found_n_files": "{count} fitxategi aurkitu dira",
|
"found_n_files": "{count} fitxategi aurkitu dira",
|
||||||
"export_cache_confirmation": "Fitxategi hauek esportatu nahi al dituzu",
|
"export_cache_confirmation": "Fitxategi hauek esportatu nahi al dituzu",
|
||||||
"exported_n_out_of_m_files": "{filesExported} fitxategi esportatu dira {files} -tik"
|
"exported_n_out_of_m_files": "{filesExported} fitxategi esportatu dira {files} -tik",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_loop": "Ez dago loop-ik",
|
||||||
|
"generate": "Sortu",
|
||||||
|
"undo": "Desegondu",
|
||||||
|
"download_all": "Guztia deskargatu",
|
||||||
|
"add_all_to_playlist": "Guztia playlist-era gehitu",
|
||||||
|
"add_all_to_queue": "Guztia zerrendara gehitu",
|
||||||
|
"play_all_next": "Guztia hurrengoan jolastu",
|
||||||
|
"pause": "Pausatu",
|
||||||
|
"view_all": "Ikusi guztia",
|
||||||
|
"no_tracks_added_yet": "Dirudienez, oraindik ez duzu abestirik gehitu.",
|
||||||
|
"no_tracks": "Ez dirudi hemen abestirik dagoenik.",
|
||||||
|
"no_tracks_listened_yet": "Dirudienez, oraindik ez duzu ezer entzun.",
|
||||||
|
"not_following_artists": "Ez zaude artisten atzetik.",
|
||||||
|
"no_favorite_albums_yet": "Dirudienez, oraindik ez duzu albumik gehitu zure gogokoen artean.",
|
||||||
|
"no_logs_found": "Ez dira log-ak aurkitu",
|
||||||
|
"youtube_engine": "YouTube Motorra",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} ez dago instalatuta",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} ez dago zure sisteman instalatuta.",
|
||||||
|
"youtube_engine_set_path": "Ziurtatu PATH aldagaiaren barruan dagoela edo\nezarri {engine} exekutagarriaren helbide absolutua behean.",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/Unix bezalako sistemetan, .zshrc/.bashrc/.bash_profile bezalako fitxategietan bidearen ezarpenak ez dira funtzionatuko.\nBidearen ezarpena shell konfigurazio fitxategian egin behar duzu.",
|
||||||
|
"download": "Deskargatu",
|
||||||
|
"file_not_found": "Fitxategia ez da aurkitu",
|
||||||
|
"custom": "Pertsonalizatua",
|
||||||
|
"add_custom_url": "Gehitu URL pertsonalizatua"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "صادر کردن فایلهای حافظه موقت",
|
"export_cache_files": "صادر کردن فایلهای حافظه موقت",
|
||||||
"found_n_files": "{count} فایل یافت شد",
|
"found_n_files": "{count} فایل یافت شد",
|
||||||
"export_cache_confirmation": "آیا میخواهید این فایلها را صادر کنید به",
|
"export_cache_confirmation": "آیا میخواهید این فایلها را صادر کنید به",
|
||||||
"exported_n_out_of_m_files": "{filesExported} از {files} فایل صادر شد"
|
"exported_n_out_of_m_files": "{filesExported} از {files} فایل صادر شد",
|
||||||
|
"playlist": "لیست پخش",
|
||||||
|
"no_loop": "بدون حلقه",
|
||||||
|
"generate": "ایجاد",
|
||||||
|
"undo": "بازگشت",
|
||||||
|
"download_all": "دانلود همه",
|
||||||
|
"add_all_to_playlist": "افزودن همه به لیست پخش",
|
||||||
|
"add_all_to_queue": "افزودن همه به صف",
|
||||||
|
"play_all_next": "پخش همه بعدی",
|
||||||
|
"pause": "مکث",
|
||||||
|
"view_all": "مشاهده همه",
|
||||||
|
"no_tracks_added_yet": "به نظر میرسد هنوز هیچ آهنگی اضافه نکردهاید.",
|
||||||
|
"no_tracks": "به نظر میرسد هیچ آهنگی در اینجا وجود ندارد.",
|
||||||
|
"no_tracks_listened_yet": "به نظر میرسد هنوز چیزی نشنیدهاید.",
|
||||||
|
"not_following_artists": "شما هیچ هنرمندی را دنبال نمیکنید.",
|
||||||
|
"no_favorite_albums_yet": "به نظر میرسد هنوز هیچ آلبومی را به علاقهمندیهایتان اضافه نکردهاید.",
|
||||||
|
"no_logs_found": "هیچ لاگی پیدا نشد",
|
||||||
|
"youtube_engine": "موتور YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} نصب نشده است",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} در سیستم شما نصب نشده است.",
|
||||||
|
"youtube_engine_set_path": "اطمینان حاصل کنید که در متغیر PATH موجود است یا\nآدرس مطلق فایل اجرایی {engine} را در زیر تنظیم کنید.",
|
||||||
|
"youtube_engine_unix_issue_message": "در macOS/Linux/سیستمعاملهای مشابه Unix، تنظیم مسیر در .zshrc/.bashrc/.bash_profile و غیره کار نمیکند.\nباید مسیر را در فایل پیکربندی شل تنظیم کنید.",
|
||||||
|
"download": "دانلود",
|
||||||
|
"file_not_found": "فایل پیدا نشد",
|
||||||
|
"custom": "شخصیسازی شده",
|
||||||
|
"add_custom_url": "اضافه کردن URL سفارشی"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Vie välimuistitiedostot",
|
"export_cache_files": "Vie välimuistitiedostot",
|
||||||
"found_n_files": "Löydettiin {count} tiedostoa",
|
"found_n_files": "Löydettiin {count} tiedostoa",
|
||||||
"export_cache_confirmation": "Haluatko viedä nämä tiedostot",
|
"export_cache_confirmation": "Haluatko viedä nämä tiedostot",
|
||||||
"exported_n_out_of_m_files": "Vietiin {filesExported}/{files} tiedostoa"
|
"exported_n_out_of_m_files": "Vietiin {filesExported}/{files} tiedostoa",
|
||||||
|
"playlist": "Soittolista",
|
||||||
|
"no_loop": "Ei silmukkaa",
|
||||||
|
"generate": "Luo",
|
||||||
|
"undo": "Peruuta",
|
||||||
|
"download_all": "Lataa kaikki",
|
||||||
|
"add_all_to_playlist": "Lisää kaikki soittolistalle",
|
||||||
|
"add_all_to_queue": "Lisää kaikki jonoon",
|
||||||
|
"play_all_next": "Toista kaikki seuraavaksi",
|
||||||
|
"pause": "Pysäytä",
|
||||||
|
"view_all": "Näytä kaikki",
|
||||||
|
"no_tracks_added_yet": "Näyttää siltä, että et ole lisännyt vielä mitään kappaleita.",
|
||||||
|
"no_tracks": "Näyttää siltä, että täällä ei ole kappaleita.",
|
||||||
|
"no_tracks_listened_yet": "Näyttää siltä, että et ole kuunnellut mitään vielä.",
|
||||||
|
"not_following_artists": "Et seuraa yhtään artistia.",
|
||||||
|
"no_favorite_albums_yet": "Näyttää siltä, että et ole lisännyt yhtään albumia suosikkeihisi.",
|
||||||
|
"no_logs_found": "Ei lokitietoja löydetty",
|
||||||
|
"youtube_engine": "YouTube-moottori",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} ei ole asennettu",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} ei ole asennettu järjestelmääsi.",
|
||||||
|
"youtube_engine_set_path": "Varmista, että se on saatavilla PATH-muuttujassa tai\nasetetaan {engine} suoritettavan tiedoston absoluuttinen polku alla.",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/unix-tyyppisissä käyttöjärjestelmissä polun asettaminen .zshrc/.bashrc/.bash_profile jne. ei toimi.\nSinun täytyy asettaa polku shellin asetustiedostoon.",
|
||||||
|
"download": "Lataa",
|
||||||
|
"file_not_found": "Tiedostoa ei löydy",
|
||||||
|
"custom": "Mukautettu",
|
||||||
|
"add_custom_url": "Lisää mukautettu URL"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Exporter les fichiers en cache",
|
"export_cache_files": "Exporter les fichiers en cache",
|
||||||
"found_n_files": "{count} fichiers trouvés",
|
"found_n_files": "{count} fichiers trouvés",
|
||||||
"export_cache_confirmation": "Voulez-vous exporter ces fichiers vers",
|
"export_cache_confirmation": "Voulez-vous exporter ces fichiers vers",
|
||||||
"exported_n_out_of_m_files": "{filesExported} fichiers exportés sur {files}"
|
"exported_n_out_of_m_files": "{filesExported} fichiers exportés sur {files}",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_loop": "Pas de boucle",
|
||||||
|
"generate": "Générer",
|
||||||
|
"undo": "Annuler",
|
||||||
|
"download_all": "Télécharger tout",
|
||||||
|
"add_all_to_playlist": "Ajouter tout à la playlist",
|
||||||
|
"add_all_to_queue": "Ajouter tout à la file d'attente",
|
||||||
|
"play_all_next": "Lire tout suivant",
|
||||||
|
"pause": "Pause",
|
||||||
|
"view_all": "Voir tout",
|
||||||
|
"no_tracks_added_yet": "Il semble que vous n'avez encore ajouté aucun morceau.",
|
||||||
|
"no_tracks": "Il semble qu'il n'y ait pas de morceaux ici.",
|
||||||
|
"no_tracks_listened_yet": "Il semble que vous n'avez encore rien écouté.",
|
||||||
|
"not_following_artists": "Vous ne suivez aucun artiste.",
|
||||||
|
"no_favorite_albums_yet": "Il semble que vous n'ayez encore ajouté aucun album à vos favoris.",
|
||||||
|
"no_logs_found": "Aucun log trouvé",
|
||||||
|
"youtube_engine": "Moteur YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} n'est pas installé",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} n'est pas installé sur votre système.",
|
||||||
|
"youtube_engine_set_path": "Assurez-vous qu'il est disponible dans la variable PATH ou\nfixez le chemin absolu du fichier exécutable {engine} ci-dessous.",
|
||||||
|
"youtube_engine_unix_issue_message": "Dans macOS/Linux/les systèmes d'exploitation similaires à Unix, définir le chemin dans .zshrc/.bashrc/.bash_profile etc. ne fonctionnera pas.\nVous devez définir le chemin dans le fichier de configuration du shell.",
|
||||||
|
"download": "Télécharger",
|
||||||
|
"file_not_found": "Fichier non trouvé",
|
||||||
|
"custom": "Personnalisé",
|
||||||
|
"add_custom_url": "Ajouter une URL personnalisée"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "कैश फ़ाइलें निर्यात करें",
|
"export_cache_files": "कैश फ़ाइलें निर्यात करें",
|
||||||
"found_n_files": "{count} फ़ाइलें मिलीं",
|
"found_n_files": "{count} फ़ाइलें मिलीं",
|
||||||
"export_cache_confirmation": "क्या आप इन फ़ाइलों को निर्यात करना चाहते हैं",
|
"export_cache_confirmation": "क्या आप इन फ़ाइलों को निर्यात करना चाहते हैं",
|
||||||
"exported_n_out_of_m_files": "{filesExported} फ़ाइलें निर्यात की गईं {files} में से"
|
"exported_n_out_of_m_files": "{filesExported} फ़ाइलें निर्यात की गईं {files} में से",
|
||||||
|
"playlist": "प्लेलिस्ट",
|
||||||
|
"no_loop": "कोई लूप नहीं",
|
||||||
|
"generate": "उत्पन्न करें",
|
||||||
|
"undo": "पूर्ववत करें",
|
||||||
|
"download_all": "सभी डाउनलोड करें",
|
||||||
|
"add_all_to_playlist": "सभी को प्लेलिस्ट में जोड़ें",
|
||||||
|
"add_all_to_queue": "सभी को कतार में जोड़ें",
|
||||||
|
"play_all_next": "सभी को अगले खेलने के लिए",
|
||||||
|
"pause": "रोकें",
|
||||||
|
"view_all": "सभी देखें",
|
||||||
|
"no_tracks_added_yet": "लगता है आपने अभी तक कोई ट्रैक नहीं जोड़ा है।",
|
||||||
|
"no_tracks": "लगता है यहाँ कोई ट्रैक नहीं है।",
|
||||||
|
"no_tracks_listened_yet": "लगता है आपने अभी तक कुछ नहीं सुना है।",
|
||||||
|
"not_following_artists": "आप किसी भी कलाकार को फॉलो नहीं कर रहे हैं।",
|
||||||
|
"no_favorite_albums_yet": "लगता है आपने अभी तक कोई एल्बम अपनी पसंदीदा सूची में नहीं जोड़ा है।",
|
||||||
|
"no_logs_found": "कोई लॉग नहीं मिला",
|
||||||
|
"youtube_engine": "YouTube इंजन",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} स्थापित नहीं है",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} आपके सिस्टम में स्थापित नहीं है।",
|
||||||
|
"youtube_engine_set_path": "यह सुनिश्चित करें कि यह PATH वेरिएबल में उपलब्ध हो या\nनीचे {engine} निष्पादन योग्य फ़ाइल का पूर्ण पथ सेट करें।",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/यूनिक्स जैसे OS में, .zshrc/.bashrc/.bash_profile आदि में पथ सेट करना काम नहीं करेगा।\nआपको पथ को शेल कॉन्फ़िगरेशन फ़ाइल में सेट करना होगा।",
|
||||||
|
"download": "डाउनलोड करें",
|
||||||
|
"file_not_found": "फाइल नहीं मिली",
|
||||||
|
"custom": "कस्टम",
|
||||||
|
"add_custom_url": "कस्टम URL जोड़ें"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Export Cached Files",
|
"export_cache_files": "Export Cached Files",
|
||||||
"found_n_files": "Found {count} files",
|
"found_n_files": "Found {count} files",
|
||||||
"export_cache_confirmation": "Do you want to export these files to",
|
"export_cache_confirmation": "Do you want to export these files to",
|
||||||
"exported_n_out_of_m_files": "Exported {filesExported} out of {files} files"
|
"exported_n_out_of_m_files": "Exported {filesExported} out of {files} files",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_loop": "No loop",
|
||||||
|
"generate": "Generate",
|
||||||
|
"undo": "Undo",
|
||||||
|
"download_all": "Download all",
|
||||||
|
"add_all_to_playlist": "Add all to playlist",
|
||||||
|
"add_all_to_queue": "Add all to queue",
|
||||||
|
"play_all_next": "Play all next",
|
||||||
|
"pause": "Pause",
|
||||||
|
"view_all": "View all",
|
||||||
|
"no_tracks_added_yet": "Looks like you haven't added any tracks yet",
|
||||||
|
"no_tracks": "Looks like there are no tracks here",
|
||||||
|
"no_tracks_listened_yet": "Looks like you haven't listened to anything yet",
|
||||||
|
"not_following_artists": "You're not following any artists",
|
||||||
|
"no_favorite_albums_yet": "Looks like you haven't added any albums to your favorites yet",
|
||||||
|
"no_logs_found": "No logs found",
|
||||||
|
"youtube_engine": "YouTube Engine",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} is not installed",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} is not installed in your system.",
|
||||||
|
"youtube_engine_set_path": "Make sure it's available in the PATH variable or\nset the absolute path to the {engine} executable below",
|
||||||
|
"youtube_engine_unix_issue_message": "In macOS/Linux/unix like OS's, setting path on .zshrc/.bashrc/.bash_profile etc. won't work.\nYou need to set the path in the shell configuration file",
|
||||||
|
"download": "Download",
|
||||||
|
"file_not_found": "File not found",
|
||||||
|
"custom": "Custom",
|
||||||
|
"add_custom_url": "Add custom URL"
|
||||||
}
|
}
|
||||||
@ -402,5 +402,30 @@
|
|||||||
"export_cache_files": "Esporta file nella cache",
|
"export_cache_files": "Esporta file nella cache",
|
||||||
"found_n_files": "Trovati {count} file",
|
"found_n_files": "Trovati {count} file",
|
||||||
"export_cache_confirmation": "Vuoi esportare questi file su",
|
"export_cache_confirmation": "Vuoi esportare questi file su",
|
||||||
"exported_n_out_of_m_files": "Esportati {filesExported} su {files} file"
|
"exported_n_out_of_m_files": "Esportati {filesExported} su {files} file",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_loop": "Nessun ciclo",
|
||||||
|
"generate": "Genera",
|
||||||
|
"undo": "Annulla",
|
||||||
|
"download_all": "Scarica tutto",
|
||||||
|
"add_all_to_playlist": "Aggiungi tutto alla playlist",
|
||||||
|
"add_all_to_queue": "Aggiungi tutto alla coda",
|
||||||
|
"play_all_next": "Riproduci tutto dopo",
|
||||||
|
"pause": "Pausa",
|
||||||
|
"view_all": "Vedi tutto",
|
||||||
|
"no_tracks_added_yet": "Sembra che non hai ancora aggiunto nessun brano",
|
||||||
|
"no_tracks": "Sembra che non ci siano brani qui",
|
||||||
|
"no_tracks_listened_yet": "Sembra che non hai ascoltato nulla ancora",
|
||||||
|
"not_following_artists": "Non stai seguendo alcun artista",
|
||||||
|
"no_favorite_albums_yet": "Sembra che non hai ancora aggiunto album ai tuoi preferiti",
|
||||||
|
"no_logs_found": "Nessun registro trovato",
|
||||||
|
"youtube_engine": "Motore YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} non è installato",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} non è installato nel tuo sistema.",
|
||||||
|
"youtube_engine_set_path": "Assicurati che sia disponibile nella variabile PATH o\nimposta il percorso assoluto all'eseguibile {engine} qui sotto",
|
||||||
|
"youtube_engine_unix_issue_message": "In macOS/Linux/os simili a unix, impostare il percorso su .zshrc/.bashrc/.bash_profile ecc. non funzionerà.\nDevi impostare il percorso nel file di configurazione della shell",
|
||||||
|
"download": "Scarica",
|
||||||
|
"file_not_found": "File non trovato",
|
||||||
|
"custom": "Personalizzato",
|
||||||
|
"add_custom_url": "Aggiungi URL personalizzato"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "キャッシュされたファイルをエクスポート",
|
"export_cache_files": "キャッシュされたファイルをエクスポート",
|
||||||
"found_n_files": "{count}ファイルが見つかりました",
|
"found_n_files": "{count}ファイルが見つかりました",
|
||||||
"export_cache_confirmation": "これらのファイルをエクスポートしますか",
|
"export_cache_confirmation": "これらのファイルをエクスポートしますか",
|
||||||
"exported_n_out_of_m_files": "{filesExported} / {files}ファイルがエクスポートされました"
|
"exported_n_out_of_m_files": "{filesExported} / {files}ファイルがエクスポートされました",
|
||||||
|
"playlist": "プレイリスト",
|
||||||
|
"no_loop": "ループなし",
|
||||||
|
"generate": "生成",
|
||||||
|
"undo": "元に戻す",
|
||||||
|
"download_all": "すべてをダウンロード",
|
||||||
|
"add_all_to_playlist": "すべてをプレイリストに追加",
|
||||||
|
"add_all_to_queue": "すべてをキューに追加",
|
||||||
|
"play_all_next": "次にすべてを再生",
|
||||||
|
"pause": "一時停止",
|
||||||
|
"view_all": "すべてを見る",
|
||||||
|
"no_tracks_added_yet": "まだ曲を追加していないようです",
|
||||||
|
"no_tracks": "ここには曲がないようです",
|
||||||
|
"no_tracks_listened_yet": "まだ何も聞いていないようです",
|
||||||
|
"not_following_artists": "アーティストをフォローしていません",
|
||||||
|
"no_favorite_albums_yet": "まだお気に入りのアルバムを追加していないようです",
|
||||||
|
"no_logs_found": "ログが見つかりませんでした",
|
||||||
|
"youtube_engine": "YouTubeエンジン",
|
||||||
|
"youtube_engine_not_installed_title": "{engine}はインストールされていません",
|
||||||
|
"youtube_engine_not_installed_message": "{engine}はシステムにインストールされていません。",
|
||||||
|
"youtube_engine_set_path": "PATH変数に設定されていることを確認するか\n{engine}実行ファイルの絶対パスを下記に設定してください",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/Unix系OSでは、.zshrc/.bashrc/.bash_profileなどでパスを設定しても動作しません。\nシェルの設定ファイルにパスを設定する必要があります",
|
||||||
|
"download": "ダウンロード",
|
||||||
|
"file_not_found": "ファイルが見つかりません",
|
||||||
|
"custom": "カスタム",
|
||||||
|
"add_custom_url": "カスタムURLを追加"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "ქეშირებული ფაილების ექსპორტი",
|
"export_cache_files": "ქეშირებული ფაილების ექსპორტი",
|
||||||
"found_n_files": "ნაპოვნია {count} ფაილი",
|
"found_n_files": "ნაპოვნია {count} ფაილი",
|
||||||
"export_cache_confirmation": "გსურთ ამ ფაილების ექსპორტი",
|
"export_cache_confirmation": "გსურთ ამ ფაილების ექსპორტი",
|
||||||
"exported_n_out_of_m_files": "{filesExported} ფაილი {files}-დან ექსპორტირებულია"
|
"exported_n_out_of_m_files": "{filesExported} ფაილი {files}-დან ექსპორტირებულია",
|
||||||
|
"playlist": "პლეისთი",
|
||||||
|
"no_loop": "არ არის ციკლი",
|
||||||
|
"generate": "გააგენერირეთ",
|
||||||
|
"undo": "დაბრუნება",
|
||||||
|
"download_all": "ყველას ჩამოტვირთვა",
|
||||||
|
"add_all_to_playlist": "ყველა დაამატეთ პლეისთში",
|
||||||
|
"add_all_to_queue": "ყველა დაამატეთ რიგში",
|
||||||
|
"play_all_next": "ყველა შემდეგ ითამაშე",
|
||||||
|
"pause": "შეჩერება",
|
||||||
|
"view_all": "ყველა ნახვა",
|
||||||
|
"no_tracks_added_yet": "გაჩნდება რომ ჯერ არ გაქვთ დამატებული ტრეკები",
|
||||||
|
"no_tracks": "გავლებული არ ჩანს არ არსებობს ტრეკები",
|
||||||
|
"no_tracks_listened_yet": "გქონდეთ გრძნობა, რომ ჯერ არაფერი უსმენია",
|
||||||
|
"not_following_artists": "არ მიჰყვებით რომელიმე არტისტს",
|
||||||
|
"no_favorite_albums_yet": "გაჩნდება რომ ჯერ არ გაქვთ დამატებული ალბომები თქვენს ფავორიტებში",
|
||||||
|
"no_logs_found": "ჩაწერები ვერ მოიძებნა",
|
||||||
|
"youtube_engine": "YouTube ძრავა",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} არ არის ინსტალირებული",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} არ არის ინსტალირებული თქვენს სისტემაში.",
|
||||||
|
"youtube_engine_set_path": "დარწმუნდით, რომ ის ხელმისაწვდომია PATH ცვლადში ან\nდაუყავით {engine} პროგრამის ფაილის სრული გზა",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/Unix მსგავსი ოპერაციული სისტემებში, .zshrc/.bashrc/.bash_profile-ით პათის დაყენება ვერ იმუშავებს.\nთქვენ უნდა დააყენოთ პათი შელ ფაილში",
|
||||||
|
"download": "ჩამოტვირთვა",
|
||||||
|
"file_not_found": "ფაილი ვერ მოიძებნა",
|
||||||
|
"custom": "პერსონალიზირებული",
|
||||||
|
"add_custom_url": "დამატება პერსონალური URL"
|
||||||
}
|
}
|
||||||
@ -402,5 +402,30 @@
|
|||||||
"export_cache_files": "캐시된 파일 내보내기",
|
"export_cache_files": "캐시된 파일 내보내기",
|
||||||
"found_n_files": "{count}개의 파일을 찾았습니다",
|
"found_n_files": "{count}개의 파일을 찾았습니다",
|
||||||
"export_cache_confirmation": "이 파일들을 내보내시겠습니까",
|
"export_cache_confirmation": "이 파일들을 내보내시겠습니까",
|
||||||
"exported_n_out_of_m_files": "{files}개 중 {filesExported}개 파일을 내보냈습니다"
|
"exported_n_out_of_m_files": "{files}개 중 {filesExported}개 파일을 내보냈습니다",
|
||||||
|
"playlist": "재생 목록",
|
||||||
|
"no_loop": "반복 없음",
|
||||||
|
"generate": "생성",
|
||||||
|
"undo": "실행 취소",
|
||||||
|
"download_all": "모두 다운로드",
|
||||||
|
"add_all_to_playlist": "모두 재생 목록에 추가",
|
||||||
|
"add_all_to_queue": "모두 큐에 추가",
|
||||||
|
"play_all_next": "모두 다음에 재생",
|
||||||
|
"pause": "일시 정지",
|
||||||
|
"view_all": "모두 보기",
|
||||||
|
"no_tracks_added_yet": "아직 트랙을 추가하지 않은 것 같습니다",
|
||||||
|
"no_tracks": "여기에 트랙이 없는 것 같습니다",
|
||||||
|
"no_tracks_listened_yet": "아직 아무 것도 듣지 않은 것 같습니다",
|
||||||
|
"not_following_artists": "아티스트를 팔로우하지 않고 있습니다",
|
||||||
|
"no_favorite_albums_yet": "아직 즐겨찾기 앨범을 추가하지 않은 것 같습니다",
|
||||||
|
"no_logs_found": "로그를 찾을 수 없습니다",
|
||||||
|
"youtube_engine": "YouTube 엔진",
|
||||||
|
"youtube_engine_not_installed_title": "{engine}가 설치되지 않았습니다",
|
||||||
|
"youtube_engine_not_installed_message": "{engine}가 시스템에 설치되지 않았습니다.",
|
||||||
|
"youtube_engine_set_path": "PATH 변수에서 사용할 수 있는지 확인하거나\n아래에 {engine} 실행 파일의 절대 경로를 설정하세요",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/unix와 같은 운영 체제에서는 .zshrc/.bashrc/.bash_profile 등에 경로 설정이 작동하지 않습니다.\n셸 구성 파일에 경로를 설정해야 합니다",
|
||||||
|
"download": "다운로드",
|
||||||
|
"file_not_found": "파일을 찾을 수 없습니다",
|
||||||
|
"custom": "사용자 정의",
|
||||||
|
"add_custom_url": "사용자 정의 URL 추가"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "क्यास फाइलहरू निर्यात गर्नुहोस्",
|
"export_cache_files": "क्यास फाइलहरू निर्यात गर्नुहोस्",
|
||||||
"found_n_files": "{count} फाइलहरू फेला परे",
|
"found_n_files": "{count} फाइलहरू फेला परे",
|
||||||
"export_cache_confirmation": "यी फाइलहरू निर्यात गर्न चाहनुहुन्छ",
|
"export_cache_confirmation": "यी फाइलहरू निर्यात गर्न चाहनुहुन्छ",
|
||||||
"exported_n_out_of_m_files": "{filesExported} मध्ये {files} फाइलहरू निर्यात गरियो"
|
"exported_n_out_of_m_files": "{filesExported} मध्ये {files} फाइलहरू निर्यात गरियो",
|
||||||
|
"playlist": "प्लेलिस्ट",
|
||||||
|
"no_loop": "कोई लूप नहीं",
|
||||||
|
"generate": "जनरेट",
|
||||||
|
"undo": "पूर्ववत",
|
||||||
|
"download_all": "सभी डाउनलोड करें",
|
||||||
|
"add_all_to_playlist": "सभी को प्लेलिस्ट में जोड़ें",
|
||||||
|
"add_all_to_queue": "सभी को कतार में जोड़ें",
|
||||||
|
"play_all_next": "सभी को अगला प्ले करें",
|
||||||
|
"pause": "विराम",
|
||||||
|
"view_all": "सभी देखें",
|
||||||
|
"no_tracks_added_yet": "लगता है आपने अभी तक कोई ट्रैक नहीं जोड़ा है",
|
||||||
|
"no_tracks": "यहाँ कोई ट्रैक नहीं दिख रहे हैं",
|
||||||
|
"no_tracks_listened_yet": "आपने अभी तक कुछ नहीं सुना है ऐसा लगता है",
|
||||||
|
"not_following_artists": "आप किसी कलाकार को फॉलो नहीं कर रहे हैं",
|
||||||
|
"no_favorite_albums_yet": "लगता है आपने अभी तक कोई एल्बम पसंदीदा में नहीं जोड़ा है",
|
||||||
|
"no_logs_found": "कोई लॉग नहीं मिला",
|
||||||
|
"youtube_engine": "YouTube इंजन",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} इंस्टॉल नहीं है",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} आपके सिस्टम में इंस्टॉल नहीं है।",
|
||||||
|
"youtube_engine_set_path": "सुनिश्चित करें कि यह PATH वेरिएबल में उपलब्ध है या\nनीचे {engine} एक्जीक्यूटेबल का पूर्ण पथ सेट करें",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/unix जैसे ऑपरेटिंग सिस्टम में, .zshrc/.bashrc/.bash_profile आदि में पथ सेट करना काम नहीं करेगा।\nआपको शेल कॉन्फ़िगरेशन फ़ाइल में पथ सेट करना होगा",
|
||||||
|
"download": "डाउनलोड",
|
||||||
|
"file_not_found": "फ़ाइल नहीं मिली",
|
||||||
|
"custom": "कस्टम",
|
||||||
|
"add_custom_url": "कस्टम URL जोड़ें"
|
||||||
}
|
}
|
||||||
@ -402,5 +402,30 @@
|
|||||||
"export_cache_files": "Gecacheerde bestanden exporteren",
|
"export_cache_files": "Gecacheerde bestanden exporteren",
|
||||||
"found_n_files": "{count} bestanden gevonden",
|
"found_n_files": "{count} bestanden gevonden",
|
||||||
"export_cache_confirmation": "Wilt u deze bestanden exporteren naar",
|
"export_cache_confirmation": "Wilt u deze bestanden exporteren naar",
|
||||||
"exported_n_out_of_m_files": "{filesExported} van de {files} bestanden geëxporteerd"
|
"exported_n_out_of_m_files": "{filesExported} van de {files} bestanden geëxporteerd",
|
||||||
|
"playlist": "Afspeellijst",
|
||||||
|
"no_loop": "Geen herhaling",
|
||||||
|
"generate": "Genereren",
|
||||||
|
"undo": "Ongedaan maken",
|
||||||
|
"download_all": "Alles downloaden",
|
||||||
|
"add_all_to_playlist": "Voeg alles toe aan afspeellijst",
|
||||||
|
"add_all_to_queue": "Voeg alles toe aan wachtrij",
|
||||||
|
"play_all_next": "Speel alles volgende",
|
||||||
|
"pause": "Pauzeren",
|
||||||
|
"view_all": "Bekijk alles",
|
||||||
|
"no_tracks_added_yet": "Het lijkt erop dat je nog geen nummers hebt toegevoegd",
|
||||||
|
"no_tracks": "Het lijkt erop dat er hier geen nummers zijn",
|
||||||
|
"no_tracks_listened_yet": "Het lijkt erop dat je nog niets hebt beluisterd",
|
||||||
|
"not_following_artists": "Je volgt geen artiesten",
|
||||||
|
"no_favorite_albums_yet": "Het lijkt erop dat je nog geen albums aan je favorieten hebt toegevoegd",
|
||||||
|
"no_logs_found": "Geen logbestanden gevonden",
|
||||||
|
"youtube_engine": "YouTube Engine",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} is niet geïnstalleerd",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} is niet geïnstalleerd op je systeem.",
|
||||||
|
"youtube_engine_set_path": "Zorg ervoor dat het beschikbaar is in de PATH-variabele of\nstel het absolute pad naar de {engine} uitvoerbare bestanden in",
|
||||||
|
"youtube_engine_unix_issue_message": "Op macOS/Linux/unix-achtige besturingssystemen werkt het instellen van paden in .zshrc/.bashrc/.bash_profile enz. niet.\nJe moet het pad instellen in het shell-configuratiebestand",
|
||||||
|
"download": "Downloaden",
|
||||||
|
"file_not_found": "Bestand niet gevonden",
|
||||||
|
"custom": "Aangepast",
|
||||||
|
"add_custom_url": "Voeg aangepaste URL toe"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Eksportuj pliki z pamięci podręcznej",
|
"export_cache_files": "Eksportuj pliki z pamięci podręcznej",
|
||||||
"found_n_files": "Znaleziono {count} plików",
|
"found_n_files": "Znaleziono {count} plików",
|
||||||
"export_cache_confirmation": "Czy chcesz wyeksportować te pliki do",
|
"export_cache_confirmation": "Czy chcesz wyeksportować te pliki do",
|
||||||
"exported_n_out_of_m_files": "Wyeksportowano {filesExported} z {files} plików"
|
"exported_n_out_of_m_files": "Wyeksportowano {filesExported} z {files} plików",
|
||||||
|
"playlist": "Playlista",
|
||||||
|
"no_loop": "Brak pętli",
|
||||||
|
"generate": "Generuj",
|
||||||
|
"undo": "Cofnij",
|
||||||
|
"download_all": "Pobierz wszystko",
|
||||||
|
"add_all_to_playlist": "Dodaj wszystko do playlisty",
|
||||||
|
"add_all_to_queue": "Dodaj wszystko do kolejki",
|
||||||
|
"play_all_next": "Odtwórz wszystko następnie",
|
||||||
|
"pause": "Pauza",
|
||||||
|
"view_all": "Zobacz wszystko",
|
||||||
|
"no_tracks_added_yet": "Wygląda na to, że jeszcze nie dodałeś żadnych utworów",
|
||||||
|
"no_tracks": "Wygląda na to, że tutaj nie ma żadnych utworów",
|
||||||
|
"no_tracks_listened_yet": "Wygląda na to, że jeszcze nic nie słuchałeś",
|
||||||
|
"not_following_artists": "Nie obserwujesz żadnych artystów",
|
||||||
|
"no_favorite_albums_yet": "Wygląda na to, że jeszcze nie dodałeś żadnych albumów do ulubionych",
|
||||||
|
"no_logs_found": "Nie znaleziono żadnych logów",
|
||||||
|
"youtube_engine": "Silnik YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} nie jest zainstalowany",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} nie jest zainstalowany w systemie.",
|
||||||
|
"youtube_engine_set_path": "Upewnij się, że jest dostępny w zmiennej PATH lub\nustaw absolutną ścieżkę do pliku wykonywalnego {engine} poniżej",
|
||||||
|
"youtube_engine_unix_issue_message": "W systemach macOS/Linux/unix, ustawianie ścieżki w .zshrc/.bashrc/.bash_profile itp. nie będzie działać.\nMusisz ustawić ścieżkę w pliku konfiguracyjnym powłoki",
|
||||||
|
"download": "Pobierz",
|
||||||
|
"file_not_found": "Plik nie znaleziony",
|
||||||
|
"custom": "Niestandardowy",
|
||||||
|
"add_custom_url": "Dodaj niestandardowy URL"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Exportar Arquivos em Cache",
|
"export_cache_files": "Exportar Arquivos em Cache",
|
||||||
"found_n_files": "Encontrados {count} arquivos",
|
"found_n_files": "Encontrados {count} arquivos",
|
||||||
"export_cache_confirmation": "Deseja exportar estes arquivos para",
|
"export_cache_confirmation": "Deseja exportar estes arquivos para",
|
||||||
"exported_n_out_of_m_files": "Exportados {filesExported} de {files} arquivos"
|
"exported_n_out_of_m_files": "Exportados {filesExported} de {files} arquivos",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"no_loop": "Sem loop",
|
||||||
|
"generate": "Gerar",
|
||||||
|
"undo": "Desfazer",
|
||||||
|
"download_all": "Baixar tudo",
|
||||||
|
"add_all_to_playlist": "Adicionar tudo à playlist",
|
||||||
|
"add_all_to_queue": "Adicionar tudo à fila",
|
||||||
|
"play_all_next": "Reproduzir tudo a seguir",
|
||||||
|
"pause": "Pausar",
|
||||||
|
"view_all": "Ver tudo",
|
||||||
|
"no_tracks_added_yet": "Parece que você ainda não adicionou nenhuma faixa",
|
||||||
|
"no_tracks": "Parece que não há faixas aqui",
|
||||||
|
"no_tracks_listened_yet": "Parece que você ainda não ouviu nada",
|
||||||
|
"not_following_artists": "Você não está seguindo nenhum artista",
|
||||||
|
"no_favorite_albums_yet": "Parece que você ainda não adicionou nenhum álbum aos favoritos",
|
||||||
|
"no_logs_found": "Nenhum log encontrado",
|
||||||
|
"youtube_engine": "Motor YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} não está instalado",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} não está instalado no seu sistema.",
|
||||||
|
"youtube_engine_set_path": "Certifique-se de que está disponível na variável PATH ou\ndefina o caminho absoluto para o executável {engine} abaixo",
|
||||||
|
"youtube_engine_unix_issue_message": "Em sistemas macOS/Linux/unix, definir o caminho no .zshrc/.bashrc/.bash_profile etc. não funcionará.\nVocê precisa definir o caminho no arquivo de configuração do shell",
|
||||||
|
"download": "Baixar",
|
||||||
|
"file_not_found": "Arquivo não encontrado",
|
||||||
|
"custom": "Personalizado",
|
||||||
|
"add_custom_url": "Adicionar URL personalizada"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Экспортировать кэшированные файлы",
|
"export_cache_files": "Экспортировать кэшированные файлы",
|
||||||
"found_n_files": "Найдено {count} файлов",
|
"found_n_files": "Найдено {count} файлов",
|
||||||
"export_cache_confirmation": "Вы хотите экспортировать эти файлы в",
|
"export_cache_confirmation": "Вы хотите экспортировать эти файлы в",
|
||||||
"exported_n_out_of_m_files": "Экспортировано {filesExported} из {files} файлов"
|
"exported_n_out_of_m_files": "Экспортировано {filesExported} из {files} файлов",
|
||||||
|
"playlist": "Плейлист",
|
||||||
|
"no_loop": "Без повтора",
|
||||||
|
"generate": "Генерировать",
|
||||||
|
"undo": "Отменить",
|
||||||
|
"download_all": "Скачать все",
|
||||||
|
"add_all_to_playlist": "Добавить все в плейлист",
|
||||||
|
"add_all_to_queue": "Добавить все в очередь",
|
||||||
|
"play_all_next": "Воспроизвести все следующее",
|
||||||
|
"pause": "Пауза",
|
||||||
|
"view_all": "Просмотреть все",
|
||||||
|
"no_tracks_added_yet": "Похоже, вы ещё не добавили ни одного трека",
|
||||||
|
"no_tracks": "Похоже, здесь нет треков",
|
||||||
|
"no_tracks_listened_yet": "Похоже, вы ещё ничего не слушали",
|
||||||
|
"not_following_artists": "Вы не подписаны на художников",
|
||||||
|
"no_favorite_albums_yet": "Похоже, вы ещё не добавили ни одного альбома в избранное",
|
||||||
|
"no_logs_found": "Логи не найдены",
|
||||||
|
"youtube_engine": "YouTube Движок",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} не установлен",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} не установлен в вашей системе.",
|
||||||
|
"youtube_engine_set_path": "Убедитесь, что он доступен в переменной PATH или\nустановите абсолютный путь к исполнимому файлу {engine} ниже",
|
||||||
|
"youtube_engine_unix_issue_message": "В macOS/Linux/Unix-подобных ОС, установка пути в .zshrc/.bashrc/.bash_profile и т.д. не будет работать.\nВы должны установить путь в файле конфигурации оболочки",
|
||||||
|
"download": "Скачать",
|
||||||
|
"file_not_found": "Файл не найден",
|
||||||
|
"custom": "Пользовательский",
|
||||||
|
"add_custom_url": "Добавить пользовательский URL"
|
||||||
}
|
}
|
||||||
@ -402,5 +402,30 @@
|
|||||||
"export_cache_files": "ส่งออกไฟล์แคช",
|
"export_cache_files": "ส่งออกไฟล์แคช",
|
||||||
"found_n_files": "พบ {count} ไฟล์",
|
"found_n_files": "พบ {count} ไฟล์",
|
||||||
"export_cache_confirmation": "คุณต้องการส่งออกไฟล์เหล่านี้ไปยัง",
|
"export_cache_confirmation": "คุณต้องการส่งออกไฟล์เหล่านี้ไปยัง",
|
||||||
"exported_n_out_of_m_files": "ส่งออก {filesExported} จาก {files} ไฟล์"
|
"exported_n_out_of_m_files": "ส่งออก {filesExported} จาก {files} ไฟล์",
|
||||||
|
"playlist": "เพลย์ลิสต์",
|
||||||
|
"no_loop": "ไม่มีการวนซ้ำ",
|
||||||
|
"generate": "สร้าง",
|
||||||
|
"undo": "ย้อนกลับ",
|
||||||
|
"download_all": "ดาวน์โหลดทั้งหมด",
|
||||||
|
"add_all_to_playlist": "เพิ่มทั้งหมดในเพลย์ลิสต์",
|
||||||
|
"add_all_to_queue": "เพิ่มทั้งหมดในคิว",
|
||||||
|
"play_all_next": "เล่นทั้งหมดถัดไป",
|
||||||
|
"pause": "หยุดชั่วคราว",
|
||||||
|
"view_all": "ดูทั้งหมด",
|
||||||
|
"no_tracks_added_yet": "ดูเหมือนคุณยังไม่ได้เพิ่มเพลงใด ๆ",
|
||||||
|
"no_tracks": "ดูเหมือนจะไม่มีเพลงที่นี่",
|
||||||
|
"no_tracks_listened_yet": "ดูเหมือนคุณยังไม่ได้ฟังอะไรเลย",
|
||||||
|
"not_following_artists": "คุณไม่ได้ติดตามศิลปินใด ๆ",
|
||||||
|
"no_favorite_albums_yet": "ดูเหมือนคุณยังไม่ได้เพิ่มอัลบัมใด ๆ ในรายการโปรด",
|
||||||
|
"no_logs_found": "ไม่พบบันทึก",
|
||||||
|
"youtube_engine": "เครื่องมือ YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} ยังไม่ได้ติดตั้ง",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} ยังไม่ได้ติดตั้งในระบบของคุณ",
|
||||||
|
"youtube_engine_set_path": "ตรวจสอบให้แน่ใจว่ามันมีอยู่ในตัวแปร PATH หรือ\nตั้งค่าพาธที่แท้จริงของไฟล์ที่สามารถทำงานได้ {engine} ด้านล่าง",
|
||||||
|
"youtube_engine_unix_issue_message": "ใน macOS/Linux/Unix อย่าง OS การตั้งค่าพาธใน .zshrc/.bashrc/.bash_profile เป็นต้น จะไม่ทำงาน\nคุณต้องตั้งค่าพาธในไฟล์การกำหนดค่า shell",
|
||||||
|
"download": "ดาวน์โหลด",
|
||||||
|
"file_not_found": "ไม่พบไฟล์",
|
||||||
|
"custom": "กำหนดเอง",
|
||||||
|
"add_custom_url": "เพิ่ม URL แบบกำหนดเอง"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Önbelleğe Alınmış Dosyaları Dışa Aktar",
|
"export_cache_files": "Önbelleğe Alınmış Dosyaları Dışa Aktar",
|
||||||
"found_n_files": "{count} dosya bulundu",
|
"found_n_files": "{count} dosya bulundu",
|
||||||
"export_cache_confirmation": "Bu dosyaları dışa aktarmak istiyor musunuz",
|
"export_cache_confirmation": "Bu dosyaları dışa aktarmak istiyor musunuz",
|
||||||
"exported_n_out_of_m_files": "{filesExported} / {files} dosya dışa aktarıldı"
|
"exported_n_out_of_m_files": "{filesExported} / {files} dosya dışa aktarıldı",
|
||||||
|
"playlist": "Çalma Listesi",
|
||||||
|
"no_loop": "Dönüş Yok",
|
||||||
|
"generate": "Oluştur",
|
||||||
|
"undo": "Geri Al",
|
||||||
|
"download_all": "Tümünü İndir",
|
||||||
|
"add_all_to_playlist": "Hepsini çalma listesine ekle",
|
||||||
|
"add_all_to_queue": "Hepsini kuyruğa ekle",
|
||||||
|
"play_all_next": "Hepsini bir sonraki çal",
|
||||||
|
"pause": "Duraklat",
|
||||||
|
"view_all": "Tümünü Gör",
|
||||||
|
"no_tracks_added_yet": "Henüz hiçbir şarkı eklemediniz gibi görünüyor",
|
||||||
|
"no_tracks": "Burada hiç şarkı yok gibi görünüyor",
|
||||||
|
"no_tracks_listened_yet": "Henüz hiçbir şey dinlemediniz gibi görünüyor",
|
||||||
|
"not_following_artists": "Hiçbir sanatçıyı takip etmiyorsunuz",
|
||||||
|
"no_favorite_albums_yet": "Henüz favorilerinize herhangi bir albüm eklemediniz gibi görünüyor",
|
||||||
|
"no_logs_found": "Log bulunamadı",
|
||||||
|
"youtube_engine": "YouTube Motoru",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} Yüklü değil",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} sisteminizde yüklü değil.",
|
||||||
|
"youtube_engine_set_path": "PATH değişkeninde kullanılabilir olduğundan emin olun veya\n{engine} çalıştırılabilir dosyasının mutlak yolunu aşağıda ayarlayın",
|
||||||
|
"youtube_engine_unix_issue_message": "macOS/Linux/Unix benzeri işletim sistemlerinde, .zshrc/.bashrc/.bash_profile gibi dosyalarda yol ayarlamak işe yaramaz.\nYolunuzu kabuk yapılandırma dosyasına ayarlamanız gerekir",
|
||||||
|
"download": "İndir",
|
||||||
|
"file_not_found": "Dosya bulunamadı",
|
||||||
|
"custom": "Özel",
|
||||||
|
"add_custom_url": "Özel URL ekle"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Експортувати кешовані файли",
|
"export_cache_files": "Експортувати кешовані файли",
|
||||||
"found_n_files": "Знайдено {count} файлів",
|
"found_n_files": "Знайдено {count} файлів",
|
||||||
"export_cache_confirmation": "Ви хочете експортувати ці файли до",
|
"export_cache_confirmation": "Ви хочете експортувати ці файли до",
|
||||||
"exported_n_out_of_m_files": "Експортовано {filesExported} з {files} файлів"
|
"exported_n_out_of_m_files": "Експортовано {filesExported} з {files} файлів",
|
||||||
|
"playlist": "Плейлист",
|
||||||
|
"no_loop": "Без повтору",
|
||||||
|
"generate": "Генерувати",
|
||||||
|
"undo": "Скасувати",
|
||||||
|
"download_all": "Завантажити все",
|
||||||
|
"add_all_to_playlist": "Додати все до плейлиста",
|
||||||
|
"add_all_to_queue": "Додати все в чергу",
|
||||||
|
"play_all_next": "Відтворити все наступне",
|
||||||
|
"pause": "Пауза",
|
||||||
|
"view_all": "Переглянути все",
|
||||||
|
"no_tracks_added_yet": "Здається, ви ще не додали жодної пісні",
|
||||||
|
"no_tracks": "Здається, тут немає пісень",
|
||||||
|
"no_tracks_listened_yet": "Здається, ви ще нічого не слухали",
|
||||||
|
"not_following_artists": "Ви не підписані на жодного артиста",
|
||||||
|
"no_favorite_albums_yet": "Здається, ви ще не додали жодного альбому в улюблені",
|
||||||
|
"no_logs_found": "Жодних журналів не знайдено",
|
||||||
|
"youtube_engine": "YouTube Двигун",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} не встановлено",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} не встановлено на вашій системі.",
|
||||||
|
"youtube_engine_set_path": "Переконайтесь, що він доступний у змінній PATH або\nвстановіть абсолютний шлях до виконуваного файлу {engine} нижче",
|
||||||
|
"youtube_engine_unix_issue_message": "У macOS/Linux/Unix-подібних ОС, встановлення шляху в .zshrc/.bashrc/.bash_profile тощо не працює.\nВам потрібно налаштувати шлях у файлі конфігурації оболонки",
|
||||||
|
"download": "Завантажити",
|
||||||
|
"file_not_found": "Файл не знайдено",
|
||||||
|
"custom": "Користувацький",
|
||||||
|
"add_custom_url": "Додати користувацький URL"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "Xuất các tệp được lưu trong bộ nhớ đệm",
|
"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",
|
"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",
|
"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"
|
"exported_n_out_of_m_files": "Đã xuất {filesExported} trên {files} tệp",
|
||||||
|
"playlist": "Danh sách phát",
|
||||||
|
"no_loop": "Không lặp lại",
|
||||||
|
"generate": "Tạo",
|
||||||
|
"undo": "Hoàn tác",
|
||||||
|
"download_all": "Tải xuống tất cả",
|
||||||
|
"add_all_to_playlist": "Thêm tất cả vào danh sách phát",
|
||||||
|
"add_all_to_queue": "Thêm tất cả vào danh sách chờ",
|
||||||
|
"play_all_next": "Chơi tất cả tiếp theo",
|
||||||
|
"pause": "Tạm dừng",
|
||||||
|
"view_all": "Xem tất cả",
|
||||||
|
"no_tracks_added_yet": "Có vẻ bạn chưa thêm bất kỳ bài hát nào",
|
||||||
|
"no_tracks": "Có vẻ không có bài hát nào ở đây",
|
||||||
|
"no_tracks_listened_yet": "Có vẻ bạn chưa nghe gì cả",
|
||||||
|
"not_following_artists": "Bạn không đang theo dõi bất kỳ nghệ sĩ nào",
|
||||||
|
"no_favorite_albums_yet": "Có vẻ bạn chưa thêm album nào vào danh sách yêu thích",
|
||||||
|
"no_logs_found": "Không tìm thấy nhật ký",
|
||||||
|
"youtube_engine": "Công cụ YouTube",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} chưa được cài đặt",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} chưa được cài đặt trong hệ thống của bạn.",
|
||||||
|
"youtube_engine_set_path": "Đảm bảo nó có sẵn trong biến PATH hoặc\nđặt đường dẫn tuyệt đối đến tệp thực thi {engine} dưới đây",
|
||||||
|
"youtube_engine_unix_issue_message": "Trên macOS/Linux/Unix, việc thiết lập đường dẫn trong .zshrc/.bashrc/.bash_profile v.v. sẽ không hoạt động.\nBạn cần thiết lập đường dẫn trong tệp cấu hình shell",
|
||||||
|
"download": "Tải xuống",
|
||||||
|
"file_not_found": "Không tìm thấy tệp",
|
||||||
|
"custom": "Tùy chỉnh",
|
||||||
|
"add_custom_url": "Thêm URL tùy chỉnh"
|
||||||
}
|
}
|
||||||
@ -401,5 +401,30 @@
|
|||||||
"export_cache_files": "导出缓存文件",
|
"export_cache_files": "导出缓存文件",
|
||||||
"found_n_files": "找到 {count} 个文件",
|
"found_n_files": "找到 {count} 个文件",
|
||||||
"export_cache_confirmation": "您要导出这些文件到",
|
"export_cache_confirmation": "您要导出这些文件到",
|
||||||
"exported_n_out_of_m_files": "导出了 {filesExported} / {files} 个文件"
|
"exported_n_out_of_m_files": "导出了 {filesExported} / {files} 个文件",
|
||||||
|
"playlist": "播放列表",
|
||||||
|
"no_loop": "无循环",
|
||||||
|
"generate": "生成",
|
||||||
|
"undo": "撤销",
|
||||||
|
"download_all": "下载全部",
|
||||||
|
"add_all_to_playlist": "将全部添加到播放列表",
|
||||||
|
"add_all_to_queue": "将全部添加到队列",
|
||||||
|
"play_all_next": "播放全部下一首",
|
||||||
|
"pause": "暂停",
|
||||||
|
"view_all": "查看所有",
|
||||||
|
"no_tracks_added_yet": "看起来你还没有添加任何曲目",
|
||||||
|
"no_tracks": "看起来这里没有任何曲目",
|
||||||
|
"no_tracks_listened_yet": "看起来你还没有听任何东西",
|
||||||
|
"not_following_artists": "你没有关注任何艺术家",
|
||||||
|
"no_favorite_albums_yet": "看起来你还没有将任何专辑添加到收藏夹",
|
||||||
|
"no_logs_found": "未找到日志",
|
||||||
|
"youtube_engine": "YouTube 引擎",
|
||||||
|
"youtube_engine_not_installed_title": "{engine} 未安装",
|
||||||
|
"youtube_engine_not_installed_message": "{engine} 未在您的系统中安装。",
|
||||||
|
"youtube_engine_set_path": "确保它可用在 PATH 变量中,或\n设置 {engine} 可执行文件的绝对路径",
|
||||||
|
"youtube_engine_unix_issue_message": "在 macOS/Linux/Unix 类操作系统中,在 .zshrc/.bashrc/.bash_profile 等文件中设置路径无效。\n您需要在 shell 配置文件中设置路径",
|
||||||
|
"download": "下载",
|
||||||
|
"file_not_found": "文件未找到",
|
||||||
|
"custom": "自定义",
|
||||||
|
"add_custom_url": "添加自定义 URL"
|
||||||
}
|
}
|
||||||
@ -224,6 +224,11 @@ class Spotube extends HookConsumerWidget {
|
|||||||
surfaceBlur: 10,
|
surfaceBlur: 10,
|
||||||
),
|
),
|
||||||
materialTheme: material.ThemeData(
|
materialTheme: material.ThemeData(
|
||||||
|
brightness: switch (themeMode) {
|
||||||
|
ThemeMode.system => MediaQuery.platformBrightnessOf(context),
|
||||||
|
ThemeMode.light => Brightness.light,
|
||||||
|
ThemeMode.dark => Brightness.dark,
|
||||||
|
},
|
||||||
splashFactory: material.NoSplash.splashFactory,
|
splashFactory: material.NoSplash.splashFactory,
|
||||||
appBarTheme: const material.AppBarTheme(
|
appBarTheme: const material.AppBarTheme(
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
|
|||||||
@ -52,6 +52,7 @@ class ConnectPageLocalDevices extends HookWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SliverGap(200)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart';
|
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
@ -21,10 +22,12 @@ class RecommendationAttributeFields extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final minController = useTextEditingController(text: values.min.toString());
|
final minController =
|
||||||
|
useShadcnTextEditingController(text: values.min.toString());
|
||||||
final targetController =
|
final targetController =
|
||||||
useTextEditingController(text: values.target.toString());
|
useShadcnTextEditingController(text: values.target.toString());
|
||||||
final maxController = useTextEditingController(text: values.max.toString());
|
final maxController =
|
||||||
|
useShadcnTextEditingController(text: values.max.toString());
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
listener() {
|
listener() {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter/material.dart' show Autocomplete;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
|
|
||||||
enum SelectedItemDisplayType {
|
enum SelectedItemDisplayType {
|
||||||
wrap,
|
wrap,
|
||||||
@ -49,7 +50,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
|
|||||||
useValueListenable(seeds);
|
useValueListenable(seeds);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final seedController = useTextEditingController();
|
final seedController = useShadcnTextEditingController();
|
||||||
|
|
||||||
final containerKey = useRef(GlobalKey());
|
final containerKey = useRef(GlobalKey());
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.dart' show showModalBottomSheet;
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
|
||||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
@ -13,7 +11,6 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/framework/app_pop_scope.dart';
|
import 'package:spotube/components/framework/app_pop_scope.dart';
|
||||||
import 'package:spotube/modules/player/player_actions.dart';
|
import 'package:spotube/modules/player/player_actions.dart';
|
||||||
import 'package:spotube/modules/player/player_controls.dart';
|
import 'package:spotube/modules/player/player_controls.dart';
|
||||||
import 'package:spotube/modules/player/player_queue.dart';
|
|
||||||
import 'package:spotube/modules/player/volume_slider.dart';
|
import 'package:spotube/modules/player/volume_slider.dart';
|
||||||
import 'package:spotube/components/dialogs/track_details_dialog.dart';
|
import 'package:spotube/components/dialogs/track_details_dialog.dart';
|
||||||
import 'package:spotube/components/links/artist_link.dart';
|
import 'package:spotube/components/links/artist_link.dart';
|
||||||
@ -25,12 +22,12 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -52,7 +49,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
|
||||||
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
||||||
final isLocalTrack = currentTrack is LocalTrack;
|
final isLocalTrack = currentTrack is LocalTrack;
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
final shouldHide = useState(true);
|
final shouldHide = useState(true);
|
||||||
|
|
||||||
@ -105,6 +102,9 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
headers: [
|
headers: [
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
minimum:
|
||||||
|
kIsMobile ? const EdgeInsets.only(top: 80) : EdgeInsets.zero,
|
||||||
|
bottom: false,
|
||||||
child: TitleBar(
|
child: TitleBar(
|
||||||
surfaceOpacity: 0,
|
surfaceOpacity: 0,
|
||||||
surfaceBlur: 0,
|
surfaceBlur: 0,
|
||||||
@ -235,39 +235,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
leading: const Icon(SpotubeIcons.queue),
|
leading: const Icon(SpotubeIcons.queue),
|
||||||
child: Text(context.l10n.queue),
|
child: Text(context.l10n.queue),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
openDrawer(
|
context.pushRoute(const PlayerQueueRoute());
|
||||||
context: context,
|
|
||||||
barrierDismissible: true,
|
|
||||||
draggable: true,
|
|
||||||
barrierColor: Colors.black.withAlpha(100),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
transformBackdrop: false,
|
|
||||||
position: OverlayPosition.bottom,
|
|
||||||
surfaceBlur: context.theme.surfaceBlur,
|
|
||||||
surfaceOpacity: 0.7,
|
|
||||||
expands: true,
|
|
||||||
builder: (context) => Consumer(
|
|
||||||
builder: (context, ref, _) {
|
|
||||||
final playlist = ref.watch(
|
|
||||||
audioPlayerProvider,
|
|
||||||
);
|
|
||||||
final playlistNotifier =
|
|
||||||
ref.read(audioPlayerProvider.notifier);
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight:
|
|
||||||
MediaQuery.of(context).size.height *
|
|
||||||
0.8,
|
|
||||||
),
|
|
||||||
child: PlayerQueue.fromAudioPlayerNotifier(
|
|
||||||
floating: false,
|
|
||||||
playlist: playlist,
|
|
||||||
notifier: playlistNotifier,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -278,22 +246,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
leading: const Icon(SpotubeIcons.music),
|
leading: const Icon(SpotubeIcons.music),
|
||||||
child: Text(context.l10n.lyrics),
|
child: Text(context.l10n.lyrics),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushRoute(const PlayerLyricsRoute());
|
||||||
context: context,
|
|
||||||
isDismissible: true,
|
|
||||||
enableDrag: true,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.black.withAlpha(100),
|
|
||||||
barrierColor: Colors.black.withAlpha(100),
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20),
|
|
||||||
topRight: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
builder: (context) =>
|
|
||||||
const LyricsPage(isModal: true),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
@ -141,30 +143,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
openDrawer(
|
context.pushRoute(const PlayerTrackSourcesRoute());
|
||||||
context: context,
|
|
||||||
position: OverlayPosition.bottom,
|
|
||||||
barrierDismissible: true,
|
|
||||||
draggable: true,
|
|
||||||
barrierColor: Colors.black.withValues(alpha: .2),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
transformBackdrop: false,
|
|
||||||
surfaceBlur: context.theme.surfaceBlur,
|
|
||||||
surfaceOpacity: context.theme.surfaceOpacity,
|
|
||||||
builder: (context) {
|
|
||||||
return Card(
|
|
||||||
borderWidth: 0,
|
|
||||||
borderColor: Colors.transparent,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: screenSize.height * .8,
|
|
||||||
),
|
|
||||||
child: SiblingTracksSheet(floating: floatingQueue),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -212,7 +191,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
sleepTimerNotifier.setSleepTimer(value);
|
sleepTimerNotifier.setSleepTimer(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
children: [
|
items: (context) => [
|
||||||
for (final entry in sleepTimerEntries.entries)
|
for (final entry in sleepTimerEntries.entries)
|
||||||
AdaptiveMenuButton(
|
AdaptiveMenuButton(
|
||||||
value: entry.value,
|
value: entry.value,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
import 'package:spotube/components/fallbacks/not_found.dart';
|
import 'package:spotube/components/fallbacks/not_found.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||||
@ -50,7 +51,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
final controller = useAutoScrollController();
|
final controller = useAutoScrollController();
|
||||||
final searchText = useState('');
|
final searchText = useState('');
|
||||||
@ -91,8 +92,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
final searchBar = ConstrainedBox(
|
final searchBar = ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxHeight: 40,
|
maxHeight: 40,
|
||||||
maxWidth:
|
maxWidth: mediaQuery.smAndDown ? mediaQuery.width - 40 : 300,
|
||||||
mediaQuery.smAndDown ? mediaQuery.size.width - 40 : 300,
|
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -157,7 +157,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
isSearching.value = !isSearching.value;
|
isSearching.value = !isSearching.value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
if (!isSearching.value) ...[
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: TooltipContainer(
|
tooltip: TooltipContainer(
|
||||||
@ -170,6 +170,9 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(5),
|
||||||
|
if (mediaQuery.smAndDown)
|
||||||
|
const BackButton(icon: SpotubeIcons.angleDown),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
@ -13,6 +14,7 @@ import 'package:spotube/extensions/artist_simple.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/hooks/utils/use_debounce.dart';
|
import 'package:spotube/hooks/utils/use_debounce.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
@ -85,7 +87,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final defaultSearchTerm =
|
final defaultSearchTerm =
|
||||||
"$title - ${activeTrack?.artists?.asString() ?? ""}";
|
"$title - ${activeTrack?.artists?.asString() ?? ""}";
|
||||||
final searchController = useTextEditingController(
|
final searchController = useShadcnTextEditingController(
|
||||||
text: defaultSearchTerm,
|
text: defaultSearchTerm,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -243,14 +245,15 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (!isSearching.value)
|
if (!isSearching.value) ...[
|
||||||
IconButton.outline(
|
IconButton.outline(
|
||||||
icon: const Icon(SpotubeIcons.search, size: 18),
|
icon: const Icon(SpotubeIcons.search, size: 18),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isSearching.value = true;
|
isSearching.value = true;
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
else ...[
|
if (!floating) const BackButton(icon: SpotubeIcons.angleDown)
|
||||||
|
] else ...[
|
||||||
if (preferences.audioSource == AudioSource.piped)
|
if (preferences.audioSource == AudioSource.piped)
|
||||||
IconButton.outline(
|
IconButton.outline(
|
||||||
icon: const Icon(SpotubeIcons.filter, size: 18),
|
icon: const Icon(SpotubeIcons.filter, size: 18),
|
||||||
|
|||||||
@ -101,11 +101,17 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
await playlistNotifier.create(payload, onError);
|
await playlistNotifier.create(payload, onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trackIds.isNotEmpty) {
|
||||||
|
await playlistNotifier.addTracks(trackIds, onError);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
if (context.mounted &&
|
if (context.mounted &&
|
||||||
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
||||||
context.router.maybePop();
|
context.router.maybePop<Playlist>(
|
||||||
|
await ref.read(playlistProvider(playlistId ?? "").future),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/form/text_form_field.dart';
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -28,7 +29,7 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final controller = useTextEditingController();
|
final controller = useShadcnTextEditingController();
|
||||||
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
description: context.l10n.summary_owed_to_artists,
|
description: context.l10n.summary_owed_to_artists,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.navigateTo(const StatsStreamsRoute());
|
context.navigateTo(const StatsStreamFeesRoute());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SummaryCard(
|
SummaryCard(
|
||||||
|
|||||||
@ -40,14 +40,20 @@ class StatsPageTopSection extends HookConsumerWidget {
|
|||||||
historyDurationNotifier.update((_) => value);
|
historyDurationNotifier.update((_) => value);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, item) => Text(translations[item]!),
|
itemBuilder: (context, item) => Text(translations[item]!),
|
||||||
children: [
|
popup: (context) {
|
||||||
for (final item in HistoryDuration.values)
|
return SelectPopup(
|
||||||
SelectItemButton(
|
items: SelectItemBuilder(
|
||||||
|
childCount: HistoryDuration.values.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
final item = HistoryDuration.values[index];
|
||||||
|
return SelectItemButton(
|
||||||
value: item,
|
value: item,
|
||||||
child: Text(translations[item]!),
|
child: Text(translations[item]!),
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return SliverLayoutBuilder(builder: (context, constraints) {
|
return SliverLayoutBuilder(builder: (context, constraints) {
|
||||||
return SliverMainAxisGroup(
|
return SliverMainAxisGroup(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
@ -27,7 +28,13 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier);
|
final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier);
|
||||||
final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
|
final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
|
||||||
|
|
||||||
return TrackPresentation(
|
return material.RefreshIndicator.adaptive(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(albumTracksProvider(album));
|
||||||
|
ref.invalidate(favoriteAlbumsProvider);
|
||||||
|
ref.invalidate(albumsIsSavedProvider(album.id!));
|
||||||
|
},
|
||||||
|
child: TrackPresentation(
|
||||||
options: TrackPresentationOptions(
|
options: TrackPresentationOptions(
|
||||||
collection: album,
|
collection: album,
|
||||||
image: album.images.asUrlString(
|
image: album.images.asUrlString(
|
||||||
@ -66,6 +73,7 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
@ -42,6 +43,18 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
floatingHeader: true,
|
floatingHeader: true,
|
||||||
|
child: material.RefreshIndicator.adaptive(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(artistProvider(artistId));
|
||||||
|
ref.invalidate(relatedArtistsProvider(artistId));
|
||||||
|
ref.invalidate(artistAlbumsProvider(artistId));
|
||||||
|
ref.invalidate(artistIsFollowingProvider(artistId));
|
||||||
|
ref.invalidate(artistTopTracksProvider(artistId));
|
||||||
|
if (artistQuery.hasValue) {
|
||||||
|
ref.invalidate(
|
||||||
|
artistWikipediaSummaryProvider(artistQuery.asData!.value));
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
if (artistQuery.hasError && artistQuery.asData?.value == null) {
|
if (artistQuery.hasError && artistQuery.asData?.value == null) {
|
||||||
return Center(child: Text(artistQuery.error.toString()));
|
return Center(child: Text(artistQuery.error.toString()));
|
||||||
@ -74,7 +87,8 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
const SliverGap(20),
|
const SliverGap(20),
|
||||||
if (artistQuery.asData?.value != null)
|
if (artistQuery.asData?.value != null)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: ArtistPageFooter(artist: artistQuery.asData!.value),
|
child:
|
||||||
|
ArtistPageFooter(artist: artistQuery.asData!.value),
|
||||||
),
|
),
|
||||||
const SliverSafeArea(sliver: SliverGap(10)),
|
const SliverSafeArea(sliver: SliverGap(10)),
|
||||||
],
|
],
|
||||||
@ -82,6 +96,7 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,9 +84,14 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
|||||||
Text(value.name.capitalize()),
|
Text(value.name.capitalize()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
children: [
|
popup: (context) {
|
||||||
for (final source in AudioSource.values)
|
return SelectPopup(
|
||||||
SelectItemButton(
|
items: SelectItemBuilder(
|
||||||
|
childCount: AudioSource.values.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
final source = AudioSource.values[index];
|
||||||
|
|
||||||
|
return SelectItemButton(
|
||||||
value: source,
|
value: source,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -96,8 +101,11 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
|||||||
Text(source.name.capitalize()),
|
Text(source.name.capitalize()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@ -14,6 +14,22 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
|||||||
const GettingStartedPageLanguageRegionSection(
|
const GettingStartedPageLanguageRegionSection(
|
||||||
{super.key, required this.onNext});
|
{super.key, required this.onNext});
|
||||||
|
|
||||||
|
bool filterMarkets(Market item, String query) {
|
||||||
|
final market = spotifyMarkets
|
||||||
|
.firstWhere((element) => element.$1 == item)
|
||||||
|
.$2
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
return market.contains(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filterLocale(Locale locale, String query) {
|
||||||
|
final language =
|
||||||
|
LanguageLocals.getDisplayLanguage(locale.languageCode).toString();
|
||||||
|
|
||||||
|
return language.toLowerCase().contains(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
@ -62,22 +78,30 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
|||||||
.firstWhere((element) => element.$1 == value)
|
.firstWhere((element) => element.$1 == value)
|
||||||
.$2,
|
.$2,
|
||||||
),
|
),
|
||||||
|
popup: SelectPopup.builder(
|
||||||
searchPlaceholder: Text(context.l10n.search),
|
searchPlaceholder: Text(context.l10n.search),
|
||||||
searchFilter: (item, query) {
|
builder: (context, searchQuery) {
|
||||||
final market = spotifyMarkets
|
final filteredMarkets = searchQuery == null ||
|
||||||
.firstWhere((element) => element.$1 == item)
|
searchQuery.isEmpty
|
||||||
.$2
|
? spotifyMarkets
|
||||||
.toLowerCase();
|
: spotifyMarkets
|
||||||
|
.where(
|
||||||
return market.contains(query.toLowerCase()) ? 1 : 0;
|
(element) =>
|
||||||
},
|
filterMarkets(element.$1, searchQuery),
|
||||||
children: [
|
)
|
||||||
for (final market in spotifyMarkets)
|
.toList();
|
||||||
SelectItemButton(
|
return SelectItemBuilder(
|
||||||
|
childCount: filteredMarkets.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
final market = filteredMarkets[index];
|
||||||
|
return SelectItemButton(
|
||||||
value: market.$1,
|
value: market.$1,
|
||||||
child: Text(market.$2),
|
child: Text(market.$2),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).call,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(36),
|
const Gap(36),
|
||||||
@ -106,33 +130,47 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
|||||||
value.languageCode)
|
value.languageCode)
|
||||||
.toString(),
|
.toString(),
|
||||||
),
|
),
|
||||||
|
popup: SelectPopup.builder(
|
||||||
searchPlaceholder: Text(context.l10n.search),
|
searchPlaceholder: Text(context.l10n.search),
|
||||||
searchFilter: (locale, query) {
|
builder: (context, searchQuery) {
|
||||||
final language = LanguageLocals.getDisplayLanguage(
|
final hasNotQueried =
|
||||||
locale.languageCode)
|
searchQuery == null || searchQuery.trim().isEmpty;
|
||||||
.toString();
|
final filteredLocale = hasNotQueried
|
||||||
|
? [
|
||||||
return language
|
const Locale("system", "system"),
|
||||||
.toLowerCase()
|
...L10n.all,
|
||||||
.contains(query.toLowerCase())
|
]
|
||||||
? 1
|
: L10n.all
|
||||||
: 0;
|
.where(
|
||||||
},
|
(element) => filterLocale(
|
||||||
children: [
|
element,
|
||||||
SelectItemButton(
|
searchQuery.trim(),
|
||||||
value: const Locale("system", "system"),
|
|
||||||
child: Text(context.l10n.system_default),
|
|
||||||
),
|
),
|
||||||
for (final locale in L10n.all)
|
)
|
||||||
SelectItemButton(
|
.toList();
|
||||||
|
|
||||||
|
return SelectItemBuilder(
|
||||||
|
childCount: filteredLocale.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
final locale = filteredLocale[index];
|
||||||
|
if (locale == const Locale("system", "system")) {
|
||||||
|
return SelectItemButton(
|
||||||
|
value: locale,
|
||||||
|
child: Text(context.l10n.system_default),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SelectItemButton(
|
||||||
value: locale,
|
value: locale,
|
||||||
child: Text(
|
child: Text(
|
||||||
LanguageLocals.getDisplayLanguage(
|
LanguageLocals.getDisplayLanguage(
|
||||||
locale.languageCode)
|
locale.languageCode,
|
||||||
.toString(),
|
).toString(),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).call,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -96,7 +96,9 @@ class LastFMLoginPage extends HookConsumerWidget {
|
|||||||
FormField(
|
FormField(
|
||||||
label: Text(context.l10n.username),
|
label: Text(context.l10n.username),
|
||||||
key: usernameKey,
|
key: usernameKey,
|
||||||
validator: const NotEmptyValidator(),
|
validator: const NotEmptyValidator(
|
||||||
|
message: "Username is required",
|
||||||
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
autofillHints: const [
|
autofillHints: const [
|
||||||
AutofillHints.username,
|
AutofillHints.username,
|
||||||
@ -107,7 +109,9 @@ class LastFMLoginPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
FormField(
|
FormField(
|
||||||
key: passwordKey,
|
key: passwordKey,
|
||||||
validator: const NotEmptyValidator(),
|
validator: const NotEmptyValidator(
|
||||||
|
message: "Password is required",
|
||||||
|
),
|
||||||
label: Text(context.l10n.password),
|
label: Text(context.l10n.password),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
autofillHints: const [
|
autofillHints: const [
|
||||||
|
|||||||
@ -69,6 +69,13 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const TitleBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
surfaceBlur: 0,
|
||||||
|
height: 32,
|
||||||
),
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -193,16 +193,11 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final genreSelector = MultiSelect<String>(
|
final genreSelector = MultiSelect<String>(
|
||||||
value: genres.value,
|
value: genres.value,
|
||||||
searchFilter: (item, query) {
|
|
||||||
return item.toLowerCase().contains(query.toLowerCase()) ? 1 : 0;
|
|
||||||
},
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
genres.value = value;
|
genres.value = value?.toList() ?? [];
|
||||||
},
|
},
|
||||||
itemBuilder: (context, item) => Text(item),
|
itemBuilder: (context, item) => Text(item),
|
||||||
searchPlaceholder: Text(context.l10n.select_genres),
|
|
||||||
orderSelectedFirst: false,
|
|
||||||
popoverAlignment: Alignment.bottomCenter,
|
popoverAlignment: Alignment.bottomCenter,
|
||||||
popupConstraints: BoxConstraints(
|
popupConstraints: BoxConstraints(
|
||||||
maxHeight: MediaQuery.sizeOf(context).height * .8,
|
maxHeight: MediaQuery.sizeOf(context).height * .8,
|
||||||
@ -213,13 +208,33 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
context.l10n.genre,
|
context.l10n.genre,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
children: [
|
popup: SelectPopup.builder(
|
||||||
for (final option in genresCollection.asData?.value ?? <String>[])
|
searchPlaceholder: Text(context.l10n.select_genres),
|
||||||
SelectItemButton(
|
builder: (context, searchQuery) {
|
||||||
|
final filteredGenres = searchQuery?.isNotEmpty != true
|
||||||
|
? genresCollection.asData?.value ?? []
|
||||||
|
: genresCollection.asData?.value
|
||||||
|
.where(
|
||||||
|
(item) => item
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(searchQuery!.toLowerCase()),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
return SelectItemBuilder(
|
||||||
|
childCount: filteredGenres.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
final option = filteredGenres[index];
|
||||||
|
|
||||||
|
return SelectItemButton(
|
||||||
value: option,
|
value: option,
|
||||||
child: Text(option),
|
child: Text(option),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).call,
|
||||||
);
|
);
|
||||||
|
|
||||||
final countrySelector = ValueListenableBuilder(
|
final countrySelector = ValueListenableBuilder(
|
||||||
@ -231,25 +246,35 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
market.value = value!;
|
market.value = value!;
|
||||||
},
|
},
|
||||||
searchFilter: (item, query) {
|
|
||||||
return item.name.toLowerCase().contains(query.toLowerCase())
|
|
||||||
? 1
|
|
||||||
: 0;
|
|
||||||
},
|
|
||||||
searchPlaceholder: Text(context.l10n.search),
|
|
||||||
popupConstraints: BoxConstraints(
|
popupConstraints: BoxConstraints(
|
||||||
maxHeight: MediaQuery.sizeOf(context).height * .8,
|
maxHeight: MediaQuery.sizeOf(context).height * .8,
|
||||||
),
|
),
|
||||||
popoverAlignment: Alignment.bottomCenter,
|
popoverAlignment: Alignment.bottomCenter,
|
||||||
itemBuilder: (context, value) => Text(value.name),
|
itemBuilder: (context, value) => Text(value.name),
|
||||||
children: spotifyMarkets
|
popup: SelectPopup.builder(
|
||||||
.map(
|
searchPlaceholder: Text(context.l10n.search),
|
||||||
(country) => SelectItemButton(
|
builder: (context, searchQuery) {
|
||||||
value: country.$1,
|
final filteredMarkets = searchQuery == null || searchQuery.isEmpty
|
||||||
child: Text(country.$2),
|
? spotifyMarkets
|
||||||
),
|
: spotifyMarkets
|
||||||
|
.where(
|
||||||
|
(item) => item.$1.name
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(searchQuery.toLowerCase()),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList();
|
||||||
|
|
||||||
|
return SelectItemBuilder(
|
||||||
|
childCount: filteredMarkets.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
return SelectItemButton(
|
||||||
|
value: filteredMarkets[index].$1,
|
||||||
|
child: Text(filteredMarkets[index].$2),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).call,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:flutter_undraw/flutter_undraw.dart';
|
import 'package:flutter_undraw/flutter_undraw.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@ -55,10 +56,10 @@ class UserAlbumsPage extends HookConsumerWidget {
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
child: RefreshTrigger(
|
child: material.RefreshIndicator.adaptive(
|
||||||
// onRefresh: () async {
|
onRefresh: () async {
|
||||||
// ref.invalidate(favoriteAlbumsProvider);
|
ref.invalidate(favoriteAlbumsProvider);
|
||||||
// },
|
},
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_undraw/flutter_undraw.dart';
|
import 'package:flutter_undraw/flutter_undraw.dart';
|
||||||
@ -60,10 +61,10 @@ class UserArtistsPage extends HookConsumerWidget {
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
child: RefreshTrigger(
|
child: material.RefreshIndicator.adaptive(
|
||||||
// onRefresh: () async {
|
onRefresh: () async {
|
||||||
// ref.invalidate(followedArtistsProvider);
|
ref.invalidate(followedArtistsProvider);
|
||||||
// },
|
},
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@ -15,6 +16,7 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/button/back_button.dart';
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
|
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
|
||||||
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
|
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
|
||||||
import 'package:spotube/components/expandable_search/expandable_search.dart';
|
import 'package:spotube/components/expandable_search/expandable_search.dart';
|
||||||
@ -78,7 +80,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
final isPlaylistPlaying = playlist.containsTracks(
|
final isPlaylistPlaying = playlist.containsTracks(
|
||||||
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
|
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
|
||||||
|
|
||||||
final searchController = useTextEditingController();
|
final searchController = useShadcnTextEditingController();
|
||||||
useValueListenable(searchController);
|
useValueListenable(searchController);
|
||||||
final searchFocus = useFocusNode();
|
final searchFocus = useFocusNode();
|
||||||
final isFiltering = useState(false);
|
final isFiltering = useState(false);
|
||||||
@ -342,9 +344,9 @@ class LocalLibraryPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: RefreshTrigger(
|
child: material.RefreshIndicator.adaptive(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
// ref.invalidate(localTracksProvider);
|
ref.invalidate(localTracksProvider);
|
||||||
},
|
},
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/material.dart' show kToolbarHeight;
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -17,7 +17,6 @@ import 'package:spotube/modules/playlist/playlist_card.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -79,10 +78,10 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
|||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
return RefreshTrigger(
|
return material.RefreshIndicator.adaptive(
|
||||||
// onRefresh: () async {
|
onRefresh: () async {
|
||||||
// ref.invalidate(favoritePlaylistsProvider);
|
ref.invalidate(favoritePlaylistsProvider);
|
||||||
// },
|
},
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
@ -103,12 +102,14 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
|||||||
leading: const Icon(SpotubeIcons.filter),
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottom: PreferredSize(
|
),
|
||||||
preferredSize:
|
const SliverGap(10),
|
||||||
Size.fromHeight(kIsDesktop ? 35 : kToolbarHeight),
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
sliver: PlaybuttonView(
|
||||||
|
leading: Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Gap(10),
|
|
||||||
const PlaylistCreateDialogButton(),
|
const PlaylistCreateDialogButton(),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Button.primary(
|
Button.primary(
|
||||||
@ -122,11 +123,6 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SliverGap(10),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
sliver: PlaybuttonView(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
hasMore: playlistsQuery.asData?.value.hasMore == true,
|
hasMore: playlistsQuery.asData?.value.hasMore == true,
|
||||||
isLoading: playlistsQuery.isLoading,
|
isLoading: playlistsQuery.isLoading,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
@ -20,8 +19,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
class LyricsPage extends HookConsumerWidget {
|
class LyricsPage extends HookConsumerWidget {
|
||||||
static const name = "lyrics";
|
static const name = "lyrics";
|
||||||
|
|
||||||
final bool isModal;
|
const LyricsPage({super.key});
|
||||||
const LyricsPage({super.key, this.isModal = false});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
@ -38,20 +36,7 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget tabbar = Padding(
|
Widget tabbar = Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: isModal
|
child: Tabs(
|
||||||
? TabList(
|
|
||||||
index: selectedIndex.value,
|
|
||||||
onChanged: (index) => selectedIndex.value = index,
|
|
||||||
children: [
|
|
||||||
TabItem(
|
|
||||||
child: Text(context.l10n.synced),
|
|
||||||
),
|
|
||||||
TabItem(
|
|
||||||
child: Text(context.l10n.plain),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Tabs(
|
|
||||||
index: selectedIndex.value,
|
index: selectedIndex.value,
|
||||||
onChanged: (index) => selectedIndex.value = index,
|
onChanged: (index) => selectedIndex.value = index,
|
||||||
children: [
|
children: [
|
||||||
@ -85,52 +70,6 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isModal) {
|
|
||||||
return SafeArea(
|
|
||||||
bottom: false,
|
|
||||||
child: SurfaceCard(
|
|
||||||
surfaceBlur: context.theme.surfaceBlur,
|
|
||||||
surfaceOpacity: context.theme.surfaceOpacity,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
borderRadius: BorderRadius.zero,
|
|
||||||
borderWidth: 0,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Container(
|
|
||||||
height: 7,
|
|
||||||
width: 150,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: palette.titleTextColor,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: tabbar,
|
|
||||||
),
|
|
||||||
IconButton.ghost(
|
|
||||||
icon: const Icon(SpotubeIcons.minimize),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: IndexedStack(
|
|
||||||
index: selectedIndex.value,
|
|
||||||
children: [
|
|
||||||
SyncedLyrics(palette: palette, isModal: isModal),
|
|
||||||
PlainLyrics(palette: palette, isModal: isModal),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
@ -142,6 +81,7 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
title: tabbar,
|
title: tabbar,
|
||||||
height: 58 * context.theme.scaling,
|
height: 58 * context.theme.scaling,
|
||||||
surfaceBlur: 0,
|
surfaceBlur: 0,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
)
|
)
|
||||||
: tabbar
|
: tabbar
|
||||||
],
|
],
|
||||||
@ -166,8 +106,8 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
child: IndexedStack(
|
child: IndexedStack(
|
||||||
index: selectedIndex.value,
|
index: selectedIndex.value,
|
||||||
children: [
|
children: [
|
||||||
SyncedLyrics(palette: palette, isModal: isModal),
|
SyncedLyrics(palette: palette, isModal: false),
|
||||||
PlainLyrics(palette: palette, isModal: isModal),
|
PlainLyrics(palette: palette, isModal: false),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
64
lib/pages/player/lyrics.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:auto_route/annotations.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/button/back_button.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PlayerLyricsPage extends HookConsumerWidget {
|
||||||
|
const PlayerLyricsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
|
String albumArt = useMemoized(
|
||||||
|
() => (playlist.activeTrack?.album?.images).asUrlString(
|
||||||
|
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
|
[playlist.activeTrack?.album?.images],
|
||||||
|
);
|
||||||
|
final selectedIndex = useState(0);
|
||||||
|
final palette = usePaletteColor(albumArt, ref);
|
||||||
|
|
||||||
|
final tabbar = Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: TabList(
|
||||||
|
index: selectedIndex.value,
|
||||||
|
onChanged: (index) => selectedIndex.value = index,
|
||||||
|
children: [
|
||||||
|
TabItem(
|
||||||
|
child: Text(context.l10n.synced),
|
||||||
|
),
|
||||||
|
TabItem(
|
||||||
|
child: Text(context.l10n.plain),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
headers: [
|
||||||
|
AppBar(
|
||||||
|
leading: [tabbar],
|
||||||
|
trailing: const [
|
||||||
|
BackButton(icon: SpotubeIcons.angleDown),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: IndexedStack(
|
||||||
|
index: selectedIndex.value,
|
||||||
|
children: [
|
||||||
|
SyncedLyrics(palette: palette, isModal: false),
|
||||||
|
PlainLyrics(palette: palette, isModal: false),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/pages/player/queue.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:auto_route/annotations.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/modules/player/player_queue.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PlayerQueuePage extends HookConsumerWidget {
|
||||||
|
const PlayerQueuePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final playlist = ref.watch(
|
||||||
|
audioPlayerProvider,
|
||||||
|
);
|
||||||
|
final playlistNotifier = ref.read(audioPlayerProvider.notifier);
|
||||||
|
return Scaffold(
|
||||||
|
child: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: PlayerQueue.fromAudioPlayerNotifier(
|
||||||
|
floating: false,
|
||||||
|
playlist: playlist,
|
||||||
|
notifier: playlistNotifier,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/pages/player/sources.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PlayerTrackSourcesPage extends StatelessWidget {
|
||||||
|
const PlayerTrackSourcesPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Scaffold(
|
||||||
|
child: SiblingTracksSheet(floating: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -22,7 +23,11 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
final likedTracks = ref.watch(likedTracksProvider);
|
final likedTracks = ref.watch(likedTracksProvider);
|
||||||
final tracks = likedTracks.asData?.value ?? <Track>[];
|
final tracks = likedTracks.asData?.value ?? <Track>[];
|
||||||
|
|
||||||
return TrackPresentation(
|
return material.RefreshIndicator.adaptive(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(likedTracksProvider);
|
||||||
|
},
|
||||||
|
child: TrackPresentation(
|
||||||
options: TrackPresentationOptions(
|
options: TrackPresentationOptions(
|
||||||
collection: playlist,
|
collection: playlist,
|
||||||
image: "assets/liked-tracks.jpg",
|
image: "assets/liked-tracks.jpg",
|
||||||
@ -46,6 +51,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
onHeart: null,
|
onHeart: null,
|
||||||
owner: playlist.owner?.displayName,
|
owner: playlist.owner?.displayName,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart' as material;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -49,7 +50,13 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
||||||
|
|
||||||
return TrackPresentation(
|
return material.RefreshIndicator.adaptive(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(playlistTracksProvider(playlist.id!));
|
||||||
|
ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
|
||||||
|
ref.invalidate(favoritePlaylistsProvider);
|
||||||
|
},
|
||||||
|
child: TrackPresentation(
|
||||||
options: TrackPresentationOptions(
|
options: TrackPresentationOptions(
|
||||||
collection: playlist,
|
collection: playlist,
|
||||||
image: playlist.images.asUrlString(
|
image: playlist.images.asUrlString(
|
||||||
@ -95,6 +102,7 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
return isUserPlaylist;
|
return isUserPlaylist;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,7 +43,9 @@ class RootAppPage extends HookConsumerWidget {
|
|||||||
final scaffold = MediaQuery.removeViewInsets(
|
final scaffold = MediaQuery.removeViewInsets(
|
||||||
context: context,
|
context: context,
|
||||||
removeBottom: true,
|
removeBottom: true,
|
||||||
child: const Scaffold(
|
child: const SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Scaffold(
|
||||||
footers: [
|
footers: [
|
||||||
BottomPlayer(),
|
BottomPlayer(),
|
||||||
SpotubeNavigationBar(),
|
SpotubeNavigationBar(),
|
||||||
@ -51,6 +53,7 @@ class RootAppPage extends HookConsumerWidget {
|
|||||||
floatingFooter: true,
|
floatingFooter: true,
|
||||||
child: Sidebar(child: AutoRouter()),
|
child: Sidebar(child: AutoRouter()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return scaffold;
|
return scaffold;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
|||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/pages/search/sections/albums.dart';
|
import 'package:spotube/pages/search/sections/albums.dart';
|
||||||
import 'package:spotube/pages/search/sections/artists.dart';
|
import 'package:spotube/pages/search/sections/artists.dart';
|
||||||
import 'package:spotube/pages/search/sections/playlists.dart';
|
import 'package:spotube/pages/search/sections/playlists.dart';
|
||||||
@ -35,7 +36,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
final mediaQuery = MediaQuery.sizeOf(context);
|
final mediaQuery = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final controller = useSearchController();
|
final controller = useShadcnTextEditingController();
|
||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
|
|
||||||
final auth = ref.watch(authenticationProvider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
@ -120,10 +121,12 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AutoComplete(
|
child: AutoComplete(
|
||||||
|
suggestions: suggestions,
|
||||||
|
child: TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
suggestions: suggestions,
|
leading:
|
||||||
leading: const Icon(SpotubeIcons.search),
|
const Icon(SpotubeIcons.search),
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
placeholder: Text(context.l10n.search),
|
placeholder: Text(context.l10n.search),
|
||||||
trailing: AnimatedCrossFade(
|
trailing: AnimatedCrossFade(
|
||||||
@ -135,7 +138,8 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: IconButton.ghost(
|
firstChild: IconButton.ghost(
|
||||||
size: ButtonSize.small,
|
size: ButtonSize.small,
|
||||||
icon: const Icon(SpotubeIcons.close),
|
icon:
|
||||||
|
const Icon(SpotubeIcons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.clear();
|
controller.clear();
|
||||||
},
|
},
|
||||||
@ -143,19 +147,9 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
secondChild: const SizedBox.square(
|
secondChild: const SizedBox.square(
|
||||||
dimension: 28),
|
dimension: 28),
|
||||||
),
|
),
|
||||||
onAcceptSuggestion: (index) {
|
|
||||||
controller.text = KVStoreService
|
|
||||||
.recentSearches[index];
|
|
||||||
ref
|
|
||||||
.read(searchTermStateProvider
|
|
||||||
.notifier)
|
|
||||||
.state =
|
|
||||||
KVStoreService
|
|
||||||
.recentSearches[index];
|
|
||||||
},
|
|
||||||
onChanged: (value) {},
|
|
||||||
onSubmitted: onSubmitted,
|
onSubmitted: onSubmitted,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -98,6 +98,18 @@ class AboutSpotubePage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const TableRow(
|
||||||
|
cells: [
|
||||||
|
TableCell(child: Text("Website")),
|
||||||
|
colon,
|
||||||
|
TableCell(
|
||||||
|
child: Hyperlink(
|
||||||
|
"spotube.krtirtho.dev",
|
||||||
|
"https://spotube.krtirtho.dev",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
TableRow(
|
TableRow(
|
||||||
cells: [
|
cells: [
|
||||||
TableCell(child: Text(context.l10n.repository)),
|
TableCell(child: Text(context.l10n.repository)),
|
||||||
|
|||||||
@ -4,6 +4,9 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart' show ListTile;
|
import 'package:flutter/material.dart' show ListTile;
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -11,6 +14,8 @@ import 'package:piped_client/piped_client.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
|
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
||||||
@ -97,10 +102,106 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
value: preferences.pipedInstance,
|
value: preferences.pipedInstance,
|
||||||
showValueWhenUnfolded: false,
|
showValueWhenUnfolded: false,
|
||||||
options: data
|
trailing: [
|
||||||
.sortedBy((e) => e.name)
|
Tooltip(
|
||||||
.map(
|
tooltip: TooltipContainer(
|
||||||
(e) => SelectItemButton(
|
child: Text(context.l10n.add_custom_url),
|
||||||
|
),
|
||||||
|
child: IconButton.outline(
|
||||||
|
icon: const Icon(SpotubeIcons.edit),
|
||||||
|
size: ButtonSize.small,
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||||||
|
builder: (context) => HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
|
final controller =
|
||||||
|
useShadcnTextEditingController(
|
||||||
|
text: preferences.pipedInstance,
|
||||||
|
);
|
||||||
|
final formKey = useMemoized(
|
||||||
|
() => GlobalKey<FormBuilderState>(), []);
|
||||||
|
|
||||||
|
return Alert(
|
||||||
|
title:
|
||||||
|
Text(context.l10n.piped_instance).h4(),
|
||||||
|
content: FormBuilder(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Gap(10),
|
||||||
|
TextFormBuilderField(
|
||||||
|
name: "url",
|
||||||
|
controller: controller,
|
||||||
|
placeholder: Text(
|
||||||
|
context.l10n.piped_instance),
|
||||||
|
validator:
|
||||||
|
FormBuilderValidators.url(),
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Button.secondary(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
Text(context.l10n.cancel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Expanded(
|
||||||
|
child: Button.primary(
|
||||||
|
onPressed: () {
|
||||||
|
if (!formKey.currentState!
|
||||||
|
.saveAndValidate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
preferencesNotifier
|
||||||
|
.setPipedInstance(
|
||||||
|
controller.text,
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
Text(context.l10n.save),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
if (data
|
||||||
|
.none((e) => e.apiUrl == preferences.pipedInstance))
|
||||||
|
SelectItemButton(
|
||||||
|
value: preferences.pipedInstance,
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: theme.typography.xSmall.copyWith(
|
||||||
|
color: theme.colorScheme.foreground,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: context.l10n.custom),
|
||||||
|
const TextSpan(text: "\n"),
|
||||||
|
TextSpan(text: preferences.pipedInstance),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final e in data.sortedBy((e) => e.name))
|
||||||
|
SelectItemButton(
|
||||||
value: e.apiUrl,
|
value: e.apiUrl,
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
@ -121,8 +222,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
preferencesNotifier.setPipedInstance(value);
|
preferencesNotifier.setPipedInstance(value);
|
||||||
@ -157,12 +257,108 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
"${context.l10n.invidious_description}\n"
|
"${context.l10n.invidious_description}\n"
|
||||||
"${context.l10n.invidious_warning}",
|
"${context.l10n.invidious_warning}",
|
||||||
),
|
),
|
||||||
|
trailing: [
|
||||||
|
Tooltip(
|
||||||
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(context.l10n.add_custom_url),
|
||||||
|
),
|
||||||
|
child: IconButton.outline(
|
||||||
|
icon: const Icon(SpotubeIcons.edit),
|
||||||
|
size: ButtonSize.small,
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||||||
|
builder: (context) => HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
|
final controller =
|
||||||
|
useShadcnTextEditingController(
|
||||||
|
text: preferences.invidiousInstance,
|
||||||
|
);
|
||||||
|
final formKey = useMemoized(
|
||||||
|
() => GlobalKey<FormBuilderState>(), []);
|
||||||
|
|
||||||
|
return Alert(
|
||||||
|
title: Text(context.l10n.invidious_instance)
|
||||||
|
.h4(),
|
||||||
|
content: FormBuilder(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Gap(10),
|
||||||
|
TextFormBuilderField(
|
||||||
|
name: "url",
|
||||||
|
controller: controller,
|
||||||
|
placeholder: Text(context
|
||||||
|
.l10n.invidious_instance),
|
||||||
|
validator:
|
||||||
|
FormBuilderValidators.url(),
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Button.secondary(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
Text(context.l10n.cancel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(10),
|
||||||
|
Expanded(
|
||||||
|
child: Button.primary(
|
||||||
|
onPressed: () {
|
||||||
|
if (!formKey.currentState!
|
||||||
|
.saveAndValidate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
preferencesNotifier
|
||||||
|
.setInvidiousInstance(
|
||||||
|
controller.text,
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
Text(context.l10n.save),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
value: preferences.invidiousInstance,
|
value: preferences.invidiousInstance,
|
||||||
showValueWhenUnfolded: false,
|
showValueWhenUnfolded: false,
|
||||||
options: data
|
options: [
|
||||||
.sortedBy((e) => e.name)
|
if (data.none((e) =>
|
||||||
.map(
|
e.details.uri == preferences.invidiousInstance))
|
||||||
(e) => SelectItemButton(
|
SelectItemButton(
|
||||||
|
value: preferences.invidiousInstance,
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: theme.typography.xSmall.copyWith(
|
||||||
|
color: theme.colorScheme.foreground,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: context.l10n.custom),
|
||||||
|
const TextSpan(text: "\n"),
|
||||||
|
TextSpan(text: preferences.invidiousInstance),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final e in data.sortedBy((e) => e.name))
|
||||||
|
SelectItemButton(
|
||||||
value: e.details.uri,
|
value: e.details.uri,
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
@ -183,8 +379,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
preferencesNotifier.setInvidiousInstance(value);
|
preferencesNotifier.setInvidiousInstance(value);
|
||||||
|
|||||||
@ -91,13 +91,18 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
|||||||
Text(translations[value]!),
|
Text(translations[value]!),
|
||||||
constraints: const BoxConstraints(maxWidth: 150),
|
constraints: const BoxConstraints(maxWidth: 150),
|
||||||
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
|
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
|
||||||
children: [
|
popup: SelectPopup(
|
||||||
for (final entry in translations.entries)
|
items: SelectItemBuilder(
|
||||||
SelectItemButton(
|
childCount: translations.length,
|
||||||
|
builder: (context, index) {
|
||||||
|
final entry = translations.entries.elementAt(index);
|
||||||
|
return SelectItemButton(
|
||||||
value: entry.key,
|
value: entry.key,
|
||||||
child: Text(entry.value),
|
child: Text(entry.value),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
).call,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -111,7 +111,9 @@ class TrackPage extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
runAlignment: WrapAlignment.center,
|
runAlignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: track.album!.images.asUrlString(
|
path: track.album!.images.asUrlString(
|
||||||
@ -121,6 +123,7 @@ class TrackPage extends HookConsumerWidget {
|
|||||||
width: 200,
|
width: 200,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 16.0),
|
const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
|||||||
@ -24,6 +24,9 @@ const supportedAudioTypes = [
|
|||||||
"audio/opus",
|
"audio/opus",
|
||||||
"audio/wav",
|
"audio/wav",
|
||||||
"audio/aac",
|
"audio/aac",
|
||||||
|
"audio/flac",
|
||||||
|
"audio/x-flac",
|
||||||
|
"audio/x-wav",
|
||||||
];
|
];
|
||||||
|
|
||||||
const imgMimeToExt = {
|
const imgMimeToExt = {
|
||||||
@ -68,13 +71,16 @@ final localTracksProvider =
|
|||||||
await Directory(location).list(recursive: true).toList();
|
await Directory(location).list(recursive: true).toList();
|
||||||
|
|
||||||
entities.addAll(
|
entities.addAll(
|
||||||
dirEntities
|
dirEntities.where(
|
||||||
.where(
|
(e) {
|
||||||
(e) =>
|
final mime = lookupMimeType(e.path) ??
|
||||||
e is File &&
|
(extension(e.path) == ".opus" ? "audio/opus" : null);
|
||||||
supportedAudioTypes.contains(lookupMimeType(e.path)),
|
|
||||||
)
|
print("${basename(e.path)}: $mime");
|
||||||
.cast<File>(),
|
|
||||||
|
return e is File && supportedAudioTypes.contains(mime);
|
||||||
|
},
|
||||||
|
).cast<File>(),
|
||||||
);
|
);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
|
|||||||
@ -25,12 +25,12 @@ import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
const _deviceClients = {
|
final _deviceClients = Set.unmodifiable({
|
||||||
YoutubeApiClient.android,
|
|
||||||
YoutubeApiClient.ios,
|
YoutubeApiClient.ios,
|
||||||
|
YoutubeApiClient.android,
|
||||||
YoutubeApiClient.mweb,
|
YoutubeApiClient.mweb,
|
||||||
YoutubeApiClient.safari,
|
YoutubeApiClient.safari,
|
||||||
};
|
});
|
||||||
|
|
||||||
String? get _randomUserAgent => _deviceClients
|
String? get _randomUserAgent => _deviceClients
|
||||||
.elementAt(
|
.elementAt(
|
||||||
|
|||||||