mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Compare commits
7 Commits
460738fc11
...
fb362b9cfc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fb362b9cfc | ||
![]() |
308c417ed3 | ||
![]() |
0823629fb5 | ||
![]() |
7037145519 | ||
![]() |
08d1c98674 | ||
![]() |
c0dcc87eb2 | ||
![]() |
3ff0f6dd27 |
2
.github/workflows/spotube-release-binary.yml
vendored
2
.github/workflows/spotube-release-binary.yml
vendored
@ -56,7 +56,7 @@ jobs:
|
||||
files: |
|
||||
dist/Spotube-windows-x86_64.nupkg
|
||||
dist/Spotube-windows-x86_64-setup.exe
|
||||
- os: macos-latest
|
||||
- os: macos-14
|
||||
platform: ios
|
||||
arch: all
|
||||
files: |
|
||||
|
96
assets/fonts/Ubuntu_Mono/UFL.txt
Normal file
96
assets/fonts/Ubuntu_Mono/UFL.txt
Normal file
@ -0,0 +1,96 @@
|
||||
-------------------------------
|
||||
UBUNTU FONT LICENCE Version 1.0
|
||||
-------------------------------
|
||||
|
||||
PREAMBLE
|
||||
This licence allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely. The fonts, including any derivative works, can be
|
||||
bundled, embedded, and redistributed provided the terms of this licence
|
||||
are met. The fonts and derivatives, however, cannot be released under
|
||||
any other licence. The requirement for fonts to remain under this
|
||||
licence does not require any document created using the fonts or their
|
||||
derivatives to be published under this licence, as long as the primary
|
||||
purpose of the document is not to be a vehicle for the distribution of
|
||||
the fonts.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this licence and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as received under this licence.
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to
|
||||
a new environment.
|
||||
|
||||
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||
copyright ownership of the Font Software.
|
||||
|
||||
"Substantially Changed" refers to Modified Versions which can be easily
|
||||
identified as dissimilar to the Font Software by users of the Font
|
||||
Software comparing the Original Version with the Modified Version.
|
||||
|
||||
To "Propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification and with or without charging
|
||||
a redistribution fee), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
This licence does not grant any rights under trademark law and all such
|
||||
rights are reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to propagate the Font Software, subject to
|
||||
the below conditions:
|
||||
|
||||
1) Each copy of the Font Software must contain the above copyright
|
||||
notice and this licence. These can be included either as stand-alone
|
||||
text files, human-readable headers or in the appropriate machine-
|
||||
readable metadata fields within text or binary files as long as those
|
||||
fields can be easily viewed by the user.
|
||||
|
||||
2) The font name complies with the following:
|
||||
(a) The Original Version must retain its name, unmodified.
|
||||
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||
avoid use of the name of the Original Version or similar names entirely.
|
||||
(c) Modified Versions which are not Substantially Changed must be
|
||||
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||
additional naming elements to distinguish the Modified Version from the
|
||||
Original Version. The name of such Modified Versions must be the name of
|
||||
the Original Version, with "derivative X" where X represents the name of
|
||||
the new work, appended to that name.
|
||||
|
||||
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||
Font Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except (i) as required by this licence, (ii) to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||
their explicit written permission.
|
||||
|
||||
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this licence, and must not be distributed
|
||||
under any other licence. The requirement for fonts to remain under this
|
||||
licence does not affect any document created using the Font Software,
|
||||
except any version of the Font Software extracted from a document
|
||||
created using the Font Software may only be distributed under this
|
||||
licence.
|
||||
|
||||
TERMINATION
|
||||
This licence becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
Normal file
Binary file not shown.
@ -108,6 +108,10 @@ class AppRouter extends RootStackRouter {
|
||||
path: "settings/about",
|
||||
page: AboutSpotubeRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "settings/scrobbling",
|
||||
page: SettingsScrobblingRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: "album/:id",
|
||||
page: AlbumRoute.page,
|
||||
|
@ -8,10 +8,10 @@
|
||||
// coverage:ignore-file
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:auto_route/auto_route.dart' as _i40;
|
||||
import 'package:flutter/material.dart' as _i41;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as _i43;
|
||||
import 'package:spotube/models/metadata/metadata.dart' as _i42;
|
||||
import 'package:auto_route/auto_route.dart' as _i41;
|
||||
import 'package:flutter/material.dart' as _i42;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as _i44;
|
||||
import 'package:spotube/models/metadata/metadata.dart' as _i43;
|
||||
import 'package:spotube/pages/album/album.dart' as _i2;
|
||||
import 'package:spotube/pages/artist/artist.dart' as _i3;
|
||||
import 'package:spotube/pages/connect/connect.dart' as _i6;
|
||||
@ -21,14 +21,14 @@ import 'package:spotube/pages/home/home.dart' as _i9;
|
||||
import 'package:spotube/pages/home/sections/section_items.dart' as _i8;
|
||||
import 'package:spotube/pages/lastfm_login/lastfm_login.dart' as _i10;
|
||||
import 'package:spotube/pages/library/library.dart' as _i11;
|
||||
import 'package:spotube/pages/library/user_albums.dart' as _i35;
|
||||
import 'package:spotube/pages/library/user_artists.dart' as _i36;
|
||||
import 'package:spotube/pages/library/user_downloads.dart' as _i37;
|
||||
import 'package:spotube/pages/library/user_albums.dart' as _i36;
|
||||
import 'package:spotube/pages/library/user_artists.dart' as _i37;
|
||||
import 'package:spotube/pages/library/user_downloads.dart' as _i38;
|
||||
import 'package:spotube/pages/library/user_local_tracks/local_folder.dart'
|
||||
as _i13;
|
||||
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'
|
||||
as _i38;
|
||||
import 'package:spotube/pages/library/user_playlists.dart' as _i39;
|
||||
as _i39;
|
||||
import 'package:spotube/pages/library/user_playlists.dart' as _i40;
|
||||
import 'package:spotube/pages/lyrics/lyrics.dart' as _i15;
|
||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart' as _i16;
|
||||
import 'package:spotube/pages/player/lyrics.dart' as _i17;
|
||||
@ -44,20 +44,21 @@ import 'package:spotube/pages/settings/blacklist.dart' as _i4;
|
||||
import 'package:spotube/pages/settings/logs.dart' as _i14;
|
||||
import 'package:spotube/pages/settings/metadata/metadata_form.dart' as _i24;
|
||||
import 'package:spotube/pages/settings/metadata_plugins.dart' as _i25;
|
||||
import 'package:spotube/pages/settings/scrobbling/scrobbling.dart' as _i27;
|
||||
import 'package:spotube/pages/settings/settings.dart' as _i26;
|
||||
import 'package:spotube/pages/stats/albums/albums.dart' as _i27;
|
||||
import 'package:spotube/pages/stats/artists/artists.dart' as _i28;
|
||||
import 'package:spotube/pages/stats/fees/fees.dart' as _i32;
|
||||
import 'package:spotube/pages/stats/minutes/minutes.dart' as _i29;
|
||||
import 'package:spotube/pages/stats/playlists/playlists.dart' as _i31;
|
||||
import 'package:spotube/pages/stats/stats.dart' as _i30;
|
||||
import 'package:spotube/pages/stats/streams/streams.dart' as _i33;
|
||||
import 'package:spotube/pages/track/track.dart' as _i34;
|
||||
import 'package:spotube/pages/stats/albums/albums.dart' as _i28;
|
||||
import 'package:spotube/pages/stats/artists/artists.dart' as _i29;
|
||||
import 'package:spotube/pages/stats/fees/fees.dart' as _i33;
|
||||
import 'package:spotube/pages/stats/minutes/minutes.dart' as _i30;
|
||||
import 'package:spotube/pages/stats/playlists/playlists.dart' as _i32;
|
||||
import 'package:spotube/pages/stats/stats.dart' as _i31;
|
||||
import 'package:spotube/pages/stats/streams/streams.dart' as _i34;
|
||||
import 'package:spotube/pages/track/track.dart' as _i35;
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.AboutSpotubePage]
|
||||
class AboutSpotubeRoute extends _i40.PageRouteInfo<void> {
|
||||
const AboutSpotubeRoute({List<_i40.PageRouteInfo>? children})
|
||||
class AboutSpotubeRoute extends _i41.PageRouteInfo<void> {
|
||||
const AboutSpotubeRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
AboutSpotubeRoute.name,
|
||||
initialChildren: children,
|
||||
@ -65,7 +66,7 @@ class AboutSpotubeRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'AboutSpotubeRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i1.AboutSpotubePage();
|
||||
@ -75,12 +76,12 @@ class AboutSpotubeRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.AlbumPage]
|
||||
class AlbumRoute extends _i40.PageRouteInfo<AlbumRouteArgs> {
|
||||
class AlbumRoute extends _i41.PageRouteInfo<AlbumRouteArgs> {
|
||||
AlbumRoute({
|
||||
_i41.Key? key,
|
||||
_i42.Key? key,
|
||||
required String id,
|
||||
required _i42.SpotubeSimpleAlbumObject album,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
required _i43.SpotubeSimpleAlbumObject album,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumRoute.name,
|
||||
args: AlbumRouteArgs(
|
||||
@ -94,7 +95,7 @@ class AlbumRoute extends _i40.PageRouteInfo<AlbumRouteArgs> {
|
||||
|
||||
static const String name = 'AlbumRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<AlbumRouteArgs>();
|
||||
@ -114,11 +115,11 @@ class AlbumRouteArgs {
|
||||
required this.album,
|
||||
});
|
||||
|
||||
final _i41.Key? key;
|
||||
final _i42.Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
final _i42.SpotubeSimpleAlbumObject album;
|
||||
final _i43.SpotubeSimpleAlbumObject album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -128,11 +129,11 @@ class AlbumRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.ArtistPage]
|
||||
class ArtistRoute extends _i40.PageRouteInfo<ArtistRouteArgs> {
|
||||
class ArtistRoute extends _i41.PageRouteInfo<ArtistRouteArgs> {
|
||||
ArtistRoute({
|
||||
required String artistId,
|
||||
_i41.Key? key,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
_i42.Key? key,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
ArtistRoute.name,
|
||||
args: ArtistRouteArgs(
|
||||
@ -145,7 +146,7 @@ class ArtistRoute extends _i40.PageRouteInfo<ArtistRouteArgs> {
|
||||
|
||||
static const String name = 'ArtistRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
@ -167,7 +168,7 @@ class ArtistRouteArgs {
|
||||
|
||||
final String artistId;
|
||||
|
||||
final _i41.Key? key;
|
||||
final _i42.Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -177,8 +178,8 @@ class ArtistRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.BlackListPage]
|
||||
class BlackListRoute extends _i40.PageRouteInfo<void> {
|
||||
const BlackListRoute({List<_i40.PageRouteInfo>? children})
|
||||
class BlackListRoute extends _i41.PageRouteInfo<void> {
|
||||
const BlackListRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
BlackListRoute.name,
|
||||
initialChildren: children,
|
||||
@ -186,7 +187,7 @@ class BlackListRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'BlackListRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i4.BlackListPage();
|
||||
@ -196,8 +197,8 @@ class BlackListRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i5.ConnectControlPage]
|
||||
class ConnectControlRoute extends _i40.PageRouteInfo<void> {
|
||||
const ConnectControlRoute({List<_i40.PageRouteInfo>? children})
|
||||
class ConnectControlRoute extends _i41.PageRouteInfo<void> {
|
||||
const ConnectControlRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
ConnectControlRoute.name,
|
||||
initialChildren: children,
|
||||
@ -205,7 +206,7 @@ class ConnectControlRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'ConnectControlRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i5.ConnectControlPage();
|
||||
@ -215,8 +216,8 @@ class ConnectControlRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i6.ConnectPage]
|
||||
class ConnectRoute extends _i40.PageRouteInfo<void> {
|
||||
const ConnectRoute({List<_i40.PageRouteInfo>? children})
|
||||
class ConnectRoute extends _i41.PageRouteInfo<void> {
|
||||
const ConnectRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
ConnectRoute.name,
|
||||
initialChildren: children,
|
||||
@ -224,7 +225,7 @@ class ConnectRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'ConnectRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i6.ConnectPage();
|
||||
@ -234,8 +235,8 @@ class ConnectRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i7.GettingStartedPage]
|
||||
class GettingStartedRoute extends _i40.PageRouteInfo<void> {
|
||||
const GettingStartedRoute({List<_i40.PageRouteInfo>? children})
|
||||
class GettingStartedRoute extends _i41.PageRouteInfo<void> {
|
||||
const GettingStartedRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
GettingStartedRoute.name,
|
||||
initialChildren: children,
|
||||
@ -243,7 +244,7 @@ class GettingStartedRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'GettingStartedRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i7.GettingStartedPage();
|
||||
@ -254,12 +255,12 @@ class GettingStartedRoute extends _i40.PageRouteInfo<void> {
|
||||
/// generated route for
|
||||
/// [_i8.HomeBrowseSectionItemsPage]
|
||||
class HomeBrowseSectionItemsRoute
|
||||
extends _i40.PageRouteInfo<HomeBrowseSectionItemsRouteArgs> {
|
||||
extends _i41.PageRouteInfo<HomeBrowseSectionItemsRouteArgs> {
|
||||
HomeBrowseSectionItemsRoute({
|
||||
_i43.Key? key,
|
||||
_i44.Key? key,
|
||||
required String sectionId,
|
||||
required _i42.SpotubeBrowseSectionObject<Object> section,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
required _i43.SpotubeBrowseSectionObject<Object> section,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
HomeBrowseSectionItemsRoute.name,
|
||||
args: HomeBrowseSectionItemsRouteArgs(
|
||||
@ -273,7 +274,7 @@ class HomeBrowseSectionItemsRoute
|
||||
|
||||
static const String name = 'HomeBrowseSectionItemsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<HomeBrowseSectionItemsRouteArgs>();
|
||||
@ -293,11 +294,11 @@ class HomeBrowseSectionItemsRouteArgs {
|
||||
required this.section,
|
||||
});
|
||||
|
||||
final _i43.Key? key;
|
||||
final _i44.Key? key;
|
||||
|
||||
final String sectionId;
|
||||
|
||||
final _i42.SpotubeBrowseSectionObject<Object> section;
|
||||
final _i43.SpotubeBrowseSectionObject<Object> section;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -307,8 +308,8 @@ class HomeBrowseSectionItemsRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i9.HomePage]
|
||||
class HomeRoute extends _i40.PageRouteInfo<void> {
|
||||
const HomeRoute({List<_i40.PageRouteInfo>? children})
|
||||
class HomeRoute extends _i41.PageRouteInfo<void> {
|
||||
const HomeRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
HomeRoute.name,
|
||||
initialChildren: children,
|
||||
@ -316,7 +317,7 @@ class HomeRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'HomeRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i9.HomePage();
|
||||
@ -326,8 +327,8 @@ class HomeRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i10.LastFMLoginPage]
|
||||
class LastFMLoginRoute extends _i40.PageRouteInfo<void> {
|
||||
const LastFMLoginRoute({List<_i40.PageRouteInfo>? children})
|
||||
class LastFMLoginRoute extends _i41.PageRouteInfo<void> {
|
||||
const LastFMLoginRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
LastFMLoginRoute.name,
|
||||
initialChildren: children,
|
||||
@ -335,7 +336,7 @@ class LastFMLoginRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'LastFMLoginRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i10.LastFMLoginPage();
|
||||
@ -345,8 +346,8 @@ class LastFMLoginRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i11.LibraryPage]
|
||||
class LibraryRoute extends _i40.PageRouteInfo<void> {
|
||||
const LibraryRoute({List<_i40.PageRouteInfo>? children})
|
||||
class LibraryRoute extends _i41.PageRouteInfo<void> {
|
||||
const LibraryRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
LibraryRoute.name,
|
||||
initialChildren: children,
|
||||
@ -354,7 +355,7 @@ class LibraryRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'LibraryRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i11.LibraryPage();
|
||||
@ -364,11 +365,11 @@ class LibraryRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i12.LikedPlaylistPage]
|
||||
class LikedPlaylistRoute extends _i40.PageRouteInfo<LikedPlaylistRouteArgs> {
|
||||
class LikedPlaylistRoute extends _i41.PageRouteInfo<LikedPlaylistRouteArgs> {
|
||||
LikedPlaylistRoute({
|
||||
_i41.Key? key,
|
||||
required _i42.SpotubeSimplePlaylistObject playlist,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
_i42.Key? key,
|
||||
required _i43.SpotubeSimplePlaylistObject playlist,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
LikedPlaylistRoute.name,
|
||||
args: LikedPlaylistRouteArgs(
|
||||
@ -380,7 +381,7 @@ class LikedPlaylistRoute extends _i40.PageRouteInfo<LikedPlaylistRouteArgs> {
|
||||
|
||||
static const String name = 'LikedPlaylistRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<LikedPlaylistRouteArgs>();
|
||||
@ -398,9 +399,9 @@ class LikedPlaylistRouteArgs {
|
||||
required this.playlist,
|
||||
});
|
||||
|
||||
final _i41.Key? key;
|
||||
final _i42.Key? key;
|
||||
|
||||
final _i42.SpotubeSimplePlaylistObject playlist;
|
||||
final _i43.SpotubeSimplePlaylistObject playlist;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -410,13 +411,13 @@ class LikedPlaylistRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i13.LocalLibraryPage]
|
||||
class LocalLibraryRoute extends _i40.PageRouteInfo<LocalLibraryRouteArgs> {
|
||||
class LocalLibraryRoute extends _i41.PageRouteInfo<LocalLibraryRouteArgs> {
|
||||
LocalLibraryRoute({
|
||||
required String location,
|
||||
_i41.Key? key,
|
||||
_i42.Key? key,
|
||||
bool isDownloads = false,
|
||||
bool isCache = false,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
LocalLibraryRoute.name,
|
||||
args: LocalLibraryRouteArgs(
|
||||
@ -430,7 +431,7 @@ class LocalLibraryRoute extends _i40.PageRouteInfo<LocalLibraryRouteArgs> {
|
||||
|
||||
static const String name = 'LocalLibraryRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<LocalLibraryRouteArgs>();
|
||||
@ -454,7 +455,7 @@ class LocalLibraryRouteArgs {
|
||||
|
||||
final String location;
|
||||
|
||||
final _i41.Key? key;
|
||||
final _i42.Key? key;
|
||||
|
||||
final bool isDownloads;
|
||||
|
||||
@ -468,8 +469,8 @@ class LocalLibraryRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i14.LogsPage]
|
||||
class LogsRoute extends _i40.PageRouteInfo<void> {
|
||||
const LogsRoute({List<_i40.PageRouteInfo>? children})
|
||||
class LogsRoute extends _i41.PageRouteInfo<void> {
|
||||
const LogsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
LogsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -477,7 +478,7 @@ class LogsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'LogsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i14.LogsPage();
|
||||
@ -487,8 +488,8 @@ class LogsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i15.LyricsPage]
|
||||
class LyricsRoute extends _i40.PageRouteInfo<void> {
|
||||
const LyricsRoute({List<_i40.PageRouteInfo>? children})
|
||||
class LyricsRoute extends _i41.PageRouteInfo<void> {
|
||||
const LyricsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
LyricsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -496,7 +497,7 @@ class LyricsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'LyricsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i15.LyricsPage();
|
||||
@ -506,11 +507,11 @@ class LyricsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i16.MiniLyricsPage]
|
||||
class MiniLyricsRoute extends _i40.PageRouteInfo<MiniLyricsRouteArgs> {
|
||||
class MiniLyricsRoute extends _i41.PageRouteInfo<MiniLyricsRouteArgs> {
|
||||
MiniLyricsRoute({
|
||||
_i43.Key? key,
|
||||
required _i43.Size prevSize,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
_i44.Key? key,
|
||||
required _i44.Size prevSize,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
MiniLyricsRoute.name,
|
||||
args: MiniLyricsRouteArgs(
|
||||
@ -522,7 +523,7 @@ class MiniLyricsRoute extends _i40.PageRouteInfo<MiniLyricsRouteArgs> {
|
||||
|
||||
static const String name = 'MiniLyricsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<MiniLyricsRouteArgs>();
|
||||
@ -540,9 +541,9 @@ class MiniLyricsRouteArgs {
|
||||
required this.prevSize,
|
||||
});
|
||||
|
||||
final _i43.Key? key;
|
||||
final _i44.Key? key;
|
||||
|
||||
final _i43.Size prevSize;
|
||||
final _i44.Size prevSize;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -552,8 +553,8 @@ class MiniLyricsRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i17.PlayerLyricsPage]
|
||||
class PlayerLyricsRoute extends _i40.PageRouteInfo<void> {
|
||||
const PlayerLyricsRoute({List<_i40.PageRouteInfo>? children})
|
||||
class PlayerLyricsRoute extends _i41.PageRouteInfo<void> {
|
||||
const PlayerLyricsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
PlayerLyricsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -561,7 +562,7 @@ class PlayerLyricsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'PlayerLyricsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i17.PlayerLyricsPage();
|
||||
@ -571,8 +572,8 @@ class PlayerLyricsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i18.PlayerQueuePage]
|
||||
class PlayerQueueRoute extends _i40.PageRouteInfo<void> {
|
||||
const PlayerQueueRoute({List<_i40.PageRouteInfo>? children})
|
||||
class PlayerQueueRoute extends _i41.PageRouteInfo<void> {
|
||||
const PlayerQueueRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
PlayerQueueRoute.name,
|
||||
initialChildren: children,
|
||||
@ -580,7 +581,7 @@ class PlayerQueueRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'PlayerQueueRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i18.PlayerQueuePage();
|
||||
@ -590,8 +591,8 @@ class PlayerQueueRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i19.PlayerTrackSourcesPage]
|
||||
class PlayerTrackSourcesRoute extends _i40.PageRouteInfo<void> {
|
||||
const PlayerTrackSourcesRoute({List<_i40.PageRouteInfo>? children})
|
||||
class PlayerTrackSourcesRoute extends _i41.PageRouteInfo<void> {
|
||||
const PlayerTrackSourcesRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
PlayerTrackSourcesRoute.name,
|
||||
initialChildren: children,
|
||||
@ -599,7 +600,7 @@ class PlayerTrackSourcesRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'PlayerTrackSourcesRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i19.PlayerTrackSourcesPage();
|
||||
@ -609,12 +610,12 @@ class PlayerTrackSourcesRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i20.PlaylistPage]
|
||||
class PlaylistRoute extends _i40.PageRouteInfo<PlaylistRouteArgs> {
|
||||
class PlaylistRoute extends _i41.PageRouteInfo<PlaylistRouteArgs> {
|
||||
PlaylistRoute({
|
||||
_i41.Key? key,
|
||||
_i42.Key? key,
|
||||
required String id,
|
||||
required _i42.SpotubeSimplePlaylistObject playlist,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
required _i43.SpotubeSimplePlaylistObject playlist,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PlaylistRoute.name,
|
||||
args: PlaylistRouteArgs(
|
||||
@ -628,7 +629,7 @@ class PlaylistRoute extends _i40.PageRouteInfo<PlaylistRouteArgs> {
|
||||
|
||||
static const String name = 'PlaylistRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<PlaylistRouteArgs>();
|
||||
@ -648,11 +649,11 @@ class PlaylistRouteArgs {
|
||||
required this.playlist,
|
||||
});
|
||||
|
||||
final _i41.Key? key;
|
||||
final _i42.Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
final _i42.SpotubeSimplePlaylistObject playlist;
|
||||
final _i43.SpotubeSimplePlaylistObject playlist;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -662,8 +663,8 @@ class PlaylistRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i21.ProfilePage]
|
||||
class ProfileRoute extends _i40.PageRouteInfo<void> {
|
||||
const ProfileRoute({List<_i40.PageRouteInfo>? children})
|
||||
class ProfileRoute extends _i41.PageRouteInfo<void> {
|
||||
const ProfileRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
ProfileRoute.name,
|
||||
initialChildren: children,
|
||||
@ -671,7 +672,7 @@ class ProfileRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'ProfileRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i21.ProfilePage();
|
||||
@ -681,8 +682,8 @@ class ProfileRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i22.RootAppPage]
|
||||
class RootAppRoute extends _i40.PageRouteInfo<void> {
|
||||
const RootAppRoute({List<_i40.PageRouteInfo>? children})
|
||||
class RootAppRoute extends _i41.PageRouteInfo<void> {
|
||||
const RootAppRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
RootAppRoute.name,
|
||||
initialChildren: children,
|
||||
@ -690,7 +691,7 @@ class RootAppRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'RootAppRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i22.RootAppPage();
|
||||
@ -700,8 +701,8 @@ class RootAppRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i23.SearchPage]
|
||||
class SearchRoute extends _i40.PageRouteInfo<void> {
|
||||
const SearchRoute({List<_i40.PageRouteInfo>? children})
|
||||
class SearchRoute extends _i41.PageRouteInfo<void> {
|
||||
const SearchRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
SearchRoute.name,
|
||||
initialChildren: children,
|
||||
@ -709,7 +710,7 @@ class SearchRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'SearchRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i23.SearchPage();
|
||||
@ -720,12 +721,12 @@ class SearchRoute extends _i40.PageRouteInfo<void> {
|
||||
/// generated route for
|
||||
/// [_i24.SettingsMetadataProviderFormPage]
|
||||
class SettingsMetadataProviderFormRoute
|
||||
extends _i40.PageRouteInfo<SettingsMetadataProviderFormRouteArgs> {
|
||||
extends _i41.PageRouteInfo<SettingsMetadataProviderFormRouteArgs> {
|
||||
SettingsMetadataProviderFormRoute({
|
||||
_i43.Key? key,
|
||||
_i44.Key? key,
|
||||
required String title,
|
||||
required List<_i42.MetadataFormFieldObject> fields,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
required List<_i43.MetadataFormFieldObject> fields,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
SettingsMetadataProviderFormRoute.name,
|
||||
args: SettingsMetadataProviderFormRouteArgs(
|
||||
@ -738,7 +739,7 @@ class SettingsMetadataProviderFormRoute
|
||||
|
||||
static const String name = 'SettingsMetadataProviderFormRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<SettingsMetadataProviderFormRouteArgs>();
|
||||
@ -758,11 +759,11 @@ class SettingsMetadataProviderFormRouteArgs {
|
||||
required this.fields,
|
||||
});
|
||||
|
||||
final _i43.Key? key;
|
||||
final _i44.Key? key;
|
||||
|
||||
final String title;
|
||||
|
||||
final List<_i42.MetadataFormFieldObject> fields;
|
||||
final List<_i43.MetadataFormFieldObject> fields;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -772,8 +773,8 @@ class SettingsMetadataProviderFormRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i25.SettingsMetadataProviderPage]
|
||||
class SettingsMetadataProviderRoute extends _i40.PageRouteInfo<void> {
|
||||
const SettingsMetadataProviderRoute({List<_i40.PageRouteInfo>? children})
|
||||
class SettingsMetadataProviderRoute extends _i41.PageRouteInfo<void> {
|
||||
const SettingsMetadataProviderRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsMetadataProviderRoute.name,
|
||||
initialChildren: children,
|
||||
@ -781,7 +782,7 @@ class SettingsMetadataProviderRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'SettingsMetadataProviderRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i25.SettingsMetadataProviderPage();
|
||||
@ -791,8 +792,8 @@ class SettingsMetadataProviderRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i26.SettingsPage]
|
||||
class SettingsRoute extends _i40.PageRouteInfo<void> {
|
||||
const SettingsRoute({List<_i40.PageRouteInfo>? children})
|
||||
class SettingsRoute extends _i41.PageRouteInfo<void> {
|
||||
const SettingsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -800,7 +801,7 @@ class SettingsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'SettingsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i26.SettingsPage();
|
||||
@ -809,9 +810,28 @@ class SettingsRoute extends _i40.PageRouteInfo<void> {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i27.StatsAlbumsPage]
|
||||
class StatsAlbumsRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsAlbumsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i27.SettingsScrobblingPage]
|
||||
class SettingsScrobblingRoute extends _i41.PageRouteInfo<void> {
|
||||
const SettingsScrobblingRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsScrobblingRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SettingsScrobblingRoute';
|
||||
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i27.SettingsScrobblingPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i28.StatsAlbumsPage]
|
||||
class StatsAlbumsRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsAlbumsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsAlbumsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -819,18 +839,18 @@ class StatsAlbumsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsAlbumsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i27.StatsAlbumsPage();
|
||||
return const _i28.StatsAlbumsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i28.StatsArtistsPage]
|
||||
class StatsArtistsRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsArtistsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i29.StatsArtistsPage]
|
||||
class StatsArtistsRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsArtistsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsArtistsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -838,18 +858,18 @@ class StatsArtistsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsArtistsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i28.StatsArtistsPage();
|
||||
return const _i29.StatsArtistsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i29.StatsMinutesPage]
|
||||
class StatsMinutesRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsMinutesRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i30.StatsMinutesPage]
|
||||
class StatsMinutesRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsMinutesRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsMinutesRoute.name,
|
||||
initialChildren: children,
|
||||
@ -857,18 +877,18 @@ class StatsMinutesRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsMinutesRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i29.StatsMinutesPage();
|
||||
return const _i30.StatsMinutesPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i30.StatsPage]
|
||||
class StatsRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i31.StatsPage]
|
||||
class StatsRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -876,18 +896,18 @@ class StatsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i30.StatsPage();
|
||||
return const _i31.StatsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i31.StatsPlaylistsPage]
|
||||
class StatsPlaylistsRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsPlaylistsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i32.StatsPlaylistsPage]
|
||||
class StatsPlaylistsRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsPlaylistsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsPlaylistsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -895,18 +915,18 @@ class StatsPlaylistsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsPlaylistsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i31.StatsPlaylistsPage();
|
||||
return const _i32.StatsPlaylistsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i32.StatsStreamFeesPage]
|
||||
class StatsStreamFeesRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsStreamFeesRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i33.StatsStreamFeesPage]
|
||||
class StatsStreamFeesRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsStreamFeesRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsStreamFeesRoute.name,
|
||||
initialChildren: children,
|
||||
@ -914,18 +934,18 @@ class StatsStreamFeesRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsStreamFeesRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i32.StatsStreamFeesPage();
|
||||
return const _i33.StatsStreamFeesPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i33.StatsStreamsPage]
|
||||
class StatsStreamsRoute extends _i40.PageRouteInfo<void> {
|
||||
const StatsStreamsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i34.StatsStreamsPage]
|
||||
class StatsStreamsRoute extends _i41.PageRouteInfo<void> {
|
||||
const StatsStreamsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
StatsStreamsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -933,21 +953,21 @@ class StatsStreamsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'StatsStreamsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i33.StatsStreamsPage();
|
||||
return const _i34.StatsStreamsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i34.TrackPage]
|
||||
class TrackRoute extends _i40.PageRouteInfo<TrackRouteArgs> {
|
||||
/// [_i35.TrackPage]
|
||||
class TrackRoute extends _i41.PageRouteInfo<TrackRouteArgs> {
|
||||
TrackRoute({
|
||||
_i43.Key? key,
|
||||
_i44.Key? key,
|
||||
required String trackId,
|
||||
List<_i40.PageRouteInfo>? children,
|
||||
List<_i41.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
TrackRoute.name,
|
||||
args: TrackRouteArgs(
|
||||
@ -960,13 +980,13 @@ class TrackRoute extends _i40.PageRouteInfo<TrackRouteArgs> {
|
||||
|
||||
static const String name = 'TrackRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
final args = data.argsAs<TrackRouteArgs>(
|
||||
orElse: () => TrackRouteArgs(trackId: pathParams.getString('id')));
|
||||
return _i34.TrackPage(
|
||||
return _i35.TrackPage(
|
||||
key: args.key,
|
||||
trackId: args.trackId,
|
||||
);
|
||||
@ -980,7 +1000,7 @@ class TrackRouteArgs {
|
||||
required this.trackId,
|
||||
});
|
||||
|
||||
final _i43.Key? key;
|
||||
final _i44.Key? key;
|
||||
|
||||
final String trackId;
|
||||
|
||||
@ -991,9 +1011,9 @@ class TrackRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i35.UserAlbumsPage]
|
||||
class UserAlbumsRoute extends _i40.PageRouteInfo<void> {
|
||||
const UserAlbumsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i36.UserAlbumsPage]
|
||||
class UserAlbumsRoute extends _i41.PageRouteInfo<void> {
|
||||
const UserAlbumsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
UserAlbumsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -1001,18 +1021,18 @@ class UserAlbumsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'UserAlbumsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i35.UserAlbumsPage();
|
||||
return const _i36.UserAlbumsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i36.UserArtistsPage]
|
||||
class UserArtistsRoute extends _i40.PageRouteInfo<void> {
|
||||
const UserArtistsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i37.UserArtistsPage]
|
||||
class UserArtistsRoute extends _i41.PageRouteInfo<void> {
|
||||
const UserArtistsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
UserArtistsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -1020,18 +1040,18 @@ class UserArtistsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'UserArtistsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i36.UserArtistsPage();
|
||||
return const _i37.UserArtistsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i37.UserDownloadsPage]
|
||||
class UserDownloadsRoute extends _i40.PageRouteInfo<void> {
|
||||
const UserDownloadsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i38.UserDownloadsPage]
|
||||
class UserDownloadsRoute extends _i41.PageRouteInfo<void> {
|
||||
const UserDownloadsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
UserDownloadsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -1039,18 +1059,18 @@ class UserDownloadsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'UserDownloadsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i37.UserDownloadsPage();
|
||||
return const _i38.UserDownloadsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i38.UserLocalLibraryPage]
|
||||
class UserLocalLibraryRoute extends _i40.PageRouteInfo<void> {
|
||||
const UserLocalLibraryRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i39.UserLocalLibraryPage]
|
||||
class UserLocalLibraryRoute extends _i41.PageRouteInfo<void> {
|
||||
const UserLocalLibraryRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
UserLocalLibraryRoute.name,
|
||||
initialChildren: children,
|
||||
@ -1058,18 +1078,18 @@ class UserLocalLibraryRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'UserLocalLibraryRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i38.UserLocalLibraryPage();
|
||||
return const _i39.UserLocalLibraryPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i39.UserPlaylistsPage]
|
||||
class UserPlaylistsRoute extends _i40.PageRouteInfo<void> {
|
||||
const UserPlaylistsRoute({List<_i40.PageRouteInfo>? children})
|
||||
/// [_i40.UserPlaylistsPage]
|
||||
class UserPlaylistsRoute extends _i41.PageRouteInfo<void> {
|
||||
const UserPlaylistsRoute({List<_i41.PageRouteInfo>? children})
|
||||
: super(
|
||||
UserPlaylistsRoute.name,
|
||||
initialChildren: children,
|
||||
@ -1077,10 +1097,10 @@ class UserPlaylistsRoute extends _i40.PageRouteInfo<void> {
|
||||
|
||||
static const String name = 'UserPlaylistsRoute';
|
||||
|
||||
static _i40.PageInfo page = _i40.PageInfo(
|
||||
static _i41.PageInfo page = _i41.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i39.UserPlaylistsPage();
|
||||
return const _i40.UserPlaylistsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
137
lib/components/fallbacks/error_box.dart
Normal file
137
lib/components/fallbacks/error_box.dart
Normal file
@ -0,0 +1,137 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
|
||||
class ErrorBox extends StatelessWidget {
|
||||
final Object error;
|
||||
final VoidCallback? onRetry;
|
||||
const ErrorBox({
|
||||
super.key,
|
||||
required this.error,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Make a monospace error log view. Make sure it's only 4 lines
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Basic(
|
||||
leading: Icon(SpotubeIcons.error),
|
||||
contentSpacing: 8,
|
||||
title: Text("An error occurred"),
|
||||
),
|
||||
Card(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
filled: true,
|
||||
fillColor: context.theme.colorScheme.muted,
|
||||
child: Text(
|
||||
error.toString(),
|
||||
style: TextStyle(
|
||||
// Use monospace
|
||||
fontFamily: 'Ubuntu Mono',
|
||||
color: context.theme.colorScheme.mutedForeground,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 6,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// Show a dialog with full log and a retry button as well
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Button.text(
|
||||
leading: const Icon(SpotubeIcons.logs),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 480,
|
||||
maxHeight:
|
||||
MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
child: AlertDialog(
|
||||
padding: const EdgeInsets.all(12),
|
||||
title: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.logs),
|
||||
const Text("Logs"),
|
||||
const Spacer(),
|
||||
IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.close),
|
||||
onPressed: () => context.maybePop(),
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
HookBuilder(builder: (context) {
|
||||
final copied = useState(false);
|
||||
|
||||
return Button.ghost(
|
||||
leading: copied.value
|
||||
? const Icon(SpotubeIcons.done)
|
||||
: const Icon(SpotubeIcons.clipboard),
|
||||
child: const Text("Copy to clipboard"),
|
||||
onPressed: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: error.toString()),
|
||||
);
|
||||
copied.value = true;
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
content: SingleChildScrollView(
|
||||
child: Card(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
filled: true,
|
||||
fillColor: context.theme.colorScheme.muted,
|
||||
child: SelectableText(
|
||||
error.toString(),
|
||||
style: TextStyle(
|
||||
// Use monospace
|
||||
fontFamily: 'Ubuntu Mono',
|
||||
color: context
|
||||
.theme.colorScheme.mutedForeground,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text("View logs"),
|
||||
),
|
||||
if (onRetry != null)
|
||||
Button.text(
|
||||
leading: const Icon(SpotubeIcons.refresh),
|
||||
onPressed: onRetry,
|
||||
child: const Text("Retry"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
41
lib/components/fallbacks/no_default_metadata_plugin.dart
Normal file
41
lib/components/fallbacks/no_default_metadata_plugin.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter_undraw/flutter_undraw.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
|
||||
class NoDefaultMetadataPlugin extends StatelessWidget {
|
||||
const NoDefaultMetadataPlugin({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Undraw(
|
||||
height: 200 * context.theme.scaling,
|
||||
illustration: UndrawIllustration.stars,
|
||||
color: context.theme.colorScheme.primary,
|
||||
),
|
||||
AutoSizeText(
|
||||
"You've no default metadata provider set",
|
||||
style: context.theme.typography.h4,
|
||||
maxLines: 1,
|
||||
),
|
||||
Button.primary(
|
||||
leading: const Icon(SpotubeIcons.extensions),
|
||||
child: const Text("Manage metadata providers"),
|
||||
onPressed: () {
|
||||
context.pushRoute(const SettingsMetadataProviderRoute());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ class TextFormBuilderField extends StatelessWidget {
|
||||
// final AlignmentGeometry? placeholderAlignment;
|
||||
// final AlignmentGeometry? leadingAlignment;
|
||||
// final AlignmentGeometry? trailingAlignment;
|
||||
final bool border;
|
||||
final Border? border;
|
||||
final List<InputFeature> features;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
@ -61,7 +61,7 @@ class TextFormBuilderField extends StatelessWidget {
|
||||
this.minLines,
|
||||
this.filled = false,
|
||||
this.placeholder,
|
||||
this.border = true,
|
||||
this.border,
|
||||
this.padding,
|
||||
this.onSubmitted,
|
||||
this.onEditingComplete,
|
||||
|
@ -217,7 +217,7 @@ class Spotube extends HookConsumerWidget {
|
||||
iconTheme: const IconThemeProperties(),
|
||||
colorScheme:
|
||||
colorSchemeMap[accentMaterialColor.name]?.call(ThemeMode.light) ??
|
||||
ColorSchemes.lightOrange(),
|
||||
LegacyColorSchemes.lightOrange(),
|
||||
surfaceOpacity: .8,
|
||||
surfaceBlur: 10,
|
||||
),
|
||||
@ -226,7 +226,7 @@ class Spotube extends HookConsumerWidget {
|
||||
iconTheme: const IconThemeProperties(),
|
||||
colorScheme:
|
||||
colorSchemeMap[accentMaterialColor.name]?.call(ThemeMode.dark) ??
|
||||
ColorSchemes.darkOrange(),
|
||||
LegacyColorSchemes.darkOrange(),
|
||||
surfaceOpacity: .8,
|
||||
surfaceBlur: 10,
|
||||
),
|
||||
|
@ -2,10 +2,13 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
|
||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/browse/sections.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
import 'package:flutter_undraw/flutter_undraw.dart';
|
||||
|
||||
@ -44,6 +47,29 @@ class HomePageBrowseSection extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (browseSections.error
|
||||
case MetadataPluginException(
|
||||
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||
message: _,
|
||||
)) {
|
||||
return const SliverFillRemaining(
|
||||
child: Center(child: NoDefaultMetadataPlugin()),
|
||||
);
|
||||
}
|
||||
|
||||
if (browseSections.hasError) {
|
||||
return SliverFillRemaining(
|
||||
child: Center(
|
||||
child: ErrorBox(
|
||||
error: browseSections.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginBrowseSectionsProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverInfiniteList(
|
||||
hasReachedMax: browseSections.asData?.value.hasMore == false,
|
||||
isLoading: !browseSections.isLoading && browseSections.isLoadingNextPage,
|
||||
|
@ -80,6 +80,9 @@ class Sidebar extends HookConsumerWidget {
|
||||
),
|
||||
for (final tile in sidebarTileList)
|
||||
NavigationButton(
|
||||
style: router.currentPath.startsWith(tile.pathPrefix)
|
||||
? const ButtonStyle.secondary()
|
||||
: null,
|
||||
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
|
||||
child: Tooltip(
|
||||
tooltip: TooltipContainer(child: Text(tile.title)).call,
|
||||
@ -94,6 +97,9 @@ class Sidebar extends HookConsumerWidget {
|
||||
NavigationLabel(child: Text(context.l10n.library)),
|
||||
for (final tile in sidebarLibraryTileList)
|
||||
NavigationButton(
|
||||
style: router.currentPath.startsWith(tile.pathPrefix)
|
||||
? const ButtonStyle.secondary()
|
||||
: null,
|
||||
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
|
||||
onPressed: () {
|
||||
context.navigateTo(tile.route);
|
||||
|
@ -41,18 +41,18 @@ final Set<SpotubeColor> colorsMap = {
|
||||
};
|
||||
|
||||
final colorSchemeMap = {
|
||||
"slate": ColorSchemes.slate,
|
||||
"gray": ColorSchemes.gray,
|
||||
"zinc": ColorSchemes.zinc,
|
||||
"neutral": ColorSchemes.neutral,
|
||||
"stone": ColorSchemes.stone,
|
||||
"red": ColorSchemes.red,
|
||||
"orange": ColorSchemes.orange,
|
||||
"yellow": ColorSchemes.yellow,
|
||||
"green": ColorSchemes.green,
|
||||
"blue": ColorSchemes.blue,
|
||||
"violet": ColorSchemes.violet,
|
||||
"rose": ColorSchemes.rose,
|
||||
"slate": LegacyColorSchemes.slate,
|
||||
"gray": LegacyColorSchemes.gray,
|
||||
"zinc": LegacyColorSchemes.zinc,
|
||||
"neutral": LegacyColorSchemes.neutral,
|
||||
"stone": LegacyColorSchemes.stone,
|
||||
"red": LegacyColorSchemes.red,
|
||||
"orange": LegacyColorSchemes.orange,
|
||||
"yellow": LegacyColorSchemes.yellow,
|
||||
"green": LegacyColorSchemes.green,
|
||||
"blue": LegacyColorSchemes.blue,
|
||||
"violet": LegacyColorSchemes.violet,
|
||||
"rose": LegacyColorSchemes.rose,
|
||||
};
|
||||
|
||||
class ColorSchemePickerDialog extends HookConsumerWidget {
|
||||
|
@ -104,15 +104,15 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Button.secondary(
|
||||
leading: const Icon(SpotubeIcons.anonymous),
|
||||
Button.primary(
|
||||
leading: const Icon(SpotubeIcons.extensions),
|
||||
onPressed: () async {
|
||||
await KVStoreService.setDoneGettingStarted(true);
|
||||
if (context.mounted) {
|
||||
context.navigateTo(const HomeRoute());
|
||||
context.pushRoute(const SettingsMetadataProviderRoute());
|
||||
}
|
||||
},
|
||||
child: Text(context.l10n.browse_anonymously),
|
||||
child: const Text("Install a Metadata Provider"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -9,6 +9,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/modules/album/album_card.dart';
|
||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
@ -17,6 +19,7 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/albums.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class UserAlbumsPage extends HookConsumerWidget {
|
||||
@ -50,10 +53,27 @@ class UserAlbumsPage extends HookConsumerWidget {
|
||||
[];
|
||||
}, [albumsQuery.asData?.value, searchText.value]);
|
||||
|
||||
if (albumsQuery.error
|
||||
case MetadataPluginException(
|
||||
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||
message: _,
|
||||
)) {
|
||||
return const Center(child: NoDefaultMetadataPlugin());
|
||||
}
|
||||
|
||||
if (authenticated.asData?.value != true) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
|
||||
if (albumsQuery.hasError) {
|
||||
return ErrorBox(
|
||||
error: albumsQuery.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSavedAlbumsProvider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
|
@ -12,6 +12,8 @@ import 'package:spotube/collections/fake.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
|
||||
import 'package:spotube/modules/artist/artist_card.dart';
|
||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
@ -20,6 +22,7 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class UserArtistsPage extends HookConsumerWidget {
|
||||
@ -55,10 +58,27 @@ class UserArtistsPage extends HookConsumerWidget {
|
||||
|
||||
final controller = useScrollController();
|
||||
|
||||
if (artistQuery.error
|
||||
case MetadataPluginException(
|
||||
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||
message: _,
|
||||
)) {
|
||||
return const Center(child: NoDefaultMetadataPlugin());
|
||||
}
|
||||
|
||||
if (authenticated.asData?.value != true) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
|
||||
if (artistQuery.hasError) {
|
||||
return ErrorBox(
|
||||
error: artistQuery.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSavedArtistsProvider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
|
@ -136,8 +136,10 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.delete),
|
||||
Text(context.l10n.clear_cache)
|
||||
const Expanded(child: Icon(SpotubeIcons.delete)),
|
||||
Text(
|
||||
context.l10n.clear_cache,
|
||||
)
|
||||
],
|
||||
).xSmall().iconSmall(),
|
||||
onPressed: () async {
|
||||
@ -178,7 +180,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.export),
|
||||
const Expanded(child: Icon(SpotubeIcons.export)),
|
||||
Text(
|
||||
context.l10n.export,
|
||||
)
|
||||
|
@ -8,6 +8,8 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||
@ -19,6 +21,7 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class UserPlaylistsPage extends HookConsumerWidget {
|
||||
@ -78,10 +81,27 @@ class UserPlaylistsPage extends HookConsumerWidget {
|
||||
|
||||
final controller = useScrollController();
|
||||
|
||||
if (playlistsQuery.error
|
||||
case MetadataPluginException(
|
||||
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||
message: _,
|
||||
)) {
|
||||
return const Center(child: NoDefaultMetadataPlugin());
|
||||
}
|
||||
|
||||
if (authenticated.asData?.value != true) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
|
||||
if (playlistsQuery.hasError) {
|
||||
return ErrorBox(
|
||||
error: playlistsQuery.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSavedPlaylistsProvider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return material.RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(metadataPluginSavedPlaylistsProvider);
|
||||
|
@ -7,7 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/fallbacks/no_default_metadata_plugin.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/string.dart';
|
||||
@ -17,10 +18,10 @@ import 'package:spotube/pages/search/tabs/all.dart';
|
||||
import 'package:spotube/pages/search/tabs/artists.dart';
|
||||
import 'package:spotube/pages/search/tabs/playlists.dart';
|
||||
import 'package:spotube/pages/search/tabs/tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/search/all.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
final searchTermStateProvider = StateProvider<String>((ref) {
|
||||
return "";
|
||||
@ -37,8 +38,6 @@ class SearchPage extends HookConsumerWidget {
|
||||
final controller = useShadcnTextEditingController();
|
||||
final focusNode = useFocusNode();
|
||||
|
||||
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||
|
||||
final searchTerm = ref.watch(searchTermStateProvider);
|
||||
final searchChipSnapshot = ref.watch(metadataPluginSearchChipsProvider);
|
||||
final selectedChip = useState<String?>(
|
||||
@ -83,147 +82,163 @@ class SearchPage extends HookConsumerWidget {
|
||||
if (kTitlebarVisible)
|
||||
const TitleBar(automaticallyImplyLeading: false, height: 30)
|
||||
],
|
||||
child: authenticated.asData?.value != true
|
||||
? const AnonymousFallback()
|
||||
: Column(
|
||||
child: Builder(builder: (context) {
|
||||
if (searchChipSnapshot.error
|
||||
case MetadataPluginException(
|
||||
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||
message: _
|
||||
)) {
|
||||
return const NoDefaultMetadataPlugin();
|
||||
}
|
||||
|
||||
if (searchChipSnapshot.hasError) {
|
||||
return ErrorBox(
|
||||
error: searchChipSnapshot.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSearchChipsProvider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, _) {
|
||||
final suggestions = controller.text.isEmpty
|
||||
? KVStoreService.recentSearches
|
||||
: KVStoreService.recentSearches
|
||||
.where(
|
||||
(s) =>
|
||||
weightedRatio(
|
||||
s.toLowerCase(),
|
||||
controller.text.toLowerCase(),
|
||||
) >
|
||||
50,
|
||||
)
|
||||
.toList();
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, _) {
|
||||
final suggestions = controller.text.isEmpty
|
||||
? KVStoreService.recentSearches
|
||||
: KVStoreService.recentSearches
|
||||
.where(
|
||||
(s) =>
|
||||
weightedRatio(
|
||||
s.toLowerCase(),
|
||||
controller.text.toLowerCase(),
|
||||
) >
|
||||
50,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return KeyboardListener(
|
||||
focusNode: focusNode,
|
||||
return KeyboardListener(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: (value) {
|
||||
final isEnter = value.logicalKey ==
|
||||
LogicalKeyboardKey.enter;
|
||||
|
||||
if (isEnter) {
|
||||
onSubmitted(controller.text);
|
||||
focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
child: AutoComplete(
|
||||
suggestions: suggestions.length <= 2
|
||||
? [
|
||||
...suggestions,
|
||||
"Twenty One Pilots",
|
||||
"Linkin Park",
|
||||
"d4vd"
|
||||
]
|
||||
: suggestions,
|
||||
completer: (suggestion) => suggestion,
|
||||
mode: AutoCompleteMode.replaceAll,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
onKeyEvent: (value) {
|
||||
final isEnter = value.logicalKey ==
|
||||
LogicalKeyboardKey.enter;
|
||||
|
||||
if (isEnter) {
|
||||
onSubmitted(controller.text);
|
||||
focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
child: AutoComplete(
|
||||
suggestions: suggestions.length <= 2
|
||||
? [
|
||||
...suggestions,
|
||||
"Twenty One Pilots",
|
||||
"Linkin Park",
|
||||
"d4vd"
|
||||
]
|
||||
: suggestions,
|
||||
completer: (suggestion) => suggestion,
|
||||
mode: AutoCompleteMode.replaceAll,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
features: [
|
||||
const InputFeature.leading(
|
||||
Icon(SpotubeIcons.search),
|
||||
),
|
||||
InputFeature.trailing(
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(
|
||||
milliseconds: 300),
|
||||
crossFadeState: controller
|
||||
.text.isNotEmpty
|
||||
controller: controller,
|
||||
features: [
|
||||
const InputFeature.leading(
|
||||
Icon(SpotubeIcons.search),
|
||||
),
|
||||
InputFeature.trailing(
|
||||
AnimatedCrossFade(
|
||||
duration:
|
||||
const Duration(milliseconds: 300),
|
||||
crossFadeState:
|
||||
controller.text.isNotEmpty
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: IconButton.ghost(
|
||||
size: ButtonSize.small,
|
||||
icon: const Icon(
|
||||
SpotubeIcons.close),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
},
|
||||
),
|
||||
secondChild:
|
||||
const SizedBox.square(
|
||||
dimension: 28),
|
||||
),
|
||||
)
|
||||
],
|
||||
textInputAction: TextInputAction.search,
|
||||
placeholder: Text(context.l10n.search),
|
||||
onSubmitted: onSubmitted,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Gap(12),
|
||||
if (searchChipSnapshot.asData?.value != null)
|
||||
for (final chip in searchChipSnapshot.asData!.value)
|
||||
Chip(
|
||||
style: selectedChip.value == chip
|
||||
? ButtonVariance.primary.copyWith(
|
||||
decoration: (context, states, value) {
|
||||
return ButtonVariance.primary
|
||||
.decoration(context, states)
|
||||
.copyWithIfBoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ButtonVariance.secondary.copyWith(
|
||||
decoration: (context, states, value) {
|
||||
return ButtonVariance.secondary
|
||||
.decoration(context, states)
|
||||
.copyWithIfBoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Text(chip.capitalize()),
|
||||
onPressed: () {
|
||||
selectedChip.value = chip;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: switch (selectedChip.value) {
|
||||
"tracks" => const SearchPageTracksTab(),
|
||||
"albums" => const SearchPageAlbumsTab(),
|
||||
"artists" => const SearchPageArtistsTab(),
|
||||
"playlists" => const SearchPagePlaylistsTab(),
|
||||
_ => const SearchPageAllTab(),
|
||||
},
|
||||
firstChild: IconButton.ghost(
|
||||
size: ButtonSize.small,
|
||||
icon:
|
||||
const Icon(SpotubeIcons.close),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
},
|
||||
),
|
||||
secondChild: const SizedBox.square(
|
||||
dimension: 28),
|
||||
),
|
||||
)
|
||||
],
|
||||
textInputAction: TextInputAction.search,
|
||||
placeholder: Text(context.l10n.search),
|
||||
onSubmitted: onSubmitted,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Gap(12),
|
||||
if (searchChipSnapshot.asData?.value != null)
|
||||
for (final chip in searchChipSnapshot.asData!.value)
|
||||
Chip(
|
||||
style: selectedChip.value == chip
|
||||
? ButtonVariance.primary.copyWith(
|
||||
decoration: (context, states, value) {
|
||||
return ButtonVariance.primary
|
||||
.decoration(context, states)
|
||||
.copyWithIfBoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ButtonVariance.secondary.copyWith(
|
||||
decoration: (context, states, value) {
|
||||
return ButtonVariance.secondary
|
||||
.decoration(context, states)
|
||||
.copyWithIfBoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Text(chip.capitalize()),
|
||||
onPressed: () {
|
||||
selectedChip.value = chip;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: switch (selectedChip.value) {
|
||||
"tracks" => const SearchPageTracksTab(),
|
||||
"albums" => const SearchPageAlbumsTab(),
|
||||
"artists" => const SearchPageArtistsTab(),
|
||||
"playlists" => const SearchPagePlaylistsTab(),
|
||||
_ => const SearchPageAllTab(),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/modules/album/album_card.dart';
|
||||
import 'package:spotube/modules/search/loading.dart';
|
||||
@ -23,6 +24,15 @@ class SearchPageAlbumsTab extends HookConsumerWidget {
|
||||
final searchAlbums =
|
||||
searchAlbumsSnapshot.asData?.value.items ?? [FakeData.albumSimple];
|
||||
|
||||
if (searchAlbumsSnapshot.hasError) {
|
||||
return ErrorBox(
|
||||
error: searchAlbumsSnapshot.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSearchAlbumsProvider(searchTerm));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SearchPlaceholder(
|
||||
snapshot: searchAlbumsSnapshot,
|
||||
child: Padding(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/modules/search/loading.dart';
|
||||
import 'package:spotube/pages/search/search.dart';
|
||||
@ -19,6 +20,15 @@ class SearchPageAllTab extends HookConsumerWidget {
|
||||
final searchSnapshot =
|
||||
ref.watch(metadataPluginSearchAllProvider(searchTerm));
|
||||
|
||||
if (searchSnapshot.hasError) {
|
||||
return ErrorBox(
|
||||
error: searchSnapshot.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSearchAllProvider(searchTerm));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SearchPlaceholder(
|
||||
snapshot: searchSnapshot,
|
||||
child: InterScrollbar(
|
||||
|
@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
@ -27,6 +28,15 @@ class SearchPageArtistsTab extends HookConsumerWidget {
|
||||
ref.read(metadataPluginSearchArtistsProvider(searchTerm).notifier);
|
||||
final searchArtists = searchArtistsSnapshot.asData?.value.items ?? [];
|
||||
|
||||
if (searchArtistsSnapshot.hasError) {
|
||||
return ErrorBox(
|
||||
error: searchArtistsSnapshot.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSearchArtistsProvider(searchTerm));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SearchPlaceholder(
|
||||
snapshot: searchArtistsSnapshot,
|
||||
child: AnimatedSwitcher(
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||
import 'package:spotube/modules/search/loading.dart';
|
||||
@ -23,6 +24,15 @@ class SearchPagePlaylistsTab extends HookConsumerWidget {
|
||||
final searchPlaylists = searchPlaylistsSnapshot.asData?.value.items ??
|
||||
[FakeData.playlistSimple];
|
||||
|
||||
if (searchPlaylistsSnapshot.hasError) {
|
||||
return ErrorBox(
|
||||
error: searchPlaylistsSnapshot.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSearchPlaylistsProvider(searchTerm));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SearchPlaceholder(
|
||||
snapshot: searchPlaylistsSnapshot,
|
||||
child: Padding(
|
||||
|
@ -4,6 +4,7 @@ import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||
import 'package:spotube/components/fallbacks/error_box.dart';
|
||||
import 'package:spotube/components/track_tile/track_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/connect/connect.dart';
|
||||
@ -31,6 +32,15 @@ class SearchPageTracksTab extends HookConsumerWidget {
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||
|
||||
if (searchTracksSnapshot.hasError) {
|
||||
return ErrorBox(
|
||||
error: searchTracksSnapshot.error!,
|
||||
onRetry: () {
|
||||
ref.invalidate(metadataPluginSearchTracksProvider(searchTerm));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return SearchPlaceholder(
|
||||
snapshot: searchTracksSnapshot,
|
||||
child: InfiniteList(
|
||||
|
67
lib/pages/settings/scrobbling/scrobbling.dart
Normal file
67
lib/pages/settings/scrobbling/scrobbling.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
show ListTile, ListTileTheme, ListTileThemeData, Material, MaterialType;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SettingsScrobblingPage extends HookConsumerWidget {
|
||||
static const name = "settings_scrobbling";
|
||||
|
||||
const SettingsScrobblingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ListTileTheme(
|
||||
data: ListTileThemeData(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
minVerticalPadding: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: context.theme.borderRadiusLg,
|
||||
side: BorderSide(
|
||||
color: context.theme.colorScheme.border,
|
||||
width: .5,
|
||||
),
|
||||
),
|
||||
textColor: context.theme.colorScheme.foreground,
|
||||
iconColor: context.theme.colorScheme.foreground,
|
||||
selectedColor: context.theme.colorScheme.accent,
|
||||
subtitleTextStyle: context.theme.typography.xSmall,
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
headers: const [TitleBar(title: Text("Scrobbling"))],
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: [
|
||||
Card(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.lastFm, color: Colors.red),
|
||||
title: Text(context.l10n.login_with_lastfm),
|
||||
subtitle: Text(context.l10n.scrobble_to_lastfm),
|
||||
trailing: Button.secondary(
|
||||
leading: const Icon(SpotubeIcons.lastFm),
|
||||
onPressed: () {
|
||||
context.navigateTo(const LastFMLoginRoute());
|
||||
},
|
||||
child: Text(context.l10n.connect),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -31,16 +31,12 @@ class SettingsAccountSection extends HookConsumerWidget {
|
||||
),
|
||||
if (scrobbler.asData?.value == null)
|
||||
ListTile(
|
||||
leading: const Icon(SpotubeIcons.lastFm),
|
||||
title: Text(context.l10n.login_with_lastfm),
|
||||
subtitle: Text(context.l10n.scrobble_to_lastfm),
|
||||
trailing: Button.secondary(
|
||||
leading: const Icon(SpotubeIcons.lastFm),
|
||||
onPressed: () {
|
||||
context.navigateTo(const LastFMLoginRoute());
|
||||
},
|
||||
child: Text(context.l10n.connect),
|
||||
),
|
||||
leading: const Icon(SpotubeIcons.music),
|
||||
title: const Text("Audio scrobblers"),
|
||||
onTap: () {
|
||||
context.pushRoute(const SettingsScrobblingRoute());
|
||||
},
|
||||
trailing: const Icon(SpotubeIcons.angleRight),
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
final metadataPluginAlbumProvider =
|
||||
FutureProvider.autoDispose.family<SpotubeFullAlbumObject, String>(
|
||||
@ -12,9 +12,7 @@ final metadataPluginAlbumProvider =
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No metadata plugin is not set",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.album.getAlbum(id);
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
final metadataPluginArtistProvider =
|
||||
FutureProvider.autoDispose.family<SpotubeFullArtistObject, String>(
|
||||
@ -12,9 +12,7 @@ final metadataPluginArtistProvider =
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No metadata plugin is not set",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.artist.getArtist(artistId);
|
||||
|
@ -4,7 +4,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/tracks/playlist.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
class MetadataPluginSavedPlaylistsNotifier
|
||||
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
|
||||
@ -111,9 +111,7 @@ final metadataPluginIsSavedPlaylistProvider =
|
||||
final plugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (plugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"Failed to get metadata plugin",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
final follows = await plugin.user.isSavedPlaylist(id);
|
||||
|
@ -4,7 +4,7 @@ import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/common.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
|
||||
class MetadataPluginPlaylistNotifier
|
||||
@ -13,9 +13,7 @@ class MetadataPluginPlaylistNotifier
|
||||
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"Metadata plugin is not set",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
final metadataPluginSearchAllProvider =
|
||||
FutureProvider.autoDispose.family<SpotubeSearchResponseObject, String>(
|
||||
@ -9,9 +9,7 @@ final metadataPluginSearchAllProvider =
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No default metadata plugin found",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.search.all(query);
|
||||
@ -22,9 +20,7 @@ final metadataPluginSearchChipsProvider = FutureProvider((ref) async {
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No default metadata plugin found",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
return metadataPlugin.search.chips;
|
||||
});
|
||||
|
@ -1,15 +1,14 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
|
||||
final metadataPluginTrackProvider =
|
||||
FutureProvider.family<SpotubeFullTrackObject, String>((ref, trackId) async {
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No metadata plugin is set as default.");
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
return metadataPlugin.track.getTrack(trackId);
|
||||
|
@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
|
||||
extension PaginationExtension<T> on AsyncValue<T> {
|
||||
@ -20,8 +20,7 @@ mixin MetadataPluginMixin<K>
|
||||
final plugin = await ref.read(metadataPluginProvider.future);
|
||||
|
||||
if (plugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"Metadata plugin is not set");
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
return plugin;
|
||||
|
@ -20,7 +20,7 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
enum TrackOptionValue {
|
||||
@ -97,9 +97,7 @@ class TrackOptionsActions {
|
||||
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"No default metadata plugin set",
|
||||
);
|
||||
throw MetadataPluginException.noDefaultPlugin();
|
||||
}
|
||||
|
||||
final tracks = await metadataPlugin.track.radio(track.id);
|
||||
|
@ -1,12 +0,0 @@
|
||||
class MetadataPluginException implements Exception {
|
||||
final String exceptionType;
|
||||
final String message;
|
||||
|
||||
MetadataPluginException.noDefaultPlugin(this.message)
|
||||
: exceptionType = "NoDefault";
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "${exceptionType}MetadataPluginException: $message";
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ enum MetadataPluginErrorCode {
|
||||
pluginDownloadFailed,
|
||||
duplicatePlugin,
|
||||
pluginByteCodeFileNotFound,
|
||||
noDefaultPlugin,
|
||||
}
|
||||
|
||||
class MetadataPluginException implements Exception {
|
||||
@ -67,6 +68,11 @@ class MetadataPluginException implements Exception {
|
||||
'Plugin byte code file, plugin.out not found. Please ensure the plugin is correctly packaged.',
|
||||
errorCode: MetadataPluginErrorCode.pluginByteCodeFileNotFound,
|
||||
);
|
||||
MetadataPluginException.noDefaultPlugin()
|
||||
: this._(
|
||||
'No default metadata plugin is set. Please set a default plugin in the settings.',
|
||||
errorCode: MetadataPluginErrorCode.noDefaultPlugin,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => 'MetadataPluginException: $message';
|
||||
|
29
pubspec.lock
29
pubspec.lock
@ -515,8 +515,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/desktop_webview_window"
|
||||
ref: "feat/cookies"
|
||||
resolved-ref: f20e433d4a948515b35089d40069f7dd9bced9e4
|
||||
ref: HEAD
|
||||
resolved-ref: f261ff20e310d05713249b21c199a9fe17a3de6f
|
||||
url: "https://github.com/KRTirtho/flutter-plugins.git"
|
||||
source: git
|
||||
version: "0.2.4"
|
||||
@ -762,6 +762,15 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
fk_user_agent:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: "922f9f9eafd8b501da83dca67d56b2887fa8f916"
|
||||
url: "https://github.com/TiffApps/fk_user_agent.git"
|
||||
source: git
|
||||
version: "2.1.1"
|
||||
fluentui_system_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1217,7 +1226,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: c4895250ee45a59c88770f97abebc9e9bbb62259
|
||||
resolved-ref: "52cd25a12c1af6a8819963d222026539e8537586"
|
||||
url: "https://github.com/KRTirtho/hetu_spotube_plugin.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
@ -1226,7 +1235,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "7e9032c054c547f7900c9c9fe4b76e29c8ac1cd1"
|
||||
resolved-ref: "577ad115dce0514afc53e2b3ab7b96bcd88d3be3"
|
||||
url: "https://github.com/hetu-community/hetu_std.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
@ -2011,6 +2020,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
random_user_agents:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: random_user_agents
|
||||
sha256: "19facde509a2482dababb454faf2aceff797a6ae08e80f91268c0c8a7420f03b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.15"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2104,10 +2121,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shadcn_flutter
|
||||
sha256: "979a86e203eb1fb139e8b5c84b49b17b28808804cbff189b43052d56ba6854b5"
|
||||
sha256: "4a4dff36252101f344c77fd659d791c52c808cfe3ab68deea4ea949d942d9e5b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.37"
|
||||
version: "0.0.39"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
18
pubspec.yaml
18
pubspec.yaml
@ -27,7 +27,6 @@ dependencies:
|
||||
desktop_webview_window:
|
||||
git:
|
||||
path: packages/desktop_webview_window
|
||||
ref: feat/cookies
|
||||
url: https://github.com/KRTirtho/flutter-plugins.git
|
||||
device_info_plus: ^11.1.1
|
||||
dio: ^5.4.3+1
|
||||
@ -102,7 +101,7 @@ dependencies:
|
||||
ref: dart-3-support
|
||||
url: https://github.com/KRTirtho/scrobblenaut.git
|
||||
scroll_to_index: ^3.0.1
|
||||
shadcn_flutter: ^0.0.37
|
||||
shadcn_flutter: ^0.0.39
|
||||
shared_preferences: ^2.2.3
|
||||
shelf: ^1.4.1
|
||||
shelf_router: ^1.1.4
|
||||
@ -217,6 +216,7 @@ flutter:
|
||||
- packages/flutter_undraw/assets/undraw/empty.svg
|
||||
- packages/flutter_undraw/assets/undraw/no_data.svg
|
||||
- packages/flutter_undraw/assets/undraw/process.svg
|
||||
- packages/flutter_undraw/assets/undraw/stars.svg
|
||||
# hetu script bytecode
|
||||
- packages/hetu_std/assets/bytecode/std.out
|
||||
- packages/hetu_otp_util/assets/bytecode/otp_util.out
|
||||
@ -233,6 +233,20 @@ flutter:
|
||||
- asset: assets/fonts/Cookie-Regular.ttf
|
||||
style: normal
|
||||
weight: 500
|
||||
- family: Ubuntu Mono
|
||||
fonts:
|
||||
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
|
||||
style: normal
|
||||
weight: 400
|
||||
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
|
||||
style: normal
|
||||
weight: 700
|
||||
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
|
||||
style: italic
|
||||
weight: 400
|
||||
- asset: assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
|
||||
style: italic
|
||||
weight: 700
|
||||
|
||||
flutter_gen:
|
||||
output: lib/collections
|
||||
|
Loading…
Reference in New Issue
Block a user