mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-10 17:07:30 +00:00
Merge branch 'KRTirtho:master' into master
This commit is contained in:
commit
b8de7aace3
17
.github/workflows/spotube-release-binary.yml
vendored
17
.github/workflows/spotube-release-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to release (x.x.x)
|
||||
default: 3.3.0
|
||||
default: 3.4.0
|
||||
required: true
|
||||
channel:
|
||||
type: choice
|
||||
@ -26,18 +26,13 @@ on:
|
||||
default: true
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: '3.16.0'
|
||||
FLUTTER_VERSION: '3.16.3'
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: KRTirtho/flutter_distributor
|
||||
path: flutter_distributor
|
||||
ref: fix-windows-build
|
||||
- uses: subosito/flutter-action@v2.10.0
|
||||
with:
|
||||
cache: true
|
||||
@ -79,10 +74,9 @@ jobs:
|
||||
|
||||
- name: Build Windows Executable
|
||||
run: |
|
||||
dart pub global activate melos
|
||||
cd flutter_distributor && melos bs && cd ..
|
||||
dart pub global activate flutter_distributor
|
||||
make innoinstall
|
||||
dart run ./flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=windows --targets=exe --skip-clean
|
||||
flutter_distributor package --platform=windows --targets=exe --skip-clean
|
||||
mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe
|
||||
|
||||
- name: Create Chocolatey Package and set hash
|
||||
@ -169,7 +163,6 @@ jobs:
|
||||
dart pub global activate flutter_distributor
|
||||
alias dpkg-deb="dpkg-deb --Zxz"
|
||||
flutter_distributor package --platform=linux --targets=deb
|
||||
flutter_distributor package --platform=linux --targets=appimage
|
||||
flutter_distributor package --platform=linux --targets=rpm
|
||||
|
||||
- name: Create tar.xz (stable)
|
||||
@ -185,7 +178,6 @@ jobs:
|
||||
mv build/spotube-linux-*-x86_64.tar.xz dist/
|
||||
mv dist/**/spotube-*-linux.deb dist/Spotube-linux-x86_64.deb
|
||||
mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm
|
||||
mv dist/**/spotube-*-linux.AppImage dist/Spotube-linux-x86_64.AppImage
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
@ -193,7 +185,6 @@ jobs:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
path: |
|
||||
dist/Spotube-linux-x86_64.AppImage
|
||||
dist/Spotube-linux-x86_64.deb
|
||||
dist/Spotube-linux-x86_64.rpm
|
||||
dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@ -2,6 +2,32 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [3.4.0](https://github.com/KRTirtho/spotube/compare/v3.3.0...v3.4.0) (2023-12-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Go to Album option in track option [#917](https://github.com/KRTirtho/spotube/issues/917) ([b0beeca](https://github.com/KRTirtho/spotube/commit/b0beeca0cbaf810fae27832cff98cfda95715050))
|
||||
* **translations:** add Italian language translations ([#818](https://github.com/KRTirtho/spotube/issues/818)) ([e4eb0e2](https://github.com/KRTirtho/spotube/commit/e4eb0e2596ade2bb5195e183f03af42742fc8486)), closes [#676](https://github.com/KRTirtho/spotube/issues/676) [#676](https://github.com/KRTirtho/spotube/issues/676)
|
||||
* compact genre view in home page ([82ed5e9](https://github.com/KRTirtho/spotube/commit/82ed5e90576b57ef32e61a65015e04862ab15461))
|
||||
* Deep link support ([#950](https://github.com/KRTirtho/spotube/issues/950)) ([4050f55](https://github.com/KRTirtho/spotube/commit/4050f556400aaec5515231578512cf1a6b990110))
|
||||
* improve loading animations ([b92583d](https://github.com/KRTirtho/spotube/commit/b92583d0df7b8dee0d121cd2bb666b14c77d8c86))
|
||||
* toggle for discord rpc ([24a2294](https://github.com/KRTirtho/spotube/commit/24a2294512bb0c4aff77bc8dcad9b4de3e8b45c6))
|
||||
* **translations:** add Dutch Language ([#969](https://github.com/KRTirtho/spotube/issues/969)) ([3ad7ba6](https://github.com/KRTirtho/spotube/commit/3ad7ba66b56e93e69d2181d47029b7549ed225fc))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add safe area in home ([9ee6067](https://github.com/KRTirtho/spotube/commit/9ee60677f6d50df7468e12dc6653ecedefa2494f))
|
||||
* amoled mode and color scheme can't be changed ([840e014](https://github.com/KRTirtho/spotube/commit/840e014f2b18f193d040baef0e0cd595088a4a84))
|
||||
* doesn't minimize to tray when system title bar close button is used [#866](https://github.com/KRTirtho/spotube/issues/866) ([bb8f250](https://github.com/KRTirtho/spotube/commit/bb8f250f5f351c1a353791b77b25b9de7586191f))
|
||||
* genre border issues ([2fb16e6](https://github.com/KRTirtho/spotube/commit/2fb16e64e9cdfca54d633cdf287b0544ecdda3b6))
|
||||
* Incorrect "Artist" label/heading on Search Results Page [#920](https://github.com/KRTirtho/spotube/issues/920) ([f86d544](https://github.com/KRTirtho/spotube/commit/f86d5449168068e338f769d7f504d2146b86dc79))
|
||||
* metadata not getting added for YouTube tracks [#916](https://github.com/KRTirtho/spotube/issues/916) and Wrong duration of downloaded tracks [#912](https://github.com/KRTirtho/spotube/issues/912) ([a7b9398](https://github.com/KRTirtho/spotube/commit/a7b9398708ede865dc2c25fb791c8e98eeff7a38))
|
||||
* Playlist refresh not working [#915](https://github.com/KRTirtho/spotube/issues/915) ([5f1df5a](https://github.com/KRTirtho/spotube/commit/5f1df5a87d8fb7980b52cf57b7b6bedea57a1269))
|
||||
* track view header title overflow and player view drag glitch ([b04d884](https://github.com/KRTirtho/spotube/commit/b04d8849e7169824ec5b980236b5d61b2629f56e))
|
||||
* wrong artist name sent while scrobbling [#958](https://github.com/KRTirtho/spotube/issues/958) ([dcbe729](https://github.com/KRTirtho/spotube/commit/dcbe7294b742d43fbff4e89ab4c4825e94421dd9))
|
||||
|
||||
## [3.3.0](https://github.com/KRTirtho/spotube/compare/v3.2.0...v3.3.0) (2023-11-27)
|
||||
|
||||
|
||||
|
||||
19
README.md
19
README.md
@ -199,6 +199,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos.
|
||||
1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan
|
||||
1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device
|
||||
1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms.
|
||||
|
||||
### Dependencies
|
||||
1. [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.
|
||||
@ -208,7 +209,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds.
|
||||
1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button.
|
||||
1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets.
|
||||
1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
|
||||
1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer.
|
||||
1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections.
|
||||
1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons
|
||||
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.
|
||||
@ -221,9 +222,9 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
|
||||
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
||||
1. [fl_query](https://fl-query.krtirtho.dev) - Asynchronous data caching, refetching & invalidation library for Flutter
|
||||
1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
|
||||
1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
|
||||
1. [fl_query_devtools](https://fl-query.krtirtho.dev) - Devtools support for Fl-Query
|
||||
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
||||
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
||||
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
||||
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
||||
1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability.
|
||||
@ -234,7 +235,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
||||
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
|
||||
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
||||
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
|
||||
1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs!
|
||||
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
||||
1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
|
||||
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
|
||||
@ -251,10 +252,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms.
|
||||
1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files
|
||||
1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.
|
||||
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
|
||||
1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android.
|
||||
1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image.
|
||||
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
|
||||
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
||||
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
||||
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
||||
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
||||
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
|
||||
@ -273,7 +274,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback.
|
||||
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. [simple_icons](https://jlnrrg.github.io/) - 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. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification.
|
||||
1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
|
||||
1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com
|
||||
@ -282,6 +283,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
||||
1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework
|
||||
1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References.
|
||||
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
|
||||
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
||||
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. [win32_registry](https://win32.pub) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
||||
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. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
||||
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
||||
1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps.
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<activity
|
||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
@ -48,6 +48,30 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="open.spotify.com"
|
||||
/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with "spotify:// -->
|
||||
<data android:scheme="spotify" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- AudioService Config -->
|
||||
|
||||
@ -35,6 +35,11 @@ void main(List<String> args) {
|
||||
);
|
||||
}
|
||||
|
||||
print(
|
||||
"Prompt:\n"
|
||||
"Translate following to their appropriate locale for flutter arb translations files."
|
||||
" Put the respective new translations in a map of their corresponding locale.",
|
||||
);
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
const JsonEncoder.withIndent(' ').convert(
|
||||
|
||||
167
lib/collections/fake.dart
Normal file
167
lib/collections/fake.dart
Normal file
@ -0,0 +1,167 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
|
||||
abstract class FakeData {
|
||||
static final Image image = Image()
|
||||
..height = 1
|
||||
..width = 1
|
||||
..url = "url";
|
||||
|
||||
static final Followers followers = Followers()
|
||||
..href = "text"
|
||||
..total = 1;
|
||||
|
||||
static final Artist artist = Artist()
|
||||
..id = "1"
|
||||
..name = "Wow artist Good!"
|
||||
..images = [image]
|
||||
..popularity = 1
|
||||
..type = "type"
|
||||
..uri = "uri"
|
||||
..externalUrls = externalUrls
|
||||
..genres = ["genre"]
|
||||
..href = "text"
|
||||
..followers = followers;
|
||||
|
||||
static final externalIds = ExternalIds()
|
||||
..isrc = "text"
|
||||
..ean = "text"
|
||||
..upc = "text";
|
||||
|
||||
static final externalUrls = ExternalUrls()..spotify = "text";
|
||||
|
||||
static final Album album = Album()
|
||||
..id = "1"
|
||||
..genres = ["genre"]
|
||||
..label = "label"
|
||||
..popularity = 1
|
||||
..albumType = AlbumType.album
|
||||
..artists = [artist]
|
||||
..availableMarkets = [Market.BD]
|
||||
..externalUrls = externalUrls
|
||||
..href = "text"
|
||||
..images = [image]
|
||||
..name = "Another good album"
|
||||
..releaseDate = "2021-01-01"
|
||||
..releaseDatePrecision = DatePrecision.day
|
||||
..tracks = [track]
|
||||
..type = "type"
|
||||
..uri = "uri"
|
||||
..externalIds = externalIds
|
||||
..copyrights = [
|
||||
Copyright()
|
||||
..type = CopyrightType.C
|
||||
..text = "text",
|
||||
];
|
||||
|
||||
static final ArtistSimple artistSimple = ArtistSimple()
|
||||
..id = "1"
|
||||
..name = "What an artist"
|
||||
..type = "type"
|
||||
..uri = "uri"
|
||||
..externalUrls = externalUrls;
|
||||
|
||||
static final AlbumSimple albumSimple = AlbumSimple()
|
||||
..id = "1"
|
||||
..albumType = AlbumType.album
|
||||
..artists = [artistSimple]
|
||||
..availableMarkets = [Market.BD]
|
||||
..externalUrls = externalUrls
|
||||
..href = "text"
|
||||
..images = [image]
|
||||
..name = "A good album"
|
||||
..releaseDate = "2021-01-01"
|
||||
..releaseDatePrecision = DatePrecision.day
|
||||
..type = "type"
|
||||
..uri = "uri";
|
||||
|
||||
static final Track track = Track()
|
||||
..id = "1"
|
||||
..artists = [artist, artist, artist]
|
||||
..album = albumSimple
|
||||
..availableMarkets = [Market.BD]
|
||||
..discNumber = 1
|
||||
..durationMs = 50000
|
||||
..explicit = false
|
||||
..externalUrls = externalUrls
|
||||
..href = "text"
|
||||
..name = "A Track Name"
|
||||
..popularity = 1
|
||||
..previewUrl = "url"
|
||||
..trackNumber = 1
|
||||
..type = "type"
|
||||
..uri = "uri"
|
||||
..isPlayable = true
|
||||
..explicit = false
|
||||
..linkedFrom = trackLink;
|
||||
|
||||
static final TrackLink trackLink = TrackLink()
|
||||
..id = "1"
|
||||
..type = "type"
|
||||
..uri = "uri"
|
||||
..externalUrls = {"spotify": "text"}
|
||||
..href = "text";
|
||||
|
||||
static final Paging<Track> paging = Paging()
|
||||
..href = "text"
|
||||
..itemsNative = [track.toJson()]
|
||||
..limit = 1
|
||||
..next = "text"
|
||||
..offset = 1
|
||||
..previous = "text"
|
||||
..total = 1;
|
||||
|
||||
static final User user = User()
|
||||
..id = "1"
|
||||
..displayName = "Your Name"
|
||||
..birthdate = "2021-01-01"
|
||||
..country = Market.BD
|
||||
..email = "test@email.com"
|
||||
..followers = followers
|
||||
..href = "text"
|
||||
..images = [image]
|
||||
..type = "type"
|
||||
..uri = "uri";
|
||||
|
||||
static final TracksLink tracksLink = TracksLink()
|
||||
..href = "text"
|
||||
..total = 1;
|
||||
|
||||
static final Playlist playlist = Playlist()
|
||||
..id = "1"
|
||||
..collaborative = false
|
||||
..description = "A very good playlist description"
|
||||
..externalUrls = externalUrls
|
||||
..followers = followers
|
||||
..href = "text"
|
||||
..images = [image]
|
||||
..name = "A good playlist"
|
||||
..owner = user
|
||||
..public = true
|
||||
..snapshotId = "text"
|
||||
..tracks = paging
|
||||
..tracksLink = tracksLink
|
||||
..type = "type"
|
||||
..uri = "uri";
|
||||
|
||||
static final PlaylistSimple playlistSimple = PlaylistSimple()
|
||||
..id = "1"
|
||||
..collaborative = false
|
||||
..externalUrls = externalUrls
|
||||
..href = "text"
|
||||
..images = [image]
|
||||
..name = "A good playlist"
|
||||
..owner = user
|
||||
..public = true
|
||||
..snapshotId = "text"
|
||||
..tracksLink = tracksLink
|
||||
..type = "type"
|
||||
..description = "A very good playlist description"
|
||||
..uri = "uri";
|
||||
|
||||
static final Category category = Category()
|
||||
..href = "text"
|
||||
..icons = [image]
|
||||
..id = "1"
|
||||
..name = "category";
|
||||
}
|
||||
232
lib/collections/gradients.dart
Normal file
232
lib/collections/gradients.dart
Normal file
@ -0,0 +1,232 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const gradients = [
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(123, 102, 255, 1),
|
||||
Color.fromRGBO(95, 189, 255, 1),
|
||||
Color.fromRGBO(150, 239, 255, 1),
|
||||
Color.fromRGBO(197, 255, 248, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(245, 204, 160, 1),
|
||||
Color.fromRGBO(228, 143, 69, 1),
|
||||
Color.fromRGBO(153, 77, 28, 1),
|
||||
Color.fromRGBO(107, 36, 12, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(243, 243, 243, 1),
|
||||
Color.fromRGBO(197, 232, 152, 1),
|
||||
Color.fromRGBO(41, 173, 178, 1),
|
||||
Color.fromRGBO(7, 102, 173, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(240, 89, 65, 1),
|
||||
Color.fromRGBO(190, 49, 68, 1),
|
||||
Color.fromRGBO(135, 35, 65, 1),
|
||||
Color.fromRGBO(34, 9, 44, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(119, 107, 93, 1),
|
||||
Color.fromRGBO(176, 166, 149, 1),
|
||||
Color.fromRGBO(235, 227, 213, 1),
|
||||
Color.fromRGBO(243, 238, 234, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(208, 162, 247, 1),
|
||||
Color.fromRGBO(220, 191, 255, 1),
|
||||
Color.fromRGBO(229, 212, 255, 1),
|
||||
Color.fromRGBO(241, 234, 255, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(221, 242, 253, 1),
|
||||
Color.fromRGBO(155, 190, 200, 1),
|
||||
Color.fromRGBO(66, 125, 157, 1),
|
||||
Color.fromRGBO(22, 72, 99, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(119, 67, 219, 1),
|
||||
Color.fromRGBO(195, 172, 208, 1),
|
||||
Color.fromRGBO(247, 239, 229, 1),
|
||||
Color.fromRGBO(255, 251, 245, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(194, 217, 255, 1),
|
||||
Color.fromRGBO(142, 143, 250, 1),
|
||||
Color.fromRGBO(119, 82, 254, 1),
|
||||
Color.fromRGBO(25, 4, 130, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(104, 126, 255, 1),
|
||||
Color.fromRGBO(128, 179, 255, 1),
|
||||
Color.fromRGBO(152, 228, 255, 1),
|
||||
Color.fromRGBO(182, 255, 250, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(176, 87, 141, 1),
|
||||
Color.fromRGBO(217, 136, 185, 1),
|
||||
Color.fromRGBO(250, 203, 234, 1),
|
||||
Color.fromRGBO(255, 228, 214, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(190, 255, 247, 1),
|
||||
Color.fromRGBO(166, 246, 255, 1),
|
||||
Color.fromRGBO(158, 221, 255, 1),
|
||||
Color.fromRGBO(100, 153, 233, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(245, 252, 205, 1),
|
||||
Color.fromRGBO(120, 214, 198, 1),
|
||||
Color.fromRGBO(65, 145, 151, 1),
|
||||
Color.fromRGBO(18, 72, 107, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(229, 207, 247, 1),
|
||||
Color.fromRGBO(157, 118, 193, 1),
|
||||
Color.fromRGBO(113, 58, 190, 1),
|
||||
Color.fromRGBO(91, 8, 136, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(249, 222, 201, 1),
|
||||
Color.fromRGBO(247, 140, 162, 1),
|
||||
Color.fromRGBO(216, 0, 50, 1),
|
||||
Color.fromRGBO(61, 12, 17, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(242, 247, 161, 1),
|
||||
Color.fromRGBO(53, 162, 159, 1),
|
||||
Color.fromRGBO(8, 131, 149, 1),
|
||||
Color.fromRGBO(7, 25, 82, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(243, 159, 90, 1),
|
||||
Color.fromRGBO(174, 68, 90, 1),
|
||||
Color.fromRGBO(102, 37, 73, 1),
|
||||
Color.fromRGBO(69, 25, 82, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(255, 200, 200, 1),
|
||||
Color.fromRGBO(255, 155, 130, 1),
|
||||
Color.fromRGBO(255, 63, 164, 1),
|
||||
Color.fromRGBO(87, 55, 93, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(238, 238, 238, 1),
|
||||
Color.fromRGBO(100, 204, 197, 1),
|
||||
Color.fromRGBO(23, 107, 135, 1),
|
||||
Color.fromRGBO(5, 59, 80, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(198, 61, 47, 1),
|
||||
Color.fromRGBO(226, 94, 62, 1),
|
||||
Color.fromRGBO(255, 155, 80, 1),
|
||||
Color.fromRGBO(255, 187, 92, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(236, 83, 176, 1),
|
||||
Color.fromRGBO(157, 68, 192, 1),
|
||||
Color.fromRGBO(77, 45, 183, 1),
|
||||
Color.fromRGBO(14, 33, 160, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(242, 236, 190, 1),
|
||||
Color.fromRGBO(226, 199, 153, 1),
|
||||
Color.fromRGBO(192, 130, 97, 1),
|
||||
Color.fromRGBO(154, 59, 59, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(255, 253, 140, 1),
|
||||
Color.fromRGBO(151, 255, 244, 1),
|
||||
Color.fromRGBO(112, 145, 245, 1),
|
||||
Color.fromRGBO(121, 63, 223, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(67, 83, 52, 1),
|
||||
Color.fromRGBO(158, 179, 132, 1),
|
||||
Color.fromRGBO(206, 222, 189, 1),
|
||||
Color.fromRGBO(250, 241, 228, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(250, 240, 230, 1),
|
||||
Color.fromRGBO(185, 180, 199, 1),
|
||||
Color.fromRGBO(92, 84, 112, 1),
|
||||
Color.fromRGBO(53, 47, 68, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(255, 186, 134, 1),
|
||||
Color.fromRGBO(246, 99, 92, 1),
|
||||
Color.fromRGBO(194, 51, 115, 1),
|
||||
Color.fromRGBO(121, 21, 91, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(213, 255, 208, 1),
|
||||
Color.fromRGBO(64, 248, 255, 1),
|
||||
Color.fromRGBO(39, 158, 255, 1),
|
||||
Color.fromRGBO(12, 53, 106, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(131, 96, 150, 1),
|
||||
Color.fromRGBO(237, 123, 123, 1),
|
||||
Color.fromRGBO(240, 184, 110, 1),
|
||||
Color.fromRGBO(235, 231, 108, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(63, 29, 56, 1),
|
||||
Color.fromRGBO(77, 60, 119, 1),
|
||||
Color.fromRGBO(162, 103, 138, 1),
|
||||
Color.fromRGBO(225, 152, 152, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(254, 123, 229, 1),
|
||||
Color.fromRGBO(151, 78, 195, 1),
|
||||
Color.fromRGBO(80, 64, 153, 1),
|
||||
Color.fromRGBO(49, 56, 102, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(248, 222, 34, 1),
|
||||
Color.fromRGBO(249, 76, 16, 1),
|
||||
Color.fromRGBO(199, 0, 57, 1),
|
||||
Color.fromRGBO(144, 12, 63, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(101, 69, 31, 1),
|
||||
Color.fromRGBO(118, 88, 39, 1),
|
||||
Color.fromRGBO(200, 174, 125, 1),
|
||||
Color.fromRGBO(234, 198, 150, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(255, 246, 224, 1),
|
||||
Color.fromRGBO(216, 217, 218, 1),
|
||||
Color.fromRGBO(97, 103, 122, 1),
|
||||
Color.fromRGBO(39, 40, 41, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(145, 109, 179, 1),
|
||||
Color.fromRGBO(228, 133, 134, 1),
|
||||
Color.fromRGBO(252, 186, 173, 1),
|
||||
Color.fromRGBO(253, 229, 236, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(124, 115, 192, 1),
|
||||
Color.fromRGBO(148, 173, 215, 1),
|
||||
Color.fromRGBO(172, 250, 223, 1),
|
||||
Color.fromRGBO(232, 255, 206, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(174, 216, 204, 1),
|
||||
Color.fromRGBO(205, 102, 136, 1),
|
||||
Color.fromRGBO(122, 49, 111, 1),
|
||||
Color.fromRGBO(70, 25, 89, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(237, 228, 255, 1),
|
||||
Color.fromRGBO(215, 187, 245, 1),
|
||||
Color.fromRGBO(160, 118, 249, 1),
|
||||
Color.fromRGBO(101, 40, 247, 1)
|
||||
]),
|
||||
LinearGradient(colors: [
|
||||
Color.fromRGBO(255, 236, 175, 1),
|
||||
Color.fromRGBO(255, 176, 127, 1),
|
||||
Color.fromRGBO(255, 82, 162, 1),
|
||||
Color.fromRGBO(243, 21, 89, 1)
|
||||
]),
|
||||
];
|
||||
25
lib/collections/initializers.dart
Normal file
25
lib/collections/initializers.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:win32_registry/win32_registry.dart';
|
||||
|
||||
Future<void> registerWindowsScheme(String scheme) async {
|
||||
if (!DesktopTools.platform.isWindows) return;
|
||||
String appPath = Platform.resolvedExecutable;
|
||||
|
||||
String protocolRegKey = 'Software\\Classes\\$scheme';
|
||||
RegistryValue protocolRegValue = const RegistryValue(
|
||||
'URL Protocol',
|
||||
RegistryValueType.string,
|
||||
'',
|
||||
);
|
||||
String protocolCmdRegKey = 'shell\\open\\command';
|
||||
RegistryValue protocolCmdRegValue = RegistryValue(
|
||||
'',
|
||||
RegistryValueType.string,
|
||||
'"$appPath" "%1"',
|
||||
);
|
||||
|
||||
final regKey = Registry.currentUser.createKey(protocolRegKey);
|
||||
regKey.createValue(protocolRegValue);
|
||||
regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue);
|
||||
}
|
||||
@ -164,10 +164,10 @@ abstract class LanguageLocals {
|
||||
// name: "Maldivian;",
|
||||
// nativeName: "ދިވެހި",
|
||||
// ),
|
||||
// "nl": const ISOLanguageName(
|
||||
// name: "Dutch",
|
||||
// nativeName: "Vlaams",
|
||||
// ),
|
||||
"nl": const ISOLanguageName(
|
||||
name: "Dutch",
|
||||
nativeName: "Nederlands",
|
||||
),
|
||||
"en": const ISOLanguageName(
|
||||
name: "English",
|
||||
nativeName: "English",
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/foundation.dart' hide Category;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotify/spotify.dart' hide Search;
|
||||
import 'package:spotube/pages/album/album.dart';
|
||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||
import 'package:spotube/pages/home/genres/genres.dart';
|
||||
import 'package:spotube/pages/home/home.dart';
|
||||
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||
@ -38,6 +40,21 @@ final router = GoRouter(
|
||||
GoRoute(
|
||||
path: "/",
|
||||
pageBuilder: (context, state) => const SpotubePage(child: HomePage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "genres",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: GenrePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "genre/:categoryId",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: GenrePlaylistsPage(
|
||||
category: state.extra as Category,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/search",
|
||||
|
||||
@ -108,4 +108,5 @@ abstract class SpotubeIcons {
|
||||
static const noEye = FeatherIcons.eyeOff;
|
||||
static const normalize = FeatherIcons.barChart2;
|
||||
static const wikipedia = SimpleIcons.wikipedia;
|
||||
static const discord = SimpleIcons.discord;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
@ -91,12 +92,14 @@ class ArtistCard extends HookConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(50)),
|
||||
child: Text(
|
||||
context.l10n.artist,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Skeleton.ignore(
|
||||
child: Text(
|
||||
context.l10n.artist,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
class CategoryCard extends HookConsumerWidget {
|
||||
final Category category;
|
||||
CategoryCard(
|
||||
this.category, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final logger = getLogger(CategoryCard);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlistQuery = useQueries.category.playlistsOf(
|
||||
ref,
|
||||
category.id!,
|
||||
);
|
||||
|
||||
final playlists = useMemoized(
|
||||
() => playlistQuery.pages.expand(
|
||||
(page) {
|
||||
return page.items?.whereNotNull() ??
|
||||
const Iterable<PlaylistSimple>.empty();
|
||||
},
|
||||
).toList(),
|
||||
[playlistQuery.pages],
|
||||
);
|
||||
|
||||
if (playlistQuery.hasErrors &&
|
||||
!playlistQuery.hasPageData &&
|
||||
!playlistQuery.isLoadingNextPage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||
title: Text(category.name!),
|
||||
isLoadingNextPage: playlistQuery.isLoadingNextPage,
|
||||
hasNextPage: playlistQuery.hasNextPage,
|
||||
items: playlists,
|
||||
onFetchMore: playlistQuery.fetchNext,
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/components/home/sections/featured.dart
Normal file
36
lib/components/home/sections/featured.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
class HomeFeaturedSection extends HookConsumerWidget {
|
||||
const HomeFeaturedSection({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
|
||||
final playlists = useMemoized(
|
||||
() => featuredPlaylistsQuery.pages
|
||||
.whereType<Page<PlaylistSimple>>()
|
||||
.expand((page) => page.items ?? const <PlaylistSimple>[]),
|
||||
[featuredPlaylistsQuery.pages],
|
||||
);
|
||||
final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
|
||||
!featuredPlaylistsQuery.isLoadingNextPage;
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: isLoadingFeaturedPlaylists,
|
||||
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||
items: playlists.toList(),
|
||||
title: Text(context.l10n.featured),
|
||||
isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage,
|
||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
154
lib/components/home/sections/genres.dart
Normal file
154
lib/components/home/sections/genres.dart
Normal file
@ -0,0 +1,154 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/gradients.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
class HomeGenresSection extends HookConsumerWidget {
|
||||
const HomeGenresSection({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final recommendationMarket = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||
);
|
||||
final categoriesQuery =
|
||||
useQueries.category.listAll(ref, recommendationMarket);
|
||||
|
||||
final categories = categoriesQuery.data
|
||||
?.where((c) => (c.icons?.length ?? 0) > 0)
|
||||
.take(mediaQuery.mdAndDown ? 6 : 10)
|
||||
.toList() ??
|
||||
<Category>[];
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.genres,
|
||||
style: textTheme.headlineSmall,
|
||||
),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
context.push('/genres');
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(
|
||||
"Browse All",
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(8),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: Skeletonizer.sliver(
|
||||
enabled: categoriesQuery.isLoading,
|
||||
child: SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: mediaQuery.mdAndDown ? 200 : 250,
|
||||
mainAxisExtent: 50,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: categoriesQuery.isLoading
|
||||
? mediaQuery.mdAndDown
|
||||
? 6
|
||||
: 10
|
||||
: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category =
|
||||
categories.elementAtOrNull(index) ?? FakeData.category;
|
||||
|
||||
return HookBuilder(builder: (context) {
|
||||
final (:gradient, :textColor) = useMemoized(
|
||||
() {
|
||||
final gradient =
|
||||
gradients[Random().nextInt(gradients.length)];
|
||||
final text = gradient.colors
|
||||
.take(2)
|
||||
.any((c) => c.computeLuminance() > 0.5)
|
||||
? Colors.grey[900]
|
||||
: Colors.white;
|
||||
return (
|
||||
gradient: LinearGradient(
|
||||
colors: gradient.colors
|
||||
.map((c) => c.withOpacity(0.8))
|
||||
.toList(),
|
||||
),
|
||||
textColor: text
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.push('/genre/${category.id}', extra: category);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(
|
||||
category.icons!.first.url!,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: colorScheme.surfaceVariant,
|
||||
gradient: categoriesQuery.isLoading ? null : gradient,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
category.name!,
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(color: textColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/components/home/sections/made_for_user.dart
Normal file
35
lib/components/home/sections/made_for_user.dart
Normal file
@ -0,0 +1,35 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
class HomeMadeForUserSection extends HookConsumerWidget {
|
||||
const HomeMadeForUserSection({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final madeForUser = useQueries.views.get(ref, "made-for-x-hub");
|
||||
|
||||
return SliverList.builder(
|
||||
itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final item = madeForUser.data?["content"]?["items"]?[index];
|
||||
final playlists = item["content"]?["items"]
|
||||
?.where((itemL2) => itemL2["type"] == "playlist")
|
||||
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
||||
.toList()
|
||||
.cast<PlaylistSimple>() ??
|
||||
<PlaylistSimple>[];
|
||||
if (playlists.isEmpty) return const SizedBox.shrink();
|
||||
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||
items: playlists,
|
||||
title: Text(item["name"] ?? ""),
|
||||
hasNextPage: false,
|
||||
isLoadingNextPage: false,
|
||||
onFetchMore: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
51
lib/components/home/sections/new_releases.dart
Normal file
51
lib/components/home/sections/new_releases.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||
const HomeNewReleasesSection({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
|
||||
final newReleases = useQueries.album.newReleases(ref);
|
||||
final userArtistsQuery = useQueries.artist.followedByMeAll(ref);
|
||||
final userArtists =
|
||||
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
|
||||
|
||||
final albums = useMemoized(
|
||||
() => newReleases.pages
|
||||
.whereType<Page<AlbumSimple>>()
|
||||
.expand((page) => page.items ?? const <AlbumSimple>[])
|
||||
.where((album) {
|
||||
return album.artists
|
||||
?.any((artist) => userArtists.contains(artist.id!)) ==
|
||||
true;
|
||||
})
|
||||
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album))
|
||||
.toList(),
|
||||
[newReleases.pages],
|
||||
);
|
||||
|
||||
final hasNewReleases = newReleases.hasPageData &&
|
||||
userArtistsQuery.hasData &&
|
||||
!newReleases.isLoadingNextPage;
|
||||
|
||||
if (auth == null || !hasNewReleases) return const SizedBox.shrink();
|
||||
|
||||
return HorizontalPlaybuttonCardView<Album>(
|
||||
items: albums,
|
||||
title: Text(context.l10n.new_releases),
|
||||
isLoadingNextPage: newReleases.isLoadingNextPage,
|
||||
hasNextPage: newReleases.hasNextPage,
|
||||
onFetchMore: newReleases.fetchNext,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/album/album_card.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/shared/waypoint.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
@ -82,30 +84,39 @@ class UserAlbums extends HookConsumerWidget {
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
controller: controller,
|
||||
child: Wrap(
|
||||
runSpacing: 20,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (albums.isEmpty)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: const ShimmerPlaybuttonCard(count: 4),
|
||||
),
|
||||
for (final album in albums)
|
||||
AlbumCard(
|
||||
TypeConversionUtils.simpleAlbum_X_Album(album),
|
||||
),
|
||||
if (albumsQuery.hasNextPage)
|
||||
Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: albumsQuery.fetchNext,
|
||||
child: const ShimmerPlaybuttonCard(count: 1),
|
||||
)
|
||||
],
|
||||
child: Skeletonizer(
|
||||
enabled: albumsQuery.pages.isEmpty,
|
||||
child: Center(
|
||||
child: Wrap(
|
||||
runSpacing: 20,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (albumsQuery.pages.isEmpty)
|
||||
...List.generate(
|
||||
10,
|
||||
(index) => AlbumCard(FakeData.album),
|
||||
)
|
||||
else if (albums.isEmpty)
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [NotFound()],
|
||||
),
|
||||
for (final album in albums)
|
||||
AlbumCard(
|
||||
TypeConversionUtils.simpleAlbum_X_Album(album),
|
||||
),
|
||||
if (albums.isNotEmpty && albumsQuery.hasNextPage)
|
||||
Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: albumsQuery.fetchNext,
|
||||
child: AlbumCard(FakeData.album),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -3,10 +3,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/artist/artist_card.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
@ -87,12 +90,29 @@ class UserArtists extends HookConsumerWidget {
|
||||
width: double.infinity,
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Wrap(
|
||||
spacing: 15,
|
||||
runSpacing: 5,
|
||||
children: filteredArtists
|
||||
.mapIndexed((index, artist) => ArtistCard(artist))
|
||||
.toList(),
|
||||
child: Skeletonizer(
|
||||
enabled: artistQuery.isLoading,
|
||||
child: Wrap(
|
||||
spacing: 15,
|
||||
runSpacing: 5,
|
||||
children: artistQuery.isLoading
|
||||
? List.generate(
|
||||
10, (index) => ArtistCard(FakeData.artist))
|
||||
: filteredArtists.isEmpty
|
||||
? [
|
||||
const Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
NotFound(),
|
||||
],
|
||||
)
|
||||
]
|
||||
: filteredArtists
|
||||
.mapIndexed((index, artist) =>
|
||||
ArtistCard(artist))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -11,12 +11,14 @@ import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
|
||||
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
|
||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
@ -254,6 +256,15 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
.toList();
|
||||
}, [searchController.text, sortedTracks]);
|
||||
|
||||
if (!trackSnapshot.isLoading && filteredTracks.isEmpty) {
|
||||
return const Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [NotFound()],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
@ -261,32 +272,48 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
},
|
||||
child: InterScrollbar(
|
||||
controller: controller,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: filteredTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = filteredTracks[index];
|
||||
return TrackTile(
|
||||
index: index,
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
onTap: () async {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Skeletonizer(
|
||||
enabled: trackSnapshot.isLoading,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount:
|
||||
trackSnapshot.isLoading ? 5 : filteredTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (trackSnapshot.isLoading) {
|
||||
return TrackTile(track: FakeData.track, index: index);
|
||||
}
|
||||
|
||||
final track = filteredTracks[index];
|
||||
return TrackTile(
|
||||
index: index,
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
onTap: () async {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () =>
|
||||
const Expanded(child: ShimmerTrackTileGroup(noSliver: true)),
|
||||
loading: () => Expanded(
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: ListView.builder(
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) =>
|
||||
TrackTile(track: FakeData.track, index: index),
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) =>
|
||||
Text(error.toString() + stackTrace.toString()),
|
||||
)
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/playlist/playlist_card.dart';
|
||||
import 'package:spotube/components/shared/waypoint.dart';
|
||||
@ -123,7 +123,7 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
),
|
||||
SliverLayoutBuilder(builder: (context, constrains) {
|
||||
return SliverGrid.builder(
|
||||
itemCount: playlists.length + 1,
|
||||
itemCount: playlists.isEmpty ? 6 : playlists.length + 1,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||
@ -131,7 +131,7 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == playlists.length) {
|
||||
if (playlists.isNotEmpty && index == playlists.length) {
|
||||
if (!playlistsQuery.hasNextPage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@ -140,11 +140,17 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: playlistsQuery.fetchNext,
|
||||
child: const ShimmerPlaybuttonCard(count: 1),
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: PlaylistCard(FakeData.playlistSimple),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PlaylistCard(playlists[index]);
|
||||
return PlaylistCard(
|
||||
playlists.elementAtOrNull(index) ??
|
||||
FakeData.playlistSimple,
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
|
||||
@ -28,9 +28,11 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PlayerView extends HookConsumerWidget {
|
||||
final PanelController panelController;
|
||||
final ScrollController scrollController;
|
||||
const PlayerView({
|
||||
Key? key,
|
||||
required this.panelController,
|
||||
required this.scrollController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -72,10 +74,14 @@ class PlayerView extends HookConsumerWidget {
|
||||
useMemoized(() => GlobalKey(), []);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = false;
|
||||
for (final renderView in WidgetsBinding.instance.renderViews) {
|
||||
renderView.automaticSystemUiAdjustment = false;
|
||||
}
|
||||
|
||||
return () {
|
||||
WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = true;
|
||||
for (final renderView in WidgetsBinding.instance.renderViews) {
|
||||
renderView.automaticSystemUiAdjustment = true;
|
||||
}
|
||||
};
|
||||
}, [panelController.isPanelOpen]);
|
||||
|
||||
@ -88,10 +94,10 @@ class PlayerView extends HookConsumerWidget {
|
||||
|
||||
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
panelController.close();
|
||||
return false;
|
||||
},
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
||||
@ -119,40 +125,43 @@ class PlayerView extends HookConsumerWidget {
|
||||
preferredSize: Size.fromHeight(
|
||||
kToolbarHeight + topPadding,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
child: PageWindowTitleBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: titleTextColor,
|
||||
toolbarOpacity: 1,
|
||||
leading: IconButton(
|
||||
icon: const Icon(SpotubeIcons.angleDown, size: 18),
|
||||
onPressed: panelController.close,
|
||||
child: ForceDraggableWidget(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
child: PageWindowTitleBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: titleTextColor,
|
||||
toolbarOpacity: 1,
|
||||
leading: IconButton(
|
||||
icon: const Icon(SpotubeIcons.angleDown, size: 18),
|
||||
onPressed: panelController.close,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||
tooltip: context.l10n.details,
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: bodyTextColor),
|
||||
onPressed: currentTrack == null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TrackDetailsDialog(
|
||||
track: currentTrack,
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||
tooltip: context.l10n.details,
|
||||
style:
|
||||
IconButton.styleFrom(foregroundColor: bodyTextColor),
|
||||
onPressed: currentTrack == null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TrackDetailsDialog(
|
||||
track: currentTrack,
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: double.infinity,
|
||||
@ -163,27 +172,29 @@ class PlayerView extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 300, maxWidth: 300),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
spreadRadius: 2,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 0),
|
||||
ForceDraggableWidget(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 300, maxWidth: 300),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
spreadRadius: 2,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: UniversalImage(
|
||||
path: albumArt,
|
||||
placeholder: Assets.albumPlaceholder.path,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: UniversalImage(
|
||||
path: albumArt,
|
||||
placeholder: Assets.albumPlaceholder.path,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -43,6 +43,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final panelController = useMemoized(() => PanelController(), []);
|
||||
final scrollController = useScrollController();
|
||||
|
||||
useEffect(() {
|
||||
return () {
|
||||
@ -174,6 +175,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
scrollController: scrollController,
|
||||
panelBuilder: (position) {
|
||||
// this is the reason we're getting an update
|
||||
final navigationHeight = ref.watch(navigationPanelHeight);
|
||||
@ -188,8 +190,11 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
decoration: navigationHeight == 0
|
||||
? const BoxDecoration(borderRadius: BorderRadius.zero)
|
||||
: const BoxDecoration(borderRadius: radius),
|
||||
child: HorizontalScrollableWidget(
|
||||
child: PlayerView(panelController: panelController),
|
||||
child: IgnoreDraggableWidget(
|
||||
child: PlayerView(
|
||||
panelController: panelController,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -2,11 +2,12 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/album/album_card.dart';
|
||||
import 'package:spotube/components/artist/artist_card.dart';
|
||||
import 'package:spotube/components/playlist/playlist_card.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -61,30 +62,41 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: InfiniteList(
|
||||
scrollController: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
itemCount: items.length,
|
||||
onFetchData: onFetchMore,
|
||||
loadingBuilder: (context) => const ShimmerPlaybuttonCard(),
|
||||
emptyBuilder: (context) =>
|
||||
const ShimmerPlaybuttonCard(count: 5),
|
||||
isLoading: isLoadingNextPage,
|
||||
hasReachedMax: !hasNextPage,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
child: items.isEmpty
|
||||
? ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return AlbumCard(FakeData.albumSimple);
|
||||
},
|
||||
)
|
||||
: InfiniteList(
|
||||
scrollController: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
itemCount: items.length,
|
||||
onFetchData: onFetchMore,
|
||||
loadingBuilder: (context) => Skeletonizer(
|
||||
enabled: true,
|
||||
child: AlbumCard(FakeData.albumSimple),
|
||||
),
|
||||
isLoading: isLoadingNextPage,
|
||||
hasReachedMax: !hasNextPage,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
|
||||
return switch (item.runtimeType) {
|
||||
PlaylistSimple => PlaylistCard(item as PlaylistSimple),
|
||||
Album => AlbumCard(item as Album),
|
||||
Artist => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: ArtistCard(item as Artist),
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}),
|
||||
return switch (item.runtimeType) {
|
||||
PlaylistSimple =>
|
||||
PlaylistCard(item as PlaylistSimple),
|
||||
Album => AlbumCard(item as Album),
|
||||
Artist => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: ArtistCard(item as Artist),
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -11,16 +11,6 @@ import 'dart:io' show Platform, exit;
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
|
||||
final closeNotification = DesktopTools.createNotification(
|
||||
title: 'Spotube',
|
||||
message: 'Running in background. Minimized to System Tray',
|
||||
actions: [
|
||||
LocalNotificationAction(text: 'Close The App'),
|
||||
],
|
||||
)?..onClickAction = (value) {
|
||||
exit(0);
|
||||
};
|
||||
|
||||
class PageWindowTitleBar extends StatefulHookConsumerWidget
|
||||
implements PreferredSizeWidget {
|
||||
final Widget? leading;
|
||||
@ -113,12 +103,7 @@ class WindowTitleBarButtons extends HookConsumerWidget {
|
||||
const type = ThemeType.auto;
|
||||
|
||||
Future<void> onClose() async {
|
||||
if (preferences.closeBehavior == CloseBehavior.close) {
|
||||
exit(0);
|
||||
} else {
|
||||
await DesktopTools.window.hide();
|
||||
await closeNotification?.show();
|
||||
}
|
||||
await DesktopTools.window.close();
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/hover_builder.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||
|
||||
@ -48,6 +50,7 @@ class PlaybuttonCard extends HookWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final textsKey = useMemoized(() => GlobalKey(), []);
|
||||
final theme = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final radius = BorderRadius.circular(15);
|
||||
|
||||
final double size = useBreakpointValue<double>(
|
||||
@ -58,8 +61,8 @@ class PlaybuttonCard extends HookWidget {
|
||||
);
|
||||
|
||||
final end = useBreakpointValue<double>(
|
||||
xs: 10,
|
||||
sm: 10,
|
||||
xs: 7,
|
||||
sm: 7,
|
||||
others: 15,
|
||||
);
|
||||
|
||||
@ -84,22 +87,28 @@ class PlaybuttonCard extends HookWidget {
|
||||
splashFactory: theme.splashFactory,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
),
|
||||
child: ClipRRect(
|
||||
height: mediaQuery.smAndDown
|
||||
? 120
|
||||
: mediaQuery.mdAndDown
|
||||
? 130
|
||||
: 150,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: radius,
|
||||
child: UniversalImage(
|
||||
path: imageUrl,
|
||||
placeholder: Assets.albumPlaceholder.path,
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(imageUrl),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -146,31 +155,35 @@ class PlaybuttonCard extends HookWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isPlaying)
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
minimumSize: const Size.square(10),
|
||||
Skeleton.keep(
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.background,
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
minimumSize: const Size.square(10),
|
||||
),
|
||||
icon: const Icon(SpotubeIcons.queueAdd),
|
||||
onPressed: isLoading ? null : onAddToQueuePressed,
|
||||
),
|
||||
icon: const Icon(SpotubeIcons.queueAdd),
|
||||
onPressed: isLoading ? null : onAddToQueuePressed,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Gap(5),
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
minimumSize: const Size.square(10),
|
||||
),
|
||||
icon: isLoading
|
||||
? SizedBox.fromSize(
|
||||
size: const Size.square(15),
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2),
|
||||
)
|
||||
: isPlaying
|
||||
? const Icon(SpotubeIcons.pause)
|
||||
: const Icon(SpotubeIcons.play),
|
||||
icon: Skeleton.keep(
|
||||
child: isLoading
|
||||
? SizedBox.fromSize(
|
||||
size: const Size.square(15),
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2),
|
||||
)
|
||||
: isPlaying
|
||||
? const Icon(SpotubeIcons.pause)
|
||||
: const Icon(SpotubeIcons.play),
|
||||
),
|
||||
onPressed: isLoading ? null : onPlaybuttonPressed,
|
||||
),
|
||||
],
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:skeleton_text/skeleton_text.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
|
||||
import 'package:spotube/extensions/theme.dart';
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
|
||||
class ShimmerArtistProfile extends HookWidget {
|
||||
const ShimmerArtistProfile({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final shimmerTheme = ShimmerColorTheme(
|
||||
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
|
||||
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300],
|
||||
);
|
||||
final shimmerColor = shimmerTheme.shimmerColor ?? Colors.white;
|
||||
final shimmerBackgroundColor =
|
||||
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
|
||||
|
||||
final avatarWidth = useBreakpointValue(
|
||||
xs: MediaQuery.of(context).size.width * 0.80,
|
||||
sm: MediaQuery.of(context).size.width * 0.80,
|
||||
md: MediaQuery.of(context).size.width * 0.50,
|
||||
lg: MediaQuery.of(context).size.width * 0.30,
|
||||
xl: MediaQuery.of(context).size.width * 0.30,
|
||||
xxl: MediaQuery.of(context).size.width * 0.30,
|
||||
) ??
|
||||
0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SkeletonAnimation(
|
||||
shimmerColor: shimmerColor,
|
||||
borderRadius: BorderRadius.circular(avatarWidth),
|
||||
shimmerDuration: 1000,
|
||||
child: Container(
|
||||
width: avatarWidth,
|
||||
height: avatarWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: shimmerBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(avatarWidth),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Flexible(child: ShimmerTrackTileGroup(noSliver: true)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
||||
import 'package:spotube/extensions/theme.dart';
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
|
||||
class ShimmerCategories extends HookWidget {
|
||||
const ShimmerCategories({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final shimmerTheme = ShimmerColorTheme(
|
||||
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
|
||||
);
|
||||
final shimmerBackgroundColor =
|
||||
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
|
||||
|
||||
final shimmerCount = useBreakpointValue(
|
||||
xs: 2,
|
||||
sm: 2,
|
||||
md: 3,
|
||||
lg: 3,
|
||||
xl: 6,
|
||||
xxl: 8,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
height: 10,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: shimmerBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ShimmerPlaybuttonCard(count: shimmerCount),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,69 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import 'package:skeleton_text/skeleton_text.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/theme.dart';
|
||||
|
||||
const widths = [20, 56, 89, 60, 25, 69];
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class ShimmerLyrics extends HookWidget {
|
||||
const ShimmerLyrics({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final shimmerTheme = ShimmerColorTheme(
|
||||
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
|
||||
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300],
|
||||
);
|
||||
final shimmerColor = shimmerTheme.shimmerColor ?? Colors.white;
|
||||
final shimmerBackgroundColor =
|
||||
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
|
||||
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: 20,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final widthsCp = [...widths];
|
||||
if (mediaQuery.isMd) {
|
||||
widthsCp.removeLast();
|
||||
}
|
||||
if (mediaQuery.smAndDown) {
|
||||
widthsCp.removeLast();
|
||||
widthsCp.removeLast();
|
||||
}
|
||||
widthsCp.shuffle();
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
return Skeletonizer(
|
||||
enabled: true,
|
||||
child: ListView.builder(
|
||||
itemCount: 30,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final texts = [
|
||||
"Lorem ipsum",
|
||||
"consectetur.",
|
||||
"Sed",
|
||||
"Sed non risus",
|
||||
]..shuffle();
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: widthsCp.map(
|
||||
(width) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: SkeletonAnimation(
|
||||
shimmerColor: shimmerColor,
|
||||
shimmerDuration: 1000,
|
||||
child: Container(
|
||||
height: 10,
|
||||
width: width.toDouble(),
|
||||
decoration: BoxDecoration(
|
||||
color: shimmerBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
children: [
|
||||
for (final text in texts) ...[
|
||||
Text(text),
|
||||
if (text != texts.last) const Gap(10),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
|
||||
class ShimmerPlaybuttonCardPainter extends CustomPainter {
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
ShimmerPlaybuttonCardPainter({
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
const radius = Radius.circular(15);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
radius,
|
||||
),
|
||||
Paint()..color = background,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(8, 8, size.width - 16, size.height - 90),
|
||||
radius,
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(12, size.height - 67, size.width / 2, 10),
|
||||
radius,
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(12, size.height - 45, size.width - 24, 8),
|
||||
radius,
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(12, size.height - 30, size.width * .4, 8),
|
||||
radius,
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(size.width * .85, size.height * .50),
|
||||
17,
|
||||
Paint()..color = background,
|
||||
);
|
||||
|
||||
canvas.drawCircle(
|
||||
Offset(size.width * .85, size.height * .67),
|
||||
17,
|
||||
Paint()..color = background,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ShimmerPlaybuttonCard extends HookWidget {
|
||||
final int count;
|
||||
|
||||
const ShimmerPlaybuttonCard({
|
||||
Key? key,
|
||||
this.count = 1,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final Size size = useBreakpointValue<Size>(
|
||||
xs: const Size(130, 200),
|
||||
sm: const Size(130, 200),
|
||||
md: const Size(150, 220),
|
||||
others: const Size(170, 240),
|
||||
);
|
||||
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final bgColor = theme.colorScheme.surfaceVariant.withOpacity(.2);
|
||||
final fgColor = Color.lerp(
|
||||
theme.colorScheme.surfaceVariant,
|
||||
isDark ? Colors.black : Colors.white,
|
||||
.4,
|
||||
);
|
||||
|
||||
return Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 20,
|
||||
children: [
|
||||
for (var i = 0; i < count; i++) ...[
|
||||
CustomPaint(
|
||||
size: size,
|
||||
painter: ShimmerPlaybuttonCardPainter(
|
||||
background: bgColor,
|
||||
foreground: fgColor!,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:spotube/extensions/theme.dart';
|
||||
|
||||
class ShimmerTrackTilePainter extends CustomPainter {
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
ShimmerTrackTilePainter({
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = background
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
const Radius.circular(5),
|
||||
),
|
||||
paint,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.height, size.height),
|
||||
const Radius.circular(5),
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(70, 10, 100, 10),
|
||||
const Radius.circular(5),
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
|
||||
// draw Icons.play
|
||||
const icon = Icons.play_arrow_outlined;
|
||||
TextPainter textPainter = TextPainter(textDirection: TextDirection.rtl);
|
||||
textPainter.text = TextSpan(
|
||||
text: String.fromCharCode(icon.codePoint),
|
||||
style: TextStyle(
|
||||
fontSize: 40.0,
|
||||
fontFamily: icon.fontFamily,
|
||||
color: background,
|
||||
),
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(canvas, const Offset(10, 10));
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(70, 30, 170, 7),
|
||||
const Radius.circular(5),
|
||||
),
|
||||
Paint()..color = foreground,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ShimmerTrackTile extends StatelessWidget {
|
||||
const ShimmerTrackTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final shimmerTheme = ShimmerColorTheme(
|
||||
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
|
||||
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300],
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0, left: 8, right: 8),
|
||||
child: CustomPaint(
|
||||
size: const Size(double.infinity, 60),
|
||||
painter: ShimmerTrackTilePainter(
|
||||
background: shimmerTheme.shimmerBackgroundColor ??
|
||||
theme.scaffoldBackgroundColor,
|
||||
foreground: shimmerTheme.shimmerColor ?? theme.cardColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShimmerTrackTileGroup extends StatelessWidget {
|
||||
final bool noSliver;
|
||||
final int count;
|
||||
const ShimmerTrackTileGroup({
|
||||
super.key,
|
||||
this.noSliver = false,
|
||||
this.count = 5,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (noSliver) {
|
||||
return ListView.builder(
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => const ShimmerTrackTile(),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) => const ShimmerTrackTile(),
|
||||
childCount: count,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
@ -12,6 +13,7 @@ import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart
|
||||
import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
|
||||
import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
@ -22,6 +24,7 @@ import 'package:spotube/services/mutations/mutations.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
enum TrackOptionValue {
|
||||
album,
|
||||
share,
|
||||
addToPlaylist,
|
||||
addToQueue,
|
||||
@ -79,9 +82,12 @@ class TrackOptions extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final router = GoRouter.of(context);
|
||||
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
ref.watch(downloadManagerProvider);
|
||||
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
||||
@ -122,6 +128,12 @@ class TrackOptions extends HookConsumerWidget {
|
||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case TrackOptionValue.album:
|
||||
await router.push(
|
||||
'/album/${track.album!.id}',
|
||||
extra: track.album!,
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.delete:
|
||||
await File((track as LocalTrack).path).delete();
|
||||
ref.refresh(localTracksProvider);
|
||||
@ -233,6 +245,13 @@ class TrackOptions extends HookConsumerWidget {
|
||||
)
|
||||
],
|
||||
_ => [
|
||||
if (mediaQuery.smAndDown)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.album,
|
||||
leading: const Icon(SpotubeIcons.album),
|
||||
title: Text(context.l10n.go_to_album),
|
||||
subtitle: Text(track.album!.name!),
|
||||
),
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/hover_builder.dart';
|
||||
@ -158,26 +159,28 @@ class TrackTile extends HookConsumerWidget {
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme
|
||||
.copyWith(size: 26, color: Colors.white),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: (isPlaying && playlist.isFetching) ||
|
||||
isLoading.value
|
||||
? const SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: isPlaying
|
||||
? Icon(
|
||||
SpotubeIcons.pause,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: !isHovering
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(SpotubeIcons.play),
|
||||
child: Skeleton.ignore(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: (isPlaying && playlist.isFetching) ||
|
||||
isLoading.value
|
||||
? const SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: isPlaying
|
||||
? Icon(
|
||||
SpotubeIcons.pause,
|
||||
color: theme.colorScheme.primary,
|
||||
)
|
||||
: !isHovering
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(SpotubeIcons.play),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -4,9 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
|
||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
|
||||
@ -84,7 +86,22 @@ class TrackViewBodySection extends HookConsumerWidget {
|
||||
onFetchData: props.pagination.onFetchMore,
|
||||
isLoading: props.pagination.isLoading,
|
||||
hasReachedMax: !props.pagination.hasNextPage,
|
||||
loadingBuilder: (context) => const ShimmerTrackTile(),
|
||||
loadingBuilder: (context) => Skeletonizer(
|
||||
enabled: true,
|
||||
child: TrackTile(
|
||||
track: FakeData.track,
|
||||
index: 0,
|
||||
),
|
||||
),
|
||||
emptyBuilder: (context) => Skeletonizer(
|
||||
enabled: true,
|
||||
child: Column(
|
||||
children: List.generate(
|
||||
10,
|
||||
(index) => TrackTile(track: FakeData.track, index: index),
|
||||
),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final track = tracks[index];
|
||||
return TrackTile(
|
||||
|
||||
@ -88,50 +88,68 @@ class TrackViewFlexHeader extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flex(
|
||||
direction: mediaQuery.mdAndDown
|
||||
? Axis.vertical
|
||||
: Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: props.image,
|
||||
width: 200,
|
||||
height: 200,
|
||||
placeholder: Assets.albumPlaceholder.path,
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: mediaQuery.mdAndDown
|
||||
? mediaQuery.size.width
|
||||
: 800,
|
||||
),
|
||||
child: Flex(
|
||||
direction: mediaQuery.mdAndDown
|
||||
? Axis.vertical
|
||||
: Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: props.image,
|
||||
width: 200,
|
||||
height: 200,
|
||||
placeholder: Assets.albumPlaceholder.path,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(20),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: mediaQuery.mdAndDown
|
||||
? CrossAxisAlignment.center
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(props.title, style: headingStyle),
|
||||
const SizedBox(height: 10),
|
||||
if (description != null &&
|
||||
description.isNotEmpty)
|
||||
Text(
|
||||
description,
|
||||
style: defaultTextStyle.style.copyWith(
|
||||
color: palette.bodyTextColor,
|
||||
const Gap(20),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: mediaQuery.mdAndDown
|
||||
? CrossAxisAlignment.center
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
props.title,
|
||||
style: headingStyle,
|
||||
textAlign: mediaQuery.mdAndDown
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
textAlign: mediaQuery.mdAndDown
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Gap(10),
|
||||
const TrackViewHeaderActions(),
|
||||
const Gap(10),
|
||||
TrackViewHeaderButtons(color: palette),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
if (description != null &&
|
||||
description.isNotEmpty)
|
||||
Text(
|
||||
description,
|
||||
style:
|
||||
defaultTextStyle.style.copyWith(
|
||||
color: palette.bodyTextColor,
|
||||
),
|
||||
textAlign: mediaQuery.mdAndDown
|
||||
? TextAlign.center
|
||||
: TextAlign.start,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Gap(10),
|
||||
const TrackViewHeaderActions(),
|
||||
const Gap(10),
|
||||
TrackViewHeaderButtons(color: palette),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -3,7 +3,6 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||
@ -28,16 +27,17 @@ class TrackView extends HookConsumerWidget {
|
||||
)
|
||||
: null,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const TrackViewFlexHeader(),
|
||||
SliverAnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: props.tracks.isEmpty
|
||||
? const ShimmerTrackTileGroup()
|
||||
: const TrackViewBodySection(),
|
||||
),
|
||||
],
|
||||
body: RefreshIndicator(
|
||||
onRefresh: props.pagination.onRefresh,
|
||||
child: const CustomScrollView(
|
||||
slivers: [
|
||||
TrackViewFlexHeader(),
|
||||
SliverAnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 500),
|
||||
child: TrackViewBodySection(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ class PaginationProps {
|
||||
final bool hasNextPage;
|
||||
final bool isLoading;
|
||||
final VoidCallback onFetchMore;
|
||||
final Future<void> Function() onRefresh;
|
||||
final Future<List<Track>> Function() onFetchAll;
|
||||
|
||||
const PaginationProps({
|
||||
@ -13,6 +14,7 @@ class PaginationProps {
|
||||
required this.isLoading,
|
||||
required this.onFetchMore,
|
||||
required this.onFetchAll,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
factory PaginationProps.fromQuery(
|
||||
@ -24,6 +26,7 @@ class PaginationProps {
|
||||
isLoading: query.isLoadingNextPage,
|
||||
onFetchMore: query.fetchNext,
|
||||
onFetchAll: onFetchAll,
|
||||
onRefresh: query.refreshAll,
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,7 +36,8 @@ class PaginationProps {
|
||||
other.hasNextPage == hasNextPage &&
|
||||
other.isLoading == isLoading &&
|
||||
other.onFetchMore == onFetchMore &&
|
||||
other.onFetchAll == onFetchAll;
|
||||
other.onFetchAll == onFetchAll &&
|
||||
other.onRefresh == onRefresh;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -42,7 +46,8 @@ class PaginationProps {
|
||||
hasNextPage.hashCode ^
|
||||
isLoading.hashCode ^
|
||||
onFetchMore.hashCode ^
|
||||
onFetchAll.hashCode;
|
||||
onFetchAll.hashCode ^
|
||||
onRefresh.hashCode;
|
||||
}
|
||||
|
||||
class InheritedTrackView extends InheritedWidget {
|
||||
|
||||
32
lib/hooks/configurators/use_close_behavior.dart
Normal file
32
lib/hooks/configurators/use_close_behavior.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/hooks/configurators/use_window_listener.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
|
||||
final closeNotification = DesktopTools.createNotification(
|
||||
title: 'Spotube',
|
||||
message: 'Running in background. Minimized to System Tray',
|
||||
actions: [
|
||||
LocalNotificationAction(text: 'Close The App'),
|
||||
],
|
||||
)?..onClickAction = (value) {
|
||||
exit(0);
|
||||
};
|
||||
|
||||
void useCloseBehavior(WidgetRef ref) {
|
||||
useWindowListener(
|
||||
onWindowClose: () async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
if (preferences.closeBehavior == CloseBehavior.minimizeToTray) {
|
||||
await DesktopTools.window.hide();
|
||||
closeNotification?.show();
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
100
lib/hooks/configurators/use_deep_linking.dart
Normal file
100
lib/hooks/configurators/use_deep_linking.dart
Normal file
@ -0,0 +1,100 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
void useDeepLinking(WidgetRef ref) {
|
||||
// single instance no worries
|
||||
final appLinks = AppLinks();
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
useEffect(() {
|
||||
void uriListener(List<SharedFile> files) async {
|
||||
for (final file in files) {
|
||||
if (file.type != SharedMediaType.URL) continue;
|
||||
final url = Uri.parse(file.value!);
|
||||
if (url.pathSegments.length != 2) continue;
|
||||
|
||||
switch (url.pathSegments.first) {
|
||||
case "album":
|
||||
router.push(
|
||||
"/album/${url.pathSegments.last}",
|
||||
extra: await queryClient.fetchQuery<Album, dynamic>(
|
||||
"album/${url.pathSegments.last}",
|
||||
() => spotify.albums.get(url.pathSegments.last),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "artist":
|
||||
router.push("/artist/${url.pathSegments.last}");
|
||||
break;
|
||||
case "playlist":
|
||||
router.push(
|
||||
"/playlist/${url.pathSegments.last}",
|
||||
extra: await queryClient.fetchQuery<Playlist, dynamic>(
|
||||
"playlist/${url.pathSegments.last}",
|
||||
() => spotify.playlists.get(url.pathSegments.last),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription? mediaStream;
|
||||
|
||||
if (DesktopTools.platform.isMobile) {
|
||||
FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
|
||||
|
||||
mediaStream =
|
||||
FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
|
||||
}
|
||||
|
||||
final subscription = appLinks.allStringLinkStream.listen((uri) async {
|
||||
final startSegment = uri.split(":").take(2).join(":");
|
||||
final endSegment = uri.split(":").last;
|
||||
|
||||
switch (startSegment) {
|
||||
case "spotify:album":
|
||||
await router.push(
|
||||
"/album/$endSegment",
|
||||
extra: await queryClient.fetchQuery<Album, dynamic>(
|
||||
"album/$endSegment",
|
||||
() => spotify.albums.get(endSegment),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "spotify:artist":
|
||||
await router.push("/artist/$endSegment");
|
||||
break;
|
||||
case "spotify:playlist":
|
||||
await router.push(
|
||||
"/playlist/$endSegment",
|
||||
extra: await queryClient.fetchQuery<Playlist, dynamic>(
|
||||
"playlist/$endSegment",
|
||||
() => spotify.playlists.get(endSegment),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return () {
|
||||
mediaStream?.cancel();
|
||||
subscription.cancel();
|
||||
};
|
||||
}, [spotify, queryClient]);
|
||||
}
|
||||
197
lib/hooks/configurators/use_window_listener.dart
Normal file
197
lib/hooks/configurators/use_window_listener.dart
Normal file
@ -0,0 +1,197 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
class CallbackWindowListener implements WindowListener {
|
||||
final VoidCallback? _onWindowClose;
|
||||
final VoidCallback? _onWindowFocus;
|
||||
final VoidCallback? _onWindowBlur;
|
||||
final VoidCallback? _onWindowMaximize;
|
||||
final VoidCallback? _onWindowUnmaximize;
|
||||
final VoidCallback? _onWindowMinimize;
|
||||
final VoidCallback? _onWindowRestore;
|
||||
final VoidCallback? _onWindowResize;
|
||||
final VoidCallback? _onWindowResized;
|
||||
final VoidCallback? _onWindowMove;
|
||||
final VoidCallback? _onWindowMoved;
|
||||
final VoidCallback? _onWindowEnterFullScreen;
|
||||
final VoidCallback? _onWindowLeaveFullScreen;
|
||||
final VoidCallback? _onWindowDocked;
|
||||
final VoidCallback? _onWindowUndocked;
|
||||
final VoidCallback? _onWindowEvent;
|
||||
|
||||
const CallbackWindowListener({
|
||||
VoidCallback? onWindowClose,
|
||||
VoidCallback? onWindowFocus,
|
||||
VoidCallback? onWindowBlur,
|
||||
VoidCallback? onWindowMaximize,
|
||||
VoidCallback? onWindowUnmaximize,
|
||||
VoidCallback? onWindowMinimize,
|
||||
VoidCallback? onWindowRestore,
|
||||
VoidCallback? onWindowResize,
|
||||
VoidCallback? onWindowResized,
|
||||
VoidCallback? onWindowMove,
|
||||
VoidCallback? onWindowMoved,
|
||||
VoidCallback? onWindowEnterFullScreen,
|
||||
VoidCallback? onWindowLeaveFullScreen,
|
||||
VoidCallback? onWindowDocked,
|
||||
VoidCallback? onWindowUndocked,
|
||||
VoidCallback? onWindowEvent,
|
||||
}) : _onWindowClose = onWindowClose,
|
||||
_onWindowFocus = onWindowFocus,
|
||||
_onWindowBlur = onWindowBlur,
|
||||
_onWindowMaximize = onWindowMaximize,
|
||||
_onWindowUnmaximize = onWindowUnmaximize,
|
||||
_onWindowMinimize = onWindowMinimize,
|
||||
_onWindowRestore = onWindowRestore,
|
||||
_onWindowResize = onWindowResize,
|
||||
_onWindowResized = onWindowResized,
|
||||
_onWindowMove = onWindowMove,
|
||||
_onWindowMoved = onWindowMoved,
|
||||
_onWindowEnterFullScreen = onWindowEnterFullScreen,
|
||||
_onWindowLeaveFullScreen = onWindowLeaveFullScreen,
|
||||
_onWindowDocked = onWindowDocked,
|
||||
_onWindowUndocked = onWindowUndocked,
|
||||
_onWindowEvent = onWindowEvent;
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
return _onWindowBlur?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
return _onWindowClose?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowDocked() {
|
||||
return _onWindowDocked?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() {
|
||||
return _onWindowEnterFullScreen?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
return _onWindowEvent?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
return _onWindowFocus?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
return _onWindowLeaveFullScreen?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
return _onWindowMaximize?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
return _onWindowMinimize?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMove() {
|
||||
return _onWindowMove?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMoved() {
|
||||
return _onWindowMoved?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
return _onWindowResize?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResized() {
|
||||
return _onWindowResized?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
return _onWindowRestore?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUndocked() {
|
||||
return _onWindowUndocked?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUnmaximize() {
|
||||
return _onWindowUnmaximize?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void useWindowListener({
|
||||
VoidCallback? onWindowClose,
|
||||
VoidCallback? onWindowFocus,
|
||||
VoidCallback? onWindowBlur,
|
||||
VoidCallback? onWindowMaximize,
|
||||
VoidCallback? onWindowUnmaximize,
|
||||
VoidCallback? onWindowMinimize,
|
||||
VoidCallback? onWindowRestore,
|
||||
VoidCallback? onWindowResize,
|
||||
VoidCallback? onWindowResized,
|
||||
VoidCallback? onWindowMove,
|
||||
VoidCallback? onWindowMoved,
|
||||
VoidCallback? onWindowEnterFullScreen,
|
||||
VoidCallback? onWindowLeaveFullScreen,
|
||||
VoidCallback? onWindowDocked,
|
||||
VoidCallback? onWindowUndocked,
|
||||
VoidCallback? onWindowEvent,
|
||||
}) {
|
||||
useEffect(() {
|
||||
final listener = CallbackWindowListener(
|
||||
onWindowClose: onWindowClose,
|
||||
onWindowFocus: onWindowFocus,
|
||||
onWindowBlur: onWindowBlur,
|
||||
onWindowMaximize: onWindowMaximize,
|
||||
onWindowUnmaximize: onWindowUnmaximize,
|
||||
onWindowMinimize: onWindowMinimize,
|
||||
onWindowRestore: onWindowRestore,
|
||||
onWindowResize: onWindowResize,
|
||||
onWindowResized: onWindowResized,
|
||||
onWindowMove: onWindowMove,
|
||||
onWindowMoved: onWindowMoved,
|
||||
onWindowEnterFullScreen: onWindowEnterFullScreen,
|
||||
onWindowLeaveFullScreen: onWindowLeaveFullScreen,
|
||||
onWindowDocked: onWindowDocked,
|
||||
onWindowUndocked: onWindowUndocked,
|
||||
onWindowEvent: onWindowEvent,
|
||||
);
|
||||
DesktopTools.window.addListener(listener);
|
||||
return () {
|
||||
DesktopTools.window.removeListener(listener);
|
||||
};
|
||||
}, [
|
||||
onWindowClose,
|
||||
onWindowFocus,
|
||||
onWindowBlur,
|
||||
onWindowMaximize,
|
||||
onWindowUnmaximize,
|
||||
onWindowMinimize,
|
||||
onWindowRestore,
|
||||
onWindowResize,
|
||||
onWindowResized,
|
||||
onWindowMove,
|
||||
onWindowMoved,
|
||||
onWindowEnterFullScreen,
|
||||
onWindowLeaveFullScreen,
|
||||
onWindowDocked,
|
||||
onWindowUndocked,
|
||||
onWindowEvent,
|
||||
]);
|
||||
}
|
||||
@ -251,7 +251,7 @@
|
||||
"developers": "المطورون",
|
||||
"not_logged_in": "لم تقم بتسجيل الدخول",
|
||||
"search_mode": "وضع البحث",
|
||||
"youtube_api_type": "نوع الـAPI",
|
||||
"audio_source": "مصدر الصوت",
|
||||
"ok": "حسسناً",
|
||||
"failed_to_encrypt": "فشل في التشفير",
|
||||
"encryption_failed_warning": "يستخدم Spotube التشفير لتخزين بياناتك بشكل آمن. لكنها فشلت في القيام بذلك. لذلك سيعود الأمر إلى التخزين غير الآمن\nإذا كنت تستخدم Linux، فيرجى التأكد من تثبيت أي خدمة سرية (gnome-keyring، kde-wallet، keepassxc، إلخ)",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "كلمة المرور",
|
||||
"login": "تسجيل الدخول",
|
||||
"login_with_your_lastfm": "تسجيل الدخول باستخدام حساب Last.fm الخاص بك",
|
||||
"scrobble_to_lastfm": "تسجيل الاستماع على Last.fm"
|
||||
"scrobble_to_lastfm": "تسجيل الاستماع على Last.fm",
|
||||
"go_to_album": "الانتقال إلى الألبوم",
|
||||
"discord_rich_presence": "وجود ديسكورد الغني",
|
||||
"browse_all": "تصفح الكل",
|
||||
"genres": "الأنواع الموسيقية",
|
||||
"explore_genres": "استكشاف الأنواع"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "ডেভেলপার",
|
||||
"not_logged_in": "আপনি লগইন করা নেই",
|
||||
"search_mode": "অনুসন্ধান মোড",
|
||||
"youtube_api_type": "API প্রকার",
|
||||
"audio_source": "অডিও উৎস",
|
||||
"ok": "ঠিক আছে",
|
||||
"failed_to_encrypt": "এনক্রিপ্ট করা ব্যর্থ হয়েছে",
|
||||
"encryption_failed_warning": "Spotube আপনার তথ্যগুলি নিরাপদভাবে স্টোর করতে এনক্রিপশন ব্যবহার করে। কিন্তু এটি ব্যর্থ হয়েছে। তাই এটি অনিরাপদ স্টোরে ফলফল হবে\nযদি আপনি Linux ব্যবহার করেন, তবে দয়া করে নিশ্চিত হউন যে আপনার কোনও সিক্রেট-সার্ভিস gnome-keyring, kde-wallet, keepassxc ইত্যাদি ইনস্টল করা আছে",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "পাসওয়ার্ড",
|
||||
"login": "লগইন",
|
||||
"login_with_your_lastfm": "আপনার Last.fm অ্যাকাউন্ট দিয়ে লগইন করুন",
|
||||
"scrobble_to_lastfm": "Last.fm এ স্ক্রবল করুন"
|
||||
"scrobble_to_lastfm": "Last.fm এ স্ক্রবল করুন",
|
||||
"go_to_album": "الانتقال إلى الألبوم",
|
||||
"discord_rich_presence": "وجود ديسكورد الغني",
|
||||
"browse_all": "تصفح الكل",
|
||||
"genres": "الأنواع الموسيقية",
|
||||
"explore_genres": "استكشاف الأنواع"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Desenvolupadors",
|
||||
"not_logged_in": "No ha iniciat sesió",
|
||||
"search_mode": "Mode de cerca",
|
||||
"youtube_api_type": "Tipus d'API de YouTube",
|
||||
"audio_source": "Font d'àudio",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Error al xifrar",
|
||||
"encryption_failed_warning": "Spotube utilitza el xifrado per emmagatzemar les seves dades de forma segura. Però ha fallat. Per tant, tornarà a un emmagatzament no segur\nSi estè utilizant Linux, asseguri's de tenir instal·lats els serveis secrets com gnome-keyring, kde-wallet i keepassxc",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Contrasenya",
|
||||
"login": "Inicia la sessió",
|
||||
"login_with_your_lastfm": "Inicia la sessió amb el teu compte de Last.fm",
|
||||
"scrobble_to_lastfm": "Scrobble a Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobble a Last.fm",
|
||||
"go_to_album": "Anar a l'àlbum",
|
||||
"discord_rich_presence": "Presència rica de Discord",
|
||||
"browse_all": "Navega per tot",
|
||||
"genres": "Gèneres",
|
||||
"explore_genres": "Explora els gèneres"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Entwickler",
|
||||
"not_logged_in": "Sie sind nicht angemeldet",
|
||||
"search_mode": "Suchmodus",
|
||||
"youtube_api_type": "API-Typ",
|
||||
"audio_source": "Audioquelle",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Verschlüsselung fehlgeschlagen",
|
||||
"encryption_failed_warning": "Spotube verwendet Verschlüsselung, um Ihre Daten sicher zu speichern. Dies ist jedoch fehlgeschlagen. Daher wird es auf unsichere Speicherung zurückgreifen\nWenn Sie Linux verwenden, stellen Sie bitte sicher, dass Sie Secret-Services wie gnome-keyring, kde-wallet und keepassxc installiert haben",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Passwort",
|
||||
"login": "Anmelden",
|
||||
"login_with_your_lastfm": "Mit Ihrem Last.fm-Konto anmelden",
|
||||
"scrobble_to_lastfm": "Auf Last.fm scrobbeln"
|
||||
"scrobble_to_lastfm": "Auf Last.fm scrobbeln",
|
||||
"go_to_album": "Zum Album gehen",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Alles durchsuchen",
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Genres erkunden"
|
||||
}
|
||||
@ -251,7 +251,7 @@
|
||||
"developers": "Developers",
|
||||
"not_logged_in": "You're not logged in",
|
||||
"search_mode": "Search Mode",
|
||||
"youtube_api_type": "API Type",
|
||||
"audio_source": "Audio Source",
|
||||
"ok": "Ok",
|
||||
"failed_to_encrypt": "Failed to encrypt",
|
||||
"encryption_failed_warning": "Spotube uses encryption to securely store your data. But failed to do so. So it'll fallback to insecure storage\nIf you're using linux, please make sure you've any secret-service (gnome-keyring, kde-wallet, keepassxc etc) installed",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Password",
|
||||
"login": "Login",
|
||||
"login_with_your_lastfm": "Login with your Last.fm account",
|
||||
"scrobble_to_lastfm": "Scrobble to Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobble to Last.fm",
|
||||
"go_to_album": "Go to Album",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Browse All",
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Explore Genres"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Desarrolladores",
|
||||
"not_logged_in": "No has iniciado sesión",
|
||||
"search_mode": "Modo de búsqueda",
|
||||
"youtube_api_type": "Tipo de API de YouTube",
|
||||
"audio_source": "Fuente de audio",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Error al cifrar",
|
||||
"encryption_failed_warning": "Spotube utiliza el cifrado para almacenar sus datos de forma segura. Pero ha fallado. Por lo tanto, volverá a un almacenamiento no seguro\nSi está utilizando Linux, asegúrese de tener instalados servicios secretos como gnome-keyring, kde-wallet y keepassxc",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Contraseña",
|
||||
"login": "Iniciar sesión",
|
||||
"login_with_your_lastfm": "Iniciar sesión con tu cuenta de Last.fm",
|
||||
"scrobble_to_lastfm": "Scrobble a Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobble a Last.fm",
|
||||
"go_to_album": "Ir al álbum",
|
||||
"discord_rich_presence": "Presencia rica en Discord",
|
||||
"browse_all": "Explorar todo",
|
||||
"genres": "Géneros",
|
||||
"explore_genres": "Explorar géneros"
|
||||
}
|
||||
@ -251,7 +251,7 @@
|
||||
"developers": "توسعه دهنده ها",
|
||||
"not_logged_in": "شما وارد نشده اید ",
|
||||
"search_mode": "حالت جستجو",
|
||||
"youtube_api_type": "API نوع",
|
||||
"audio_source": "منبع صدا",
|
||||
"ok": "باشد",
|
||||
"failed_to_encrypt": "رمز گذاری نشده",
|
||||
"encryption_failed_warning": "Spotube از رمزگذاری برای ذخیره ایمن داده های شما استفاده می کند. اما موفق به انجام این کار نشد. بنابراین به فضای ذخیرهسازی ناامن تبدیل میشود\nاگر از لینوکس استفاده میکنید، لطفاً مطمئن شوید که سرویس مخفی (gnome-keyring، kde-wallet، keepassxc و غیره) را نصب کردهاید.",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "رمز عبور",
|
||||
"login": "ورود",
|
||||
"login_with_your_lastfm": "ورود با حساب کاربری Last.fm خود",
|
||||
"scrobble_to_lastfm": "Scrobble به Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobble به Last.fm",
|
||||
"go_to_album": "رفتن به آلبوم",
|
||||
"discord_rich_presence": "حضور غنی دیسکورد",
|
||||
"browse_all": "مرور همه",
|
||||
"genres": "ژانرها",
|
||||
"explore_genres": "استکشاف ژانرها"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Développeurs",
|
||||
"not_logged_in": "Vous n'êtes pas connecté(e)",
|
||||
"search_mode": "Mode de recherche",
|
||||
"youtube_api_type": "Type d'API",
|
||||
"audio_source": "Source audio",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Échec de la cryptage",
|
||||
"encryption_failed_warning": "Spotube utilise le cryptage pour stocker vos données en toute sécurité. Mais cela a échoué. Il basculera donc vers un stockage non sécurisé\nSi vous utilisez Linux, assurez-vous d'avoir installé des services secrets tels que gnome-keyring, kde-wallet et keepassxc",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Mot de passe",
|
||||
"login": "Se connecter",
|
||||
"login_with_your_lastfm": "Se connecter avec votre compte Last.fm",
|
||||
"scrobble_to_lastfm": "Scrobble à Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobble à Last.fm",
|
||||
"go_to_album": "Aller à l'album",
|
||||
"discord_rich_presence": "Présence riche de Discord",
|
||||
"browse_all": "Parcourir tout",
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Explorer les genres"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "डेवलपर्स",
|
||||
"not_logged_in": "आप लॉग इन नहीं हैं",
|
||||
"search_mode": "खोज मोड",
|
||||
"youtube_api_type": "API प्रकार",
|
||||
"audio_source": "ऑडियो स्रोत",
|
||||
"ok": "ठीक है",
|
||||
"failed_to_encrypt": "एन्क्रिप्ट करने में विफल रहा",
|
||||
"encryption_failed_warning": "Spotube आपके डेटा को सुरक्षित रूप से स्टोर करने के लिए एन्क्रिप्शन का उपयोग करता है। लेकिन इसमें विफल रहा। इसलिए, यह असुरक्षित स्टोरेज पर फॉलबैक करेगा\nयदि आप Linux का उपयोग कर रहे हैं, तो कृपया सुनिश्चित करें कि आपके पास gnome-keyring, kde-wallet, keepassxc आदि जैसी कोई सीक्रेट-सर्विस इंस्टॉल की गई है",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "पासवर्ड",
|
||||
"login": "लॉग इन करें",
|
||||
"login_with_your_lastfm": "अपने Last.fm अकाउंट से लॉगिन करें",
|
||||
"scrobble_to_lastfm": "Last.fm पर स्क्रॉबल करें"
|
||||
"scrobble_to_lastfm": "Last.fm पर स्क्रॉबल करें",
|
||||
"go_to_album": "एल्बम पर जाएं",
|
||||
"discord_rich_presence": "डिस्कॉर्ड रिच प्रेजेंस",
|
||||
"browse_all": "सभी को ब्राउज़ करें",
|
||||
"genres": "शैलियाँ",
|
||||
"explore_genres": "शैलियों का अन्वेषण करें"
|
||||
}
|
||||
@ -279,5 +279,11 @@
|
||||
"password": "Password",
|
||||
"login": "Accesso",
|
||||
"login_with_your_lastfm": "Accedi con il tuo account Last.fm",
|
||||
"scrobble_to_lastfm": "Invia a Last.fm"
|
||||
}
|
||||
"scrobble_to_lastfm": "Invia a Last.fm",
|
||||
"audio_source": "Fonte audio",
|
||||
"go_to_album": "Vai all'album",
|
||||
"discord_rich_presence": "Presenza ricca di Discord",
|
||||
"browse_all": "Esplora tutto",
|
||||
"genres": "Generi",
|
||||
"explore_genres": "Esplora generi"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "開発",
|
||||
"not_logged_in": "ログインしていません",
|
||||
"search_mode": "検索モード",
|
||||
"youtube_api_type": "APIの種類",
|
||||
"audio_source": "音声ソース",
|
||||
"ok": "分かりました",
|
||||
"failed_to_encrypt": "暗号化に失敗しました",
|
||||
"encryption_failed_warning": "Spotubeはデータを安全に保存するために暗号化を使用しています。しかし、失敗しました。したがって、安全でないストレージにフォールバックします\nLinuxを使用している場合は、gnome-keyring、kde-wallet、keepassxcなどのシークレットサービスがインストールされていることを確認してください",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "パスワード",
|
||||
"login": "ログインする",
|
||||
"login_with_your_lastfm": "あなたのLast.fmアカウントでログインする",
|
||||
"scrobble_to_lastfm": "Last.fmにスクロブルする"
|
||||
"scrobble_to_lastfm": "Last.fmにスクロブルする",
|
||||
"go_to_album": "アルバムに移動",
|
||||
"discord_rich_presence": "ディスコードリッチプレゼンス",
|
||||
"browse_all": "すべてを閲覧",
|
||||
"genres": "ジャンル",
|
||||
"explore_genres": "ジャンルを探索"
|
||||
}
|
||||
289
lib/l10n/app_nl.arb
Normal file
289
lib/l10n/app_nl.arb
Normal file
@ -0,0 +1,289 @@
|
||||
{
|
||||
"guest": "Gast",
|
||||
"browse": "Bladeren",
|
||||
"search": "Zoek op",
|
||||
"library": "Bibliotheek",
|
||||
"lyrics": "Liedteksten",
|
||||
"settings": "Instellingen",
|
||||
"genre_categories_filter": "Categorieën of genres filteren...",
|
||||
"genre": "Genre",
|
||||
"personalized": "Gepersonaliseerd",
|
||||
"featured": "Aanbevolen",
|
||||
"new_releases": "Nieuwe uitgaves",
|
||||
"songs": "Liedjes",
|
||||
"playing_track": "{track} afspelen",
|
||||
"queue_clear_alert": "Dit zal de huidige wachtrij wissen. {track_length} tracks worden verwijderd\nWilt u doorgaan?",
|
||||
"load_more": "Meer laden",
|
||||
"playlists": "Afspeellijsten",
|
||||
"artists": "Kunstenaars",
|
||||
"albums": "Albums",
|
||||
"tracks": "Nummers",
|
||||
"downloads": "Downloads",
|
||||
"filter_playlists": "Filter uw afspeellijsten...",
|
||||
"liked_tracks": "Geliefde tracks",
|
||||
"liked_tracks_description": "Al je favoriete nummers",
|
||||
"create_playlist": "Afspeellijst maken",
|
||||
"create_a_playlist": "Een afspeellijst maken",
|
||||
"update_playlist": "Afspeellijst bijwerken",
|
||||
"create": "Maak",
|
||||
"cancel": "Annuleren",
|
||||
"update": "Bijwerken",
|
||||
"playlist_name": "Afspeellijstnaam",
|
||||
"name_of_playlist": "Naam van de afspeellijst",
|
||||
"description": "Beschrijving",
|
||||
"public": "Openbaar",
|
||||
"collaborative": "Samenwerkend",
|
||||
"search_local_tracks": "Lokale nummers zoeken...",
|
||||
"play": "Speel",
|
||||
"delete": "Wissen",
|
||||
"none": "Geen",
|
||||
"sort_a_z": "Sorteren op A-Z",
|
||||
"sort_z_a": "Sorteren op Z-A",
|
||||
"sort_artist": "Sorteren op kunstenaar",
|
||||
"sort_album": "Sorteren op album",
|
||||
"sort_tracks": "Nummers sorteren",
|
||||
"currently_downloading": "Momenteel aan het downloaden ({tracks_length})",
|
||||
"cancel_all": "Alle annuleren",
|
||||
"filter_artist": "Kunstenaars filteren...",
|
||||
"followers": "{followers} volgers",
|
||||
"add_artist_to_blacklist": "Kunstenaar toevoegen aan zwarte lijst",
|
||||
"top_tracks": "Topsporen",
|
||||
"fans_also_like": "Liefhebbers willen ook",
|
||||
"loading": "Aan het laden...",
|
||||
"artist": "Kunstenaar",
|
||||
"blacklisted": "Op de zwarte lijst",
|
||||
"following": "Op volg",
|
||||
"follow": "Volgen",
|
||||
"artist_url_copied": "URL artiest gekopieerd naar klembord",
|
||||
"added_to_queue": "{tracks} tracks toegevoegd aan wachtrij",
|
||||
"filter_albums": "Albums filteren...",
|
||||
"synced": "Gesynchroniseerd",
|
||||
"plain": "Eenvoudig",
|
||||
"shuffle": "Schuifelen",
|
||||
"search_tracks": "Zoek nummers...",
|
||||
"released": "Vrijgegeven",
|
||||
"error": "Fout {error}",
|
||||
"title": "Titel",
|
||||
"time": "Tijd",
|
||||
"more_actions": "Meer acties",
|
||||
"download_count": "({count}) downloads",
|
||||
"add_count_to_playlist": "Voeg ({count}) toe aan afspeellijst",
|
||||
"add_count_to_queue": "Voeg ({count}) toe aan wachtrij",
|
||||
"play_count_next": "Speel ({count}) volgende",
|
||||
"album": "Album",
|
||||
"copied_to_clipboard": "{data} naar klembord gekopieerd",
|
||||
"add_to_following_playlists": "Voeg {track} toe aan volgende afspeellijsten",
|
||||
"add": "Toevoegen",
|
||||
"added_track_to_queue": "{track} toegevoegd aan wachtrij",
|
||||
"add_to_queue": "Toevoegen aan wachtrij",
|
||||
"track_will_play_next": "{track} zal hierna spelen",
|
||||
"play_next": "Volgende afspelen",
|
||||
"removed_track_from_queue": "{track} uit wachtrij verwijderd",
|
||||
"remove_from_queue": "Verwijderen uit wachtrij",
|
||||
"remove_from_favorites": "Verwijderen uit favorieten",
|
||||
"save_as_favorite": "Opslaan als favoriet",
|
||||
"add_to_playlist": "Toevoegen aan afspeellijst",
|
||||
"remove_from_playlist": "Verwijderen uit afspeellijst",
|
||||
"add_to_blacklist": "Toevoegen aan zwarte lijst",
|
||||
"remove_from_blacklist": "Verwijderen uit zwarte lijst",
|
||||
"share": "Delen",
|
||||
"mini_player": "Minispeler",
|
||||
"slide_to_seek": "Schuif om vooruit of achteruit te zoeken",
|
||||
"shuffle_playlist": "Afspeellijst schuifelen",
|
||||
"unshuffle_playlist": "Afspeellijst onschuifelen",
|
||||
"previous_track": "Vorige nummer",
|
||||
"next_track": "Volgende nummer",
|
||||
"pause_playback": "Weergave pauzeren",
|
||||
"resume_playback": "Weergave hervatten",
|
||||
"loop_track": "Nummer loopen",
|
||||
"repeat_playlist": "Afspeellijst herhalen",
|
||||
"queue": "Wachtrij",
|
||||
"alternative_track_sources": "Alternatieve nummerbronnen",
|
||||
"download_track": "Nummer downloaden",
|
||||
"tracks_in_queue": "{tracks} tracks in wachtrij",
|
||||
"clear_all": "Wis alles",
|
||||
"show_hide_ui_on_hover": "UI tonen/verbergen bij zweven",
|
||||
"always_on_top": "Altijd bovenaan",
|
||||
"exit_mini_player": "Minispeler afsluiten",
|
||||
"download_location": "Downloadlocatie",
|
||||
"account": "Account",
|
||||
"login_with_spotify": "Inloggen met je Spotify-account",
|
||||
"connect_with_spotify": "Verbinden met Spotify",
|
||||
"logout": "Afmelden",
|
||||
"logout_of_this_account": "Afmelden van dit account",
|
||||
"language_region": "Taal & Regio",
|
||||
"language": "Taal",
|
||||
"system_default": "Systeemstandaard",
|
||||
"market_place_region": "Marktplaats-regio",
|
||||
"recommendation_country": "Aanbeveling Land",
|
||||
"appearance": "Uiterlijk",
|
||||
"layout_mode": "Opmaakmodus",
|
||||
"override_layout_settings": "Instellingen voor responsieve opmaakmodus opheffen",
|
||||
"adaptive": "Aanpassingsgericht",
|
||||
"compact": "Compact",
|
||||
"extended": "Uitgebreide",
|
||||
"theme": "Thema",
|
||||
"dark": "Donker",
|
||||
"light": "Licht",
|
||||
"system": "Systeem",
|
||||
"accent_color": "Accentkleur",
|
||||
"sync_album_color": "Albumkleur synchroniseren",
|
||||
"sync_album_color_description": "Gebruikt de overheersende kleur van het albumartikel als accentkleur",
|
||||
"playback": "Weergave",
|
||||
"audio_quality": "Audiokwaliteit",
|
||||
"high": "Hoog",
|
||||
"low": "Laag",
|
||||
"pre_download_play": "Vooraf downloaden en spelen",
|
||||
"pre_download_play_description": "In plaats van audio te streamen, kun je bytes downloaden en afspelen (aanbevolen voor gebruikers met een hogere bandbreedte)",
|
||||
"skip_non_music": "Niet-muzieksegmenten overslaan (SponsorBlock)",
|
||||
"blacklist_description": "Nummers en artiesten op de zwarte lijst",
|
||||
"wait_for_download_to_finish": "Wacht tot de huidige download is voltooid",
|
||||
"desktop": "Bureaublad",
|
||||
"close_behavior": "Sluitgedrag",
|
||||
"close": "Sluit af",
|
||||
"minimize_to_tray": "Minimaliseren naar lade",
|
||||
"show_tray_icon": "Systeemvakpictogram tonen",
|
||||
"about": "Over",
|
||||
"u_love_spotube": "We weten dat jullie van Spotube houden",
|
||||
"check_for_updates": "Controleren op updates",
|
||||
"about_spotube": "Over Spotube",
|
||||
"blacklist": "Zwarte lijst",
|
||||
"please_sponsor": "Sponsor/Doneer a.u.b.",
|
||||
"spotube_description": "Spotube, een lichtgewicht, cross-platform, vrij-voor-alles Spotify-client",
|
||||
"version": "Versie",
|
||||
"build_number": "Beeldnummer",
|
||||
"founder": "Stichter",
|
||||
"repository": "Opslagplaats",
|
||||
"bug_issues": "Bug+problemen",
|
||||
"made_with": "Gemaakt met ❤️ in Bangladesh🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Licentie",
|
||||
"add_spotify_credentials": "Voeg je spotify-referenties toe om te beginnen",
|
||||
"credentials_will_not_be_shared_disclaimer": "Maakt u geen zorgen, uw gegevens worden niet verzameld of gedeeld met anderen.",
|
||||
"know_how_to_login": "Weet u niet hoe u dit moet doen?",
|
||||
"follow_step_by_step_guide": "Volg de stap voor stap gids",
|
||||
"spotify_cookie": "Spotify {name} Cookie",
|
||||
"cookie_name_cookie": "{name} Cookie",
|
||||
"fill_in_all_fields": "Vul alle velden in a.u.b.",
|
||||
"submit": "Verzenden",
|
||||
"exit": "Ga weg",
|
||||
"previous": "Vorige",
|
||||
"next": "Volgende",
|
||||
"done": "Klaar",
|
||||
"step_1": "Stap 1",
|
||||
"first_go_to": "Ga eerst naar",
|
||||
"login_if_not_logged_in": "en Inloggen/Aanmelden als u niet bent ingelogd",
|
||||
"step_2": "Stap 2",
|
||||
"step_2_steps": "1. Zodra je bent aangemeld, druk je op F12 of klik je met de rechtermuisknop > Inspect om de Browser devtools te openen.\n2. Ga vervolgens naar het tabblad \"Toepassing\" (Chrome, Edge, Brave enz..) of naar het tabblad \"Opslag\" (Firefox, Palemoon enz..).\n3. Ga naar de sectie \"Cookies\" en vervolgens naar de subsectie \"https://accounts.spotify.com\".",
|
||||
"step_3": "Stap 3",
|
||||
"step_3_steps": "Kopieer de waarden van \"sp_dc\" en \"sp_key\" (of sp_gaid) Cookies",
|
||||
"success_emoji": "Succes🥳",
|
||||
"success_message": "Je bent nu succesvol ingelogd met je Spotify account. Goed gedaan, maat!",
|
||||
"step_4": "Stap 4",
|
||||
"step_4_steps": "Plak de gekopieerde \"sp_dc\" en \"sp_key\" (of sp_gaid) waarden in de respectievelijke velden",
|
||||
"something_went_wrong": "Er ging iets mis",
|
||||
"piped_instance": "Piped-serverinstantie",
|
||||
"piped_description": "De Piped-serverinstantie die moet worden gebruikt voor het matchen van sporen",
|
||||
"piped_warning": "Sommige werken misschien niet goed. Dus gebruik ze op eigen risico",
|
||||
"generate_playlist": "Afspeellijst genereren",
|
||||
"track_exists": "Nummer {track} bestaat al",
|
||||
"replace_downloaded_tracks": "Alle gedownloade nummers vervangen",
|
||||
"skip_download_tracks": "Downloaden van alle gedownloade nummers overslaan",
|
||||
"do_you_want_to_replace": "Wil je de bestaande nummer vervangen?",
|
||||
"replace": "Vervangen",
|
||||
"skip": "Overslaan",
|
||||
"select_up_to_count_type": "Selecteer tot {count} {type}",
|
||||
"select_genres": "Genres selecteren",
|
||||
"add_genres": "Genres toevoegen",
|
||||
"country": "Land",
|
||||
"number_of_tracks_generate": "Aantal nummers om te genereren",
|
||||
"acousticness": "Akoesticiteit",
|
||||
"danceability": "Dansbaarheid",
|
||||
"energy": "Energie",
|
||||
"instrumentalness": "Instrumentaliteit",
|
||||
"liveness": "Levendigheid",
|
||||
"loudness": "Luidheid",
|
||||
"speechiness": "Sprakeligheid",
|
||||
"valence": "Valentie",
|
||||
"popularity": "Populariteit",
|
||||
"key": "Sleutel",
|
||||
"duration": "Tijdsduur (s)",
|
||||
"tempo": "Tempo (SPM)",
|
||||
"mode": "Modus",
|
||||
"time_signature": "Tijdsnotatie",
|
||||
"short": "Kort",
|
||||
"medium": "Middel",
|
||||
"long": "Lang",
|
||||
"min": "Min",
|
||||
"max": "Max",
|
||||
"target": "Doel",
|
||||
"moderate": "Matig",
|
||||
"deselect_all": "Alles deselecteren",
|
||||
"select_all": "Alles selecteren",
|
||||
"are_you_sure": "Weet je het zeker?",
|
||||
"generating_playlist": "Je aangepaste afspeellijst genereren...",
|
||||
"selected_count_tracks": "{count} nummers geselecteerd",
|
||||
"download_warning": "Als je alle Tracks in bulk downloadt, ben je duidelijk bezig met muziekpiraterij en breng je schade toe aan de creatieve muziekmaatschappij. Ik hoop dat je je hiervan bewust bent. Probeer altijd het harde werk van artiesten te respecteren en te steunen.",
|
||||
"download_ip_ban_warning": "BTW, je IP-adres kan worden geblokkeerd op YouTube als gevolg van buitensporige downloadverzoeken dan normaal. IP blokkering betekent dat je YouTube niet kunt gebruiken (zelfs als je ingelogd bent) voor tenminste 2-3 maanden vanaf dat IP apparaat. Spotube is niet verantwoordelijk als dit ooit gebeurt.",
|
||||
"by_clicking_accept_terms": "Door op 'accepteren' te klikken ga je akkoord met de volgende voorwaarden:",
|
||||
"download_agreement_1": "Ik weet dat ik muziek illegaal verveel. Ik ben en crimineel.",
|
||||
"download_agreement_2": "Ik steun de kunstenaar waar ik kan en ik doe dit alleen omdat ik geen geld heb om hun kunst te kopen.",
|
||||
"download_agreement_3": "Ik ben me er volledig van bewust dat mijn IP geblokkeerd kan worden op YouTube & ik houd Spotube of zijn eigenaars/contributeurs niet verantwoordelijk voor ongelukken die veroorzaakt worden door mijn huidige actie.",
|
||||
"decline": "Weigeren",
|
||||
"accept": "Accepteren",
|
||||
"details": "Bijzonderheden",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Kanaal",
|
||||
"likes": "Liefs",
|
||||
"dislikes": "Hekels",
|
||||
"views": "Weergaven",
|
||||
"streamUrl": "Stream-URL",
|
||||
"stop": "Stoppen",
|
||||
"sort_newest": "Sorteren op nieuwste toegevoegd",
|
||||
"sort_oldest": "Sorteren op oudste toegevoegd",
|
||||
"sleep_timer": "Slaaptimer",
|
||||
"mins": "{minutes} minuten",
|
||||
"hours": "{hours} uren",
|
||||
"hour": "{hours} uur",
|
||||
"custom_hours": "Aangepaste uren",
|
||||
"logs": "Logboeken",
|
||||
"developers": "Ontwikkelaars",
|
||||
"not_logged_in": "U bent niet aangemeld",
|
||||
"search_mode": "Zoekmodus",
|
||||
"youtube_api_type": "API-type",
|
||||
"ok": "Oké",
|
||||
"failed_to_encrypt": "Versleuteling mislukt",
|
||||
"encryption_failed_warning": "Spotube gebruikt encryptie om je gegevens veilig op te slaan. Maar dat is niet gelukt. Dus zal het terugvallen op onveilige opslag.\nAls je linux gebruikt, zorg er dan voor dat je een geheim-dienst (gnome-keyring, kde-wallet, keepassxc etc) hebt geïnstalleerd.",
|
||||
"querying_info": "Info opvragen...",
|
||||
"piped_api_down": "Piped API is uit",
|
||||
"piped_down_error_instructions": "De Piped-instantie {pipedInstance} is momenteel uitgevallen\n\nVerander de instantie of verander het 'API-type' naar de officiële YouTube API.\n\nZorg ervoor dat u de app herstart na de wijziging",
|
||||
"you_are_offline": "U bent momenteel offline",
|
||||
"connection_restored": "Uw internetverbinding is hersteld",
|
||||
"use_system_title_bar": "Systeemtitelbalk gebruiken",
|
||||
"crunching_results": "Resultaten kraken...",
|
||||
"search_to_get_results": "Zoek om resultaten te krijgen",
|
||||
"use_amoled_mode": "Pikzwart donkerthema",
|
||||
"pitch_dark_theme": "AMOLED-modus",
|
||||
"normalize_audio": "Audio normaliseren",
|
||||
"change_cover": "Dekking wijzigen",
|
||||
"add_cover": "Dekking toevoegen",
|
||||
"restore_defaults": "Standaardwaarden herstellen",
|
||||
"download_music_codec": "Muziek-codec downloaden",
|
||||
"streaming_music_codec": "Muziek-codec streamen",
|
||||
"login_with_lastfm": "Aanmelden met Last.fm",
|
||||
"connect": "Verbinden",
|
||||
"disconnect_lastfm": "Last.fm verbreken",
|
||||
"disconnect": "Ontkoppelen",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
"login": "Inloggen",
|
||||
"login_with_your_lastfm": "Inloggen met uw Last.fm account",
|
||||
"scrobble_to_lastfm": "Scrobbel naar Last.fm",
|
||||
"audio_source": "Audiobron",
|
||||
"go_to_album": "Ga naar album",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Alles bekijken",
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Verken genres"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Developerzy",
|
||||
"not_logged_in": "Nie jesteś zalogowany",
|
||||
"search_mode": "Tryb szukania",
|
||||
"youtube_api_type": "Typ API",
|
||||
"audio_source": "Źródło dźwięku",
|
||||
"ok": "Ok",
|
||||
"failed_to_encrypt": "Nie można zaszyfrować :(",
|
||||
"encryption_failed_warning": "Spotube używa szyfrowania do bezpiecznego przechowywania danych. Ale nie udało się tego zrobić. Więc powróci do niezabezpieczonego przechowywania\nJeśli używasz Linuksa, upewnij się, że masz zainstalowane jakieś usługi do szyfrowania (gnome-keyring, kde-wallet, keepassxc itp.)",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Hasło",
|
||||
"login": "Zaloguj",
|
||||
"login_with_your_lastfm": "Zaloguj się na swoje konto Last.fm",
|
||||
"scrobble_to_lastfm": "Scrobbluj do Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobbluj do Last.fm",
|
||||
"go_to_album": "Przejdź do albumu",
|
||||
"discord_rich_presence": "Obecność na Discordzie",
|
||||
"browse_all": "Przeglądaj wszystko",
|
||||
"genres": "Gatunki muzyczne",
|
||||
"explore_genres": "Eksploruj gatunki"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Desenvolvedores",
|
||||
"not_logged_in": "Você não está logado",
|
||||
"search_mode": "Modo de Busca",
|
||||
"youtube_api_type": "Tipo de API",
|
||||
"audio_source": "Fonte de Áudio",
|
||||
"ok": "Ok",
|
||||
"failed_to_encrypt": "Falha ao criptografar",
|
||||
"encryption_failed_warning": "O Spotube usa criptografia para armazenar seus dados com segurança, mas falhou em fazê-lo. Portanto, ele voltará para o armazenamento não seguro.\nSe você estiver usando o Linux, certifique-se de ter algum serviço secreto (gnome-keyring, kde-wallet, keepassxc, etc.) instalado",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Palavra-passe",
|
||||
"login": "Iniciar sessão",
|
||||
"login_with_your_lastfm": "Inicie sessão na sua conta Last.fm",
|
||||
"scrobble_to_lastfm": "Scrobble para o Last.fm"
|
||||
"scrobble_to_lastfm": "Scrobble para o Last.fm",
|
||||
"go_to_album": "Ir para o álbum",
|
||||
"discord_rich_presence": "Presença rica no Discord",
|
||||
"browse_all": "Navegar por tudo",
|
||||
"genres": "Gêneros",
|
||||
"explore_genres": "Explorar gêneros"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "Разработчики",
|
||||
"not_logged_in": "Вы не выполнили вход",
|
||||
"search_mode": "Режим поиска",
|
||||
"youtube_api_type": "Тип API",
|
||||
"audio_source": "Источник аудио",
|
||||
"ok": "Ок",
|
||||
"failed_to_encrypt": "Не удалось зашифровать",
|
||||
"encryption_failed_warning": "Spotube использует шифрование для безопасного хранения ваших данных. Однако в этом случае произошла ошибка. Поэтому будет использовано небезопасное хранилище.\nЕсли вы используете Linux, убедитесь, что у вас установлен какой-либо инструмент для работы с секретами (gnome-keyring, kde-wallet, keepassxc и т.д.)",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Пароль",
|
||||
"login": "Войти",
|
||||
"login_with_your_lastfm": "Войти в свою учетную запись Last.fm",
|
||||
"scrobble_to_lastfm": "Скробблинг на Last.fm"
|
||||
"scrobble_to_lastfm": "Скробблинг на Last.fm",
|
||||
"go_to_album": "Перейти к альбому",
|
||||
"discord_rich_presence": "Богатое присутствие в Discord",
|
||||
"browse_all": "Просмотреть все",
|
||||
"genres": "Жанры",
|
||||
"explore_genres": "Исследовать жанры"
|
||||
}
|
||||
@ -251,7 +251,7 @@
|
||||
"developers": "Geliştiriciler",
|
||||
"not_logged_in": "Giriş yapmadınız",
|
||||
"search_mode": "Arama Modu",
|
||||
"youtube_api_type": "API Türü",
|
||||
"audio_source": "Ses Kaynağı",
|
||||
"ok": "Tamam",
|
||||
"failed_to_encrypt": "Şifreleme başarısız oldu",
|
||||
"encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde depolamak için şifreleme kullanır. Ancak bunu başaramadı. Bu nedenle güvensiz bir depolamaya geri dönecektir. Linux kullanıyorsanız, lütfen gnome-keyring, kde-wallet, keepassxc vb. gibi bir güvenlik hizmetinizin kurulu olduğundan emin olun.",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Şifre",
|
||||
"login": "Giriş Yap",
|
||||
"login_with_your_lastfm": "Last.fm hesabınız ile giriş yapın",
|
||||
"scrobble_to_lastfm": "Last.fm için Scrobble"
|
||||
"scrobble_to_lastfm": "Last.fm için Scrobble",
|
||||
"go_to_album": "Albüme Git",
|
||||
"discord_rich_presence": "Discord Zengin Varlık",
|
||||
"browse_all": "Tümünü Gözat",
|
||||
"genres": "Müzik Türleri",
|
||||
"explore_genres": "Türleri Keşfet"
|
||||
}
|
||||
@ -251,7 +251,7 @@
|
||||
"developers": "Розробники",
|
||||
"not_logged_in": "Ви не ввійшли в обліковий запис",
|
||||
"search_mode": "Режим пошуку",
|
||||
"youtube_api_type": "Тип API",
|
||||
"audio_source": "Джерело аудіо",
|
||||
"ok": "Гаразд",
|
||||
"failed_to_encrypt": "Не вдалося зашифрувати",
|
||||
"encryption_failed_warning": "Spotube використовує шифрування для безпечного зберігання ваших даних. Але не вдалося цього зробити. Тому він перейде до небезпечного зберігання\nЯкщо ви використовуєте Linux, переконайтеся, що у вас встановлено будь-який секретний сервіс (gnome-keyring, kde-wallet, keepassxc тощо)",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "Пароль",
|
||||
"login": "Увійти",
|
||||
"login_with_your_lastfm": "Увійти в свій обліковий запис Last.fm",
|
||||
"scrobble_to_lastfm": "Скробблінг на Last.fm"
|
||||
"scrobble_to_lastfm": "Скробблінг на Last.fm",
|
||||
"go_to_album": "Перейти до альбому",
|
||||
"discord_rich_presence": "Багата присутність у Discord",
|
||||
"browse_all": "Переглянути все",
|
||||
"genres": "Жанри",
|
||||
"explore_genres": "Досліджувати жанри"
|
||||
}
|
||||
@ -249,7 +249,7 @@
|
||||
"developers": "开发者",
|
||||
"not_logged_in": "你尚未登录",
|
||||
"search_mode": "搜索模式",
|
||||
"youtube_api_type": "API 类型",
|
||||
"audio_source": "音频源",
|
||||
"ok": "确定",
|
||||
"failed_to_encrypt": "加密失败",
|
||||
"encryption_failed_warning": "Spotube使用加密来安全地存储您的数据。但是失败了。因此,它将回退到不安全的存储\n如果您使用Linux,请确保已安装gnome-keyring、kde-wallet和keepassxc等秘密服务",
|
||||
@ -279,5 +279,10 @@
|
||||
"password": "密码",
|
||||
"login": "登录",
|
||||
"login_with_your_lastfm": "使用您的 Last.fm 帐户登录",
|
||||
"scrobble_to_lastfm": "在 Last.fm 上记录播放"
|
||||
"scrobble_to_lastfm": "在 Last.fm 上记录播放",
|
||||
"go_to_album": "前往专辑",
|
||||
"discord_rich_presence": "Discord 丰富展现",
|
||||
"browse_all": "浏览全部",
|
||||
"genres": "音乐类型",
|
||||
"explore_genres": "探索音乐类型"
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
/// yuri-val@github => Ukrainian
|
||||
/// energywave@github, ncvescera@github, OpenCode@github => Italian
|
||||
/// mdksec@github => Turkish
|
||||
/// SecularSteve@github => Dutch
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class L10n {
|
||||
@ -23,6 +24,7 @@ class L10n {
|
||||
const Locale('hi', 'IN'),
|
||||
const Locale('it', 'IT'),
|
||||
const Locale('ja', 'JP'),
|
||||
const Locale('nl', 'NL'),
|
||||
const Locale('pl', 'PL'),
|
||||
const Locale('pt', 'PT'),
|
||||
const Locale('ru', 'RU'),
|
||||
|
||||
@ -13,8 +13,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/collections/initializers.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/collections/intents.dart';
|
||||
import 'package:spotube/hooks/configurators/use_close_behavior.dart';
|
||||
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
||||
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
||||
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
||||
import 'package:spotube/l10n/l10n.dart';
|
||||
@ -40,6 +43,8 @@ Future<void> main(List<String> rawArgs) async {
|
||||
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await registerWindowsScheme("spotify");
|
||||
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
|
||||
MediaKit.ensureInitialized();
|
||||
@ -49,6 +54,10 @@ Future<void> main(List<String> rawArgs) async {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
}
|
||||
|
||||
if (DesktopTools.platform.isDesktop) {
|
||||
await DesktopTools.window.setPreventClose(true);
|
||||
}
|
||||
|
||||
await DesktopTools.ensureInitialized(
|
||||
DesktopWindowOptions(
|
||||
hideTitleBar: true,
|
||||
@ -176,7 +185,11 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
final paletteColor =
|
||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||
|
||||
useDisableBatteryOptimizations();
|
||||
useInitSysTray(ref);
|
||||
useDeepLinking(ref);
|
||||
useCloseBehavior(ref);
|
||||
useGetStoragePermissions(ref);
|
||||
|
||||
useEffect(() {
|
||||
FlutterNativeSplash.remove();
|
||||
@ -184,13 +197,9 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
/// For enabling hot reload for audio player
|
||||
if (!kDebugMode) return;
|
||||
audioPlayer.dispose();
|
||||
// youtube.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDisableBatteryOptimizations();
|
||||
useGetStoragePermissions(ref);
|
||||
|
||||
final lightTheme = useMemoized(
|
||||
() => theme(paletteColor ?? accentMaterialColor, Brightness.light, false),
|
||||
[paletteColor, accentMaterialColor],
|
||||
@ -201,7 +210,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
Brightness.dark,
|
||||
isAmoledTheme,
|
||||
),
|
||||
|
||||
[paletteColor, accentMaterialColor, isAmoledTheme],
|
||||
);
|
||||
|
||||
return MaterialApp.router(
|
||||
|
||||
@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/artist/artist_album_list.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/pages/artist/section/footer.dart';
|
||||
@ -35,45 +35,46 @@ class ArtistPage extends HookConsumerWidget {
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Builder(builder: (context) {
|
||||
if (artistQuery.isLoading || !artistQuery.hasData) {
|
||||
const ShimmerArtistProfile();
|
||||
} else if (artistQuery.hasError) {
|
||||
if (artistQuery.hasError) {
|
||||
return Center(child: Text(artistQuery.error.toString()));
|
||||
}
|
||||
return CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: ArtistPageHeader(artistId: artistId),
|
||||
),
|
||||
),
|
||||
const SliverGap(50),
|
||||
ArtistPageTopTracks(artistId: artistId),
|
||||
const SliverGap(50),
|
||||
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
|
||||
const SliverGap(20),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
context.l10n.fans_also_like,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
return Skeletonizer(
|
||||
enabled: artistQuery.isLoading,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: ArtistPageHeader(artistId: artistId),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverSafeArea(
|
||||
sliver: ArtistPageRelatedArtists(artistId: artistId),
|
||||
),
|
||||
if (artistQuery.data != null)
|
||||
SliverSafeArea(
|
||||
top: false,
|
||||
const SliverGap(50),
|
||||
ArtistPageTopTracks(artistId: artistId),
|
||||
const SliverGap(50),
|
||||
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
|
||||
const SliverGap(20),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: ArtistPageFooter(artist: artistQuery.data!),
|
||||
child: Text(
|
||||
context.l10n.fans_also_like,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SliverSafeArea(
|
||||
sliver: ArtistPageRelatedArtists(artistId: artistId),
|
||||
),
|
||||
if (artistQuery.data != null)
|
||||
SliverSafeArea(
|
||||
top: false,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: ArtistPageFooter(artist: artistQuery.data!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@ -4,7 +4,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
@ -25,7 +27,7 @@ class ArtistPageHeader extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final queryClient = useQueryClient();
|
||||
final artistQuery = useQueries.artist.get(ref, artistId);
|
||||
final artist = artistQuery.data;
|
||||
final artist = artistQuery.data ?? FakeData.artist;
|
||||
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
@ -41,10 +43,6 @@ class ArtistPageHeader extends HookConsumerWidget {
|
||||
xxl: textTheme.titleMedium,
|
||||
);
|
||||
|
||||
if (artist == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final spotify = ref.read(spotifyProvider);
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||
@ -96,10 +94,12 @@ class ArtistPageHeader extends HookConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(50)),
|
||||
child: Text(
|
||||
artist.type!.toUpperCase(),
|
||||
style: chipTextVariant.copyWith(
|
||||
color: Colors.white,
|
||||
child: Skeleton.keep(
|
||||
child: Text(
|
||||
artist.type!.toUpperCase(),
|
||||
style: chipTextVariant.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -138,113 +138,115 @@ class ArtistPageHeader extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const Gap(20),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (auth != null)
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final isFollowingQuery =
|
||||
useQueries.artist.doIFollow(ref, artistId);
|
||||
Skeleton.keep(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (auth != null)
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final isFollowingQuery =
|
||||
useQueries.artist.doIFollow(ref, artistId);
|
||||
|
||||
final followUnfollow = useCallback(() async {
|
||||
try {
|
||||
isFollowingQuery.data!
|
||||
? await spotify.me.unfollow(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
)
|
||||
: await spotify.me.follow(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
);
|
||||
await isFollowingQuery.refresh();
|
||||
final followUnfollow = useCallback(() async {
|
||||
try {
|
||||
isFollowingQuery.data!
|
||||
? await spotify.me.unfollow(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
)
|
||||
: await spotify.me.follow(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
);
|
||||
await isFollowingQuery.refresh();
|
||||
|
||||
queryClient.refreshInfiniteQueryAllPages(
|
||||
"user-following-artists");
|
||||
} finally {
|
||||
queryClient.refreshQuery(
|
||||
"user-follows-artists-query/$artistId",
|
||||
queryClient.refreshInfiniteQueryAllPages(
|
||||
"user-following-artists");
|
||||
} finally {
|
||||
queryClient.refreshQuery(
|
||||
"user-follows-artists-query/$artistId",
|
||||
);
|
||||
}
|
||||
}, [isFollowingQuery]);
|
||||
|
||||
if (isFollowingQuery.isLoading ||
|
||||
!isFollowingQuery.hasData) {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}, [isFollowingQuery]);
|
||||
|
||||
if (isFollowingQuery.isLoading ||
|
||||
!isFollowingQuery.hasData) {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (isFollowingQuery.data!) {
|
||||
return OutlinedButton(
|
||||
onPressed: followUnfollow,
|
||||
child: Text(context.l10n.following),
|
||||
);
|
||||
}
|
||||
|
||||
if (isFollowingQuery.data!) {
|
||||
return OutlinedButton(
|
||||
return FilledButton(
|
||||
onPressed: followUnfollow,
|
||||
child: Text(context.l10n.following),
|
||||
child: Text(context.l10n.follow),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
IconButton(
|
||||
tooltip: context.l10n.add_artist_to_blacklist,
|
||||
icon: Icon(
|
||||
SpotubeIcons.userRemove,
|
||||
color:
|
||||
!isBlackListed ? Colors.red[400] : Colors.white,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
isBlackListed ? Colors.red[400] : null,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isBlackListed) {
|
||||
ref
|
||||
.read(BlackListNotifier.provider.notifier)
|
||||
.remove(
|
||||
BlacklistedElement.artist(
|
||||
artist.id!, artist.name!),
|
||||
);
|
||||
} else {
|
||||
ref.read(BlackListNotifier.provider.notifier).add(
|
||||
BlacklistedElement.artist(
|
||||
artist.id!, artist.name!),
|
||||
);
|
||||
}
|
||||
|
||||
return FilledButton(
|
||||
onPressed: followUnfollow,
|
||||
child: Text(context.l10n.follow),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
IconButton(
|
||||
tooltip: context.l10n.add_artist_to_blacklist,
|
||||
icon: Icon(
|
||||
SpotubeIcons.userRemove,
|
||||
color:
|
||||
!isBlackListed ? Colors.red[400] : Colors.white,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
isBlackListed ? Colors.red[400] : null,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isBlackListed) {
|
||||
ref
|
||||
.read(BlackListNotifier.provider.notifier)
|
||||
.remove(
|
||||
BlacklistedElement.artist(
|
||||
artist.id!, artist.name!),
|
||||
);
|
||||
} else {
|
||||
ref.read(BlackListNotifier.provider.notifier).add(
|
||||
BlacklistedElement.artist(
|
||||
artist.id!, artist.name!),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.share),
|
||||
onPressed: () async {
|
||||
if (artist.externalUrls?.spotify != null) {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: artist.externalUrls!.spotify!,
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.share),
|
||||
onPressed: () async {
|
||||
if (artist.externalUrls?.spotify != null) {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: artist.externalUrls!.spotify!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
context.l10n.artist_url_copied,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
context.l10n.artist_url_copied,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
@ -28,11 +30,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
||||
topTracksQuery.data ?? <Track>[],
|
||||
);
|
||||
|
||||
if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
} else if (topTracksQuery.hasError) {
|
||||
if (topTracksQuery.hasError) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text(topTracksQuery.error.toString()),
|
||||
@ -40,7 +38,8 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final topTracks = topTracksQuery.data!;
|
||||
final topTracks =
|
||||
topTracksQuery.data ?? List.generate(10, (index) => FakeData.track);
|
||||
|
||||
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
|
||||
currentTrack ??= tracks.first;
|
||||
@ -92,9 +91,11 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
||||
color: Colors.white,
|
||||
icon: Skeleton.keep(
|
||||
child: Icon(
|
||||
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
|
||||
@ -1,125 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/genre/category_card.dart';
|
||||
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
|
||||
import 'package:spotube/components/shared/waypoint.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class GenrePage extends HookConsumerWidget {
|
||||
const GenrePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scrollController = useScrollController();
|
||||
final recommendationMarket = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||
);
|
||||
final categoriesQuery = useQueries.category.list(ref, recommendationMarket);
|
||||
final isFiltering = useState(false);
|
||||
|
||||
final isMounted = useIsMounted();
|
||||
|
||||
final searchController = useTextEditingController();
|
||||
final searchFocus = useFocusNode();
|
||||
|
||||
useValueListenable(searchController);
|
||||
|
||||
final categories = useMemoized(
|
||||
() {
|
||||
final categories = categoriesQuery.pages
|
||||
.expand<Category>(
|
||||
(page) => page.items ?? const Iterable.empty(),
|
||||
)
|
||||
.toList();
|
||||
if (searchController.text.isEmpty) {
|
||||
return categories;
|
||||
}
|
||||
return categories
|
||||
.map((e) => (
|
||||
weightedRatio(e.name!, searchController.text),
|
||||
e,
|
||||
))
|
||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||
.where((e) => e.$1 > 50)
|
||||
.map((e) => e.$2)
|
||||
.toList();
|
||||
},
|
||||
[categoriesQuery.pages, searchController.text],
|
||||
);
|
||||
|
||||
final list = RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await categoriesQuery.refreshAll();
|
||||
},
|
||||
child: Waypoint(
|
||||
onTouchEdge: () async {
|
||||
if (categoriesQuery.hasNextPage && isMounted()) {
|
||||
await categoriesQuery.fetchNext();
|
||||
}
|
||||
},
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
ExpandableSearchField(
|
||||
isFiltering: isFiltering.value,
|
||||
onChangeFiltering: (value) => isFiltering.value = value,
|
||||
searchController: searchController,
|
||||
searchFocus: searchFocus,
|
||||
),
|
||||
if (!categoriesQuery.hasPageData &&
|
||||
!categoriesQuery.isLoadingNextPage)
|
||||
const ShimmerCategories()
|
||||
else
|
||||
Expanded(
|
||||
child: InfiniteList(
|
||||
scrollController: scrollController,
|
||||
itemCount: categories.length,
|
||||
onFetchData: categoriesQuery.fetchNext,
|
||||
isLoading: categoriesQuery.isLoadingNextPage,
|
||||
hasReachedMax: !categoriesQuery.hasNextPage,
|
||||
loadingBuilder: (context) => const ShimmerCategories(),
|
||||
itemBuilder: (context, index) {
|
||||
return CategoryCard(categories[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(child: list),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 10,
|
||||
child: ExpandableSearchButton(
|
||||
isFiltering: isFiltering.value,
|
||||
searchFocus: searchFocus,
|
||||
icon: const Icon(SpotubeIcons.search),
|
||||
onPressed: (value) {
|
||||
isFiltering.value = value;
|
||||
if (isFiltering.value) {
|
||||
scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
174
lib/pages/home/genres/genre_playlists.dart
Normal file
174
lib/pages/home/genres/genre_playlists.dart
Normal file
@ -0,0 +1,174 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart' hide Offset;
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/playlist/playlist_card.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/waypoint.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
final Category category;
|
||||
const GenrePlaylistsPage({Key? key, required this.category})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlistsQuery = useQueries.category.playlistsOf(
|
||||
ref,
|
||||
category.id!,
|
||||
);
|
||||
|
||||
final playlists = useMemoized(
|
||||
() => playlistsQuery.pages.expand(
|
||||
(page) {
|
||||
return page.items?.whereNotNull() ??
|
||||
const Iterable<PlaylistSimple>.empty();
|
||||
},
|
||||
).toList(),
|
||||
[playlistsQuery.pages],
|
||||
);
|
||||
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final scrollController = useScrollController();
|
||||
|
||||
return Scaffold(
|
||||
appBar: DesktopTools.platform.isDesktop
|
||||
? const PageWindowTitleBar(
|
||||
leading: BackButton(color: Colors.white),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
)
|
||||
: null,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: DesktopTools.platform.isMobile,
|
||||
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
||||
pinned: true,
|
||||
floating: false,
|
||||
title: const Text(""),
|
||||
backgroundColor: Colors.brown.withOpacity(0.7),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
stretchModes: const [
|
||||
StretchMode.zoomBackground,
|
||||
StretchMode.blurBackground,
|
||||
],
|
||||
background: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(
|
||||
category.icons!.first.url!,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: const ColoredBox(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
centerTitle: DesktopTools.platform.isDesktop,
|
||||
title: Text(
|
||||
category.name!,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
letterSpacing: 3,
|
||||
shadows: [
|
||||
const Shadow(
|
||||
offset: Offset(-1.5, -1.5),
|
||||
color: Colors.black54,
|
||||
),
|
||||
const Shadow(
|
||||
offset: Offset(1.5, -1.5),
|
||||
color: Colors.black54,
|
||||
),
|
||||
const Shadow(
|
||||
offset: Offset(1.5, 1.5),
|
||||
color: Colors.black54,
|
||||
),
|
||||
const Shadow(
|
||||
offset: Offset(-1.5, 1.5),
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
collapseMode: CollapseMode.parallax,
|
||||
),
|
||||
),
|
||||
const SliverGap(20),
|
||||
SliverSafeArea(
|
||||
top: false,
|
||||
sliver: SliverPadding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: mediaQuery.mdAndDown ? 12 : 24,
|
||||
),
|
||||
sliver: playlists.isEmpty
|
||||
? Skeletonizer.sliver(
|
||||
child: SliverToBoxAdapter(
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: List.generate(
|
||||
6,
|
||||
(index) => PlaylistCard(FakeData.playlist),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 190,
|
||||
mainAxisExtent: mediaQuery.mdAndDown ? 225 : 250,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: playlists.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlists.elementAtOrNull(index);
|
||||
|
||||
if (playlist == null) {
|
||||
if (!playlistsQuery.hasNextPage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Skeletonizer(
|
||||
enabled: true,
|
||||
child: Waypoint(
|
||||
controller: scrollController,
|
||||
isGrid: true,
|
||||
onTouchEdge: () async {
|
||||
if (playlistsQuery.hasNextPage) {
|
||||
await playlistsQuery.fetchNext();
|
||||
}
|
||||
},
|
||||
child: PlaylistCard(FakeData.playlist),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Skeleton.keep(
|
||||
child: PlaylistCard(playlist),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib/pages/home/genres/genres.dart
Normal file
98
lib/pages/home/genres/genres.dart
Normal file
@ -0,0 +1,98 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart' hide Offset;
|
||||
import 'package:spotube/collections/gradients.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
class GenrePage extends HookConsumerWidget {
|
||||
const GenrePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme) = Theme.of(context);
|
||||
final scrollController = useScrollController();
|
||||
final recommendationMarket = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||
);
|
||||
final categoriesQuery =
|
||||
useQueries.category.listAll(ref, recommendationMarket);
|
||||
|
||||
final categories = categoriesQuery.data ?? <Category>[];
|
||||
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
title: Text(context.l10n.explore_genres),
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
childAspectRatio: 9 / 18,
|
||||
maxCrossAxisExtent: mediaQuery.smAndDown ? 200 : 300,
|
||||
mainAxisExtent: 200,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final gradient = gradients[Random().nextInt(gradients.length)];
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
context.push("/genre/${category.id}", extra: category);
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(category.icons!.first.url!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
gradient: gradient,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: AutoSizeText(
|
||||
category.name!,
|
||||
style: textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
// stroke shadow
|
||||
const Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
maxFontSize: textTheme.titleLarge!.fontSize!,
|
||||
minFontSize: textTheme.titleMedium!.fontSize!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,36 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/home/sections/featured.dart';
|
||||
import 'package:spotube/components/home/sections/genres.dart';
|
||||
import 'package:spotube/components/home/sections/made_for_user.dart';
|
||||
import 'package:spotube/components/home/sections/new_releases.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/themed_button_tab_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/pages/home/genres.dart';
|
||||
import 'package:spotube/pages/home/personalized.dart';
|
||||
|
||||
class HomePage extends HookConsumerWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
centerTitle: true,
|
||||
leadingWidth: double.infinity,
|
||||
leading: ThemedButtonsTabBar(
|
||||
tabs: [
|
||||
Tab(text: " ${context.l10n.personalized} "),
|
||||
Tab(text: " ${context.l10n.genre} "),
|
||||
final controller = useScrollController();
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
appBar: DesktopTools.platform.isMobile
|
||||
? null
|
||||
: const PageWindowTitleBar(),
|
||||
body: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
const HomeGenresSection(),
|
||||
SliverList.list(
|
||||
children: const [
|
||||
HomeFeaturedSection(),
|
||||
HomeNewReleasesSection(),
|
||||
],
|
||||
),
|
||||
const SliverSafeArea(sliver: HomeMadeForUserSection()),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
PersonalizedPage(),
|
||||
GenrePage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PersonalizedPage extends HookConsumerWidget {
|
||||
const PersonalizedPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final controller = useScrollController();
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
|
||||
final playlists = useMemoized(
|
||||
() => featuredPlaylistsQuery.pages
|
||||
.whereType<Page<PlaylistSimple>>()
|
||||
.expand((page) => page.items ?? const <PlaylistSimple>[]),
|
||||
[featuredPlaylistsQuery.pages],
|
||||
);
|
||||
|
||||
final madeForUser = useQueries.views.get(ref, "made-for-x-hub");
|
||||
|
||||
final newReleases = useQueries.album.newReleases(ref);
|
||||
final userArtistsQuery = useQueries.artist.followedByMeAll(ref);
|
||||
final userArtists =
|
||||
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
|
||||
|
||||
final albums = useMemoized(
|
||||
() => newReleases.pages
|
||||
.whereType<Page<AlbumSimple>>()
|
||||
.expand((page) => page.items ?? const <AlbumSimple>[])
|
||||
.where((album) {
|
||||
return album.artists
|
||||
?.any((artist) => userArtists.contains(artist.id!)) ==
|
||||
true;
|
||||
})
|
||||
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album))
|
||||
.toList(),
|
||||
[newReleases.pages],
|
||||
);
|
||||
|
||||
return CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
SliverList.list(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: !featuredPlaylistsQuery.hasPageData &&
|
||||
!featuredPlaylistsQuery.isLoadingNextPage
|
||||
? const ShimmerCategories()
|
||||
: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||
items: playlists.toList(),
|
||||
title: Text(context.l10n.featured),
|
||||
isLoadingNextPage:
|
||||
featuredPlaylistsQuery.isLoadingNextPage,
|
||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: newReleases.hasPageData &&
|
||||
userArtistsQuery.hasData &&
|
||||
!newReleases.isLoadingNextPage
|
||||
? HorizontalPlaybuttonCardView<Album>(
|
||||
items: albums,
|
||||
title: Text(context.l10n.new_releases),
|
||||
isLoadingNextPage: newReleases.isLoadingNextPage,
|
||||
hasNextPage: newReleases.hasNextPage,
|
||||
onFetchMore: newReleases.fetchNext,
|
||||
)
|
||||
: const ShimmerCategories(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverSafeArea(
|
||||
sliver: SliverList.builder(
|
||||
itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final item = madeForUser.data?["content"]?["items"]?[index];
|
||||
final playlists = item["content"]?["items"]
|
||||
?.where((itemL2) => itemL2["type"] == "playlist")
|
||||
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
||||
.toList()
|
||||
.cast<PlaylistSimple>() ??
|
||||
<PlaylistSimple>[];
|
||||
if (playlists.isEmpty) return const SizedBox.shrink();
|
||||
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||
items: playlists,
|
||||
title: Text(item["name"] ?? ""),
|
||||
hasNextPage: false,
|
||||
isLoadingNextPage: false,
|
||||
onFetchMore: () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -77,7 +77,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
: textTheme.headlineMedium?.copyWith(fontSize: 25))
|
||||
?.copyWith(color: palette.titleTextColor);
|
||||
|
||||
var bodyTextTheme = textTheme.bodyLarge?.copyWith(
|
||||
final bodyTextTheme = textTheme.bodyLarge?.copyWith(
|
||||
color: palette.bodyTextColor,
|
||||
);
|
||||
return Stack(
|
||||
@ -184,7 +184,9 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
),
|
||||
if (playlist.activeTrack != null &&
|
||||
(timedLyricsQuery.isLoading || timedLyricsQuery.isRefreshing))
|
||||
const Expanded(child: ShimmerLyrics())
|
||||
const Expanded(
|
||||
child: ShimmerLyrics(),
|
||||
)
|
||||
else if (playlist.activeTrack != null &&
|
||||
(timedLyricsQuery.hasError))
|
||||
Text(
|
||||
|
||||
@ -31,6 +31,9 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
||||
onFetchAll: () async {
|
||||
return tracks.toList();
|
||||
},
|
||||
onRefresh: () async {
|
||||
await likedTracks.refresh();
|
||||
},
|
||||
),
|
||||
title: playlist.name!,
|
||||
description: playlist.description,
|
||||
|
||||
@ -32,7 +32,7 @@ class SearchArtistsSection extends HookConsumerWidget {
|
||||
hasNextPage: query.hasNextPage,
|
||||
items: artists,
|
||||
onFetchMore: query.fetchNext,
|
||||
title: Text(context.l10n.albums),
|
||||
title: Text(context.l10n.artists),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
||||
@ -50,6 +51,13 @@ class SettingsDesktopSection extends HookConsumerWidget {
|
||||
value: preferences.systemTitleBar,
|
||||
onChanged: preferencesNotifier.setSystemTitleBar,
|
||||
),
|
||||
if (!DesktopTools.platform.isMacOS)
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.discord),
|
||||
title: Text(context.l10n.discord_rich_presence),
|
||||
value: preferences.discordPresence,
|
||||
onChanged: preferencesNotifier.setDiscordPresence,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
),
|
||||
AdaptiveSelectTile<AudioSource>(
|
||||
secondary: const Icon(SpotubeIcons.api),
|
||||
title: Text(context.l10n.youtube_api_type),
|
||||
title: Text(context.l10n.audio_source),
|
||||
value: preferences.audioSource,
|
||||
options: AudioSource.values
|
||||
.map((e) => DropdownMenuItem(
|
||||
|
||||
70
lib/provider/discord_provider.dart
Normal file
70
lib/provider/discord_provider.dart
Normal file
@ -0,0 +1,70 @@
|
||||
import 'package:dart_discord_rpc/dart_discord_rpc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class Discord extends ChangeNotifier {
|
||||
final DiscordRPC? discordRPC;
|
||||
final bool isEnabled;
|
||||
|
||||
Discord(this.isEnabled)
|
||||
: discordRPC = (DesktopTools.platform.isWindows ||
|
||||
DesktopTools.platform.isLinux) &&
|
||||
isEnabled
|
||||
? DiscordRPC(applicationId: Env.discordAppId)
|
||||
: null {
|
||||
discordRPC?.start(autoRegister: true);
|
||||
}
|
||||
|
||||
void updatePresence(Track track) {
|
||||
clear();
|
||||
final artistNames =
|
||||
TypeConversionUtils.artists_X_String(track.artists ?? <Artist>[]);
|
||||
discordRPC?.updatePresence(
|
||||
DiscordPresence(
|
||||
details: "Song: ${track.name} by $artistNames",
|
||||
state: "Vibing in Music",
|
||||
startTimeStamp: DateTime.now().millisecondsSinceEpoch,
|
||||
largeImageKey: "spotube-logo-foreground",
|
||||
largeImageText: "Spotube",
|
||||
smallImageKey: "spotube-logo-foreground",
|
||||
smallImageText: "Spotube",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
discordRPC?.clearPresence();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
discordRPC?.shutDown();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
clear();
|
||||
shutdown();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final discordProvider = ChangeNotifierProvider(
|
||||
(ref) {
|
||||
final isEnabled =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.discordPresence));
|
||||
final playback = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final discord = Discord(isEnabled);
|
||||
|
||||
if (playback.activeTrack != null) {
|
||||
discord.updatePresence(playback.activeTrack!);
|
||||
}
|
||||
|
||||
return discord;
|
||||
},
|
||||
);
|
||||
@ -24,10 +24,12 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||
import 'package:spotube/services/discord/discord.dart';
|
||||
import 'package:spotube/provider/discord_provider.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
@ -62,6 +64,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
ProxyPlaylist get playlist => state;
|
||||
BlackListNotifier get blacklist =>
|
||||
ref.read(BlackListNotifier.provider.notifier);
|
||||
Discord get discord => ref.read(discordProvider);
|
||||
|
||||
static final provider =
|
||||
StateNotifierProvider<ProxyPlaylistNotifier, ProxyPlaylist>(
|
||||
@ -161,8 +164,8 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final isNotYTMode = preferences.audioSource != AudioSource.youtube ||
|
||||
(preferences.audioSource == AudioSource.piped &&
|
||||
final isNotYTMode = state.activeTrack is! YoutubeSourcedTrack &&
|
||||
(state.activeTrack is PipedSourcedTrack &&
|
||||
preferences.searchMode == SearchMode.youtubeMusic);
|
||||
|
||||
if (isNotYTMode || !preferences.skipNonMusic) return;
|
||||
|
||||
@ -43,7 +43,7 @@ class ScrobblerNotifier extends PersistedStateNotifier<ScrobblerState?> {
|
||||
_scrobbleController.stream.listen((track) async {
|
||||
try {
|
||||
await state?.scrobblenaut.track.scrobble(
|
||||
artist: TypeConversionUtils.artists_X_String(track.artists!),
|
||||
artist: track.artists!.first.name!,
|
||||
track: track.name!,
|
||||
album: track.album!.name!,
|
||||
chosenByUser: true,
|
||||
|
||||
@ -110,6 +110,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
}
|
||||
}
|
||||
|
||||
void setDiscordPresence(bool discordPresence) {
|
||||
state = state.copyWith(discordPresence: discordPresence);
|
||||
}
|
||||
|
||||
void setAmoledDarkTheme(bool isAmoled) {
|
||||
state = state.copyWith(amoledDarkTheme: isAmoled);
|
||||
}
|
||||
|
||||
@ -198,6 +198,9 @@ final class UserPreferences {
|
||||
)
|
||||
final SourceCodecs downloadMusicCodec;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool discordPresence;
|
||||
|
||||
UserPreferences({
|
||||
required this.audioQuality,
|
||||
required this.albumColorSync,
|
||||
@ -219,6 +222,7 @@ final class UserPreferences {
|
||||
required this.audioSource,
|
||||
required this.streamMusicCodec,
|
||||
required this.downloadMusicCodec,
|
||||
required this.discordPresence,
|
||||
});
|
||||
|
||||
factory UserPreferences.withDefaults() {
|
||||
@ -255,6 +259,7 @@ final class UserPreferences {
|
||||
SourceCodecs? downloadMusicCodec,
|
||||
SourceCodecs? streamMusicCodec,
|
||||
bool? systemTitleBar,
|
||||
bool? discordPresence,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
@ -277,6 +282,7 @@ final class UserPreferences {
|
||||
normalizeAudio: normalizeAudio ?? this.normalizeAudio,
|
||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||
systemTitleBar: systemTitleBar ?? this.systemTitleBar,
|
||||
discordPresence: discordPresence ?? this.discordPresence,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@ UserPreferences _$UserPreferencesFromJson(Map<String, dynamic> json) =>
|
||||
_$SourceCodecsEnumMap, json['downloadMusicCodec'],
|
||||
unknownValue: SourceCodecs.m4a) ??
|
||||
SourceCodecs.m4a,
|
||||
discordPresence: json['discordPresence'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserPreferencesToJson(UserPreferences instance) =>
|
||||
@ -88,6 +89,7 @@ Map<String, dynamic> _$UserPreferencesToJson(UserPreferences instance) =>
|
||||
'audioSource': _$AudioSourceEnumMap[instance.audioSource]!,
|
||||
'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!,
|
||||
'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!,
|
||||
'discordPresence': instance.discordPresence,
|
||||
};
|
||||
|
||||
const _$SourceQualitiesEnumMap = {
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import 'package:dart_discord_rpc/dart_discord_rpc.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class Discord {
|
||||
final DiscordRPC? discordRPC;
|
||||
|
||||
Discord()
|
||||
: discordRPC =
|
||||
DesktopTools.platform.isWindows || DesktopTools.platform.isLinux
|
||||
? DiscordRPC(applicationId: Env.discordAppId)
|
||||
: null {
|
||||
discordRPC?.start(autoRegister: true);
|
||||
}
|
||||
|
||||
void updatePresence(Track track) {
|
||||
clear();
|
||||
final artistNames =
|
||||
TypeConversionUtils.artists_X_String(track.artists ?? <Artist>[]);
|
||||
discordRPC?.updatePresence(
|
||||
DiscordPresence(
|
||||
details: "Song: ${track.name} by $artistNames",
|
||||
state: "Vibing in Music",
|
||||
startTimeStamp: DateTime.now().millisecondsSinceEpoch,
|
||||
largeImageKey: "spotube-logo-foreground",
|
||||
largeImageText: "Spotube",
|
||||
smallImageKey: "spotube-logo-foreground",
|
||||
smallImageText: "Spotube",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
discordRPC?.clearPresence();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
discordRPC?.shutDown();
|
||||
}
|
||||
}
|
||||
|
||||
final discord = Discord();
|
||||
@ -5,12 +5,37 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
class CategoryQueries {
|
||||
const CategoryQueries();
|
||||
|
||||
Query<List<Category>, dynamic> listAll(
|
||||
WidgetRef ref,
|
||||
Market recommendationMarket,
|
||||
) {
|
||||
ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
final locale = useContext().l10n.localeName;
|
||||
final query = useSpotifyQuery<List<Category>, dynamic>(
|
||||
"category-playlists",
|
||||
(spotify) async {
|
||||
final categories = await spotify.categories
|
||||
.list(
|
||||
country: recommendationMarket,
|
||||
locale: locale,
|
||||
)
|
||||
.all();
|
||||
|
||||
return categories.toList()..shuffle();
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<Category>, dynamic, int> list(
|
||||
WidgetRef ref,
|
||||
Market recommendationMarket,
|
||||
|
||||
@ -52,6 +52,7 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) {
|
||||
),
|
||||
sliderTheme: SliderThemeData(overlayShape: SliderComponentShape.noOverlay),
|
||||
searchBarTheme: SearchBarThemeData(
|
||||
textStyle: const MaterialStatePropertyAll(TextStyle(fontSize: 15)),
|
||||
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||
padding: const MaterialStatePropertyAll(EdgeInsets.all(8)),
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include <dart_discord_rpc/dart_discord_rpc_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
@ -28,6 +29,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
|
||||
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
|
||||
|
||||
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dart_discord_rpc
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
gtk
|
||||
local_notifier
|
||||
media_kit_libs_linux
|
||||
screen_retriever
|
||||
|
||||
@ -17,6 +17,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
|
||||
GList* windows = gtk_application_get_windows(GTK_APPLICATION(application));
|
||||
if (windows) {
|
||||
gtk_window_present(GTK_WINDOW(windows->data));
|
||||
return;
|
||||
}
|
||||
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
@ -78,7 +85,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
@ -98,7 +105,7 @@ static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
"com.github.KRTirtho.Spotube", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
@ -11,3 +11,6 @@ keywords:
|
||||
generic_name: Music Streaming Application
|
||||
categories:
|
||||
- Music
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/spotify
|
||||
|
||||
@ -32,3 +32,6 @@ keywords:
|
||||
generic_name: Music Streaming Application
|
||||
categories:
|
||||
- Music
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/spotify
|
||||
|
||||
@ -28,3 +28,6 @@ categories:
|
||||
- Music
|
||||
|
||||
startup_notify: true
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/spotify
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import audio_service
|
||||
import audio_session
|
||||
import device_info_plus
|
||||
@ -24,6 +25,7 @@ import window_manager
|
||||
import window_size
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
|
||||
@ -2,6 +2,19 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<!-- abstract name for this URL type (you can leave it blank) -->
|
||||
<string>Spotify</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<!-- your schemes -->
|
||||
<string>spotify</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
||||
38
pubspec.lock
38
pubspec.lock
@ -17,6 +17,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.0"
|
||||
app_package_maker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -907,6 +915,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_sharing_intent:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_sharing_intent
|
||||
sha256: "6eb896e6523b735e8230eeb206fd3b9f220f11ce879c2400a90b443147036ff9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1018,6 +1034,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gtk
|
||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1832,6 +1856,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
skeletonizer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: skeletonizer
|
||||
sha256: ff4c36e826efd5288d7a84e7619a6e9be8185d3064cecf101a9133762f3b401b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -2238,13 +2270,13 @@ packages:
|
||||
source: hosted
|
||||
version: "5.0.7"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9
|
||||
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 3.3.0+26
|
||||
version: 3.4.0+27
|
||||
|
||||
homepage: https://spotube.krtirtho.dev
|
||||
repository: https://github.com/KRTirtho/spotube
|
||||
@ -118,6 +118,10 @@ dependencies:
|
||||
url: https://github.com/Tommypop2/dart_discord_rpc.git
|
||||
html_unescape: ^2.0.0
|
||||
wikipedia_api: ^0.1.0
|
||||
skeletonizer: ^0.8.0
|
||||
app_links: ^3.5.0
|
||||
win32_registry: ^1.1.2
|
||||
flutter_sharing_intent: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <dart_discord_rpc/dart_discord_rpc_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
@ -20,6 +21,8 @@
|
||||
#include <window_size/window_size_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
DartDiscordRpcPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DartDiscordRpcPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
dart_discord_rpc
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
#include "app_links/app_links_plugin_c_api.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@ -105,6 +106,9 @@ Win32Window::~Win32Window() {
|
||||
bool Win32Window::CreateAndShow(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
if (SendAppLinkToInstance(title)) {
|
||||
return false;
|
||||
}
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
@ -244,3 +248,39 @@ bool Win32Window::OnCreate() {
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
// app_links
|
||||
bool Win32Window::SendAppLinkToInstance(const std::wstring& title) {
|
||||
// Find our exact window
|
||||
HWND hwnd = ::FindWindow(kWindowClassName, title.c_str());
|
||||
|
||||
if (hwnd) {
|
||||
// Dispatch new link to current window
|
||||
SendAppLink(hwnd);
|
||||
|
||||
// (Optional) Restore our window to front in same state
|
||||
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
||||
GetWindowPlacement(hwnd, &place);
|
||||
|
||||
switch(place.showCmd) {
|
||||
case SW_SHOWMAXIMIZED:
|
||||
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
|
||||
break;
|
||||
case SW_SHOWMINIMIZED:
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
break;
|
||||
default:
|
||||
ShowWindow(hwnd, SW_NORMAL);
|
||||
break;
|
||||
}
|
||||
|
||||
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
|
||||
SetForegroundWindow(hwnd);
|
||||
// END Restore
|
||||
|
||||
// Window has been found, don't create another one.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -93,6 +93,10 @@ class Win32Window {
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
// Dispatches link if any.
|
||||
// This method enables our app to be with a single instance too.
|
||||
// This is mandatory if you want to catch further links in same app.
|
||||
bool SendAppLinkToInstance(const std::wstring& title);
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
Loading…
Reference in New Issue
Block a user