diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index e0031d17..64ee89d2 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -71,3 +71,10 @@ body:
description: Anything else you'd like to include?
validations:
required: false
+ - type: checkboxes
+ attributes:
+ label: Self grab
+ description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions!
+ options:
+ - label: I'm ready to work on this issue!
+ required: false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/new_feature.yml b/.github/ISSUE_TEMPLATE/new_feature.yml
index 9742f91f..7f02ea38 100644
--- a/.github/ISSUE_TEMPLATE/new_feature.yml
+++ b/.github/ISSUE_TEMPLATE/new_feature.yml
@@ -35,4 +35,11 @@ body:
label: Additional information
description: Anything else you'd like to include?
validations:
- required: false
\ No newline at end of file
+ required: false
+ - type: checkboxes
+ attributes:
+ label: Self grab
+ description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions!
+ options:
+ - label: I'm ready to work on this issue!
+ required: false
\ No newline at end of file
diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index b561bca8..ff305d8b 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -33,7 +33,7 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.10.0
+ - uses: subosito/flutter-action@v2.12.0
with:
cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -89,7 +89,7 @@ jobs:
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Spotube-Release-Binaries
@@ -107,7 +107,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.10.0
+ - uses: subosito/flutter-action@v2.12.0
with:
cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -180,7 +180,7 @@ jobs:
mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Spotube-Release-Binaries
@@ -200,7 +200,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.10.0
+ - uses: subosito/flutter-action@v2.12.0
with:
cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -257,7 +257,7 @@ jobs:
mv build/app/outputs/bundle/${{ inputs.channel }}Release/app-${{ inputs.channel }}-release.aab build/Spotube-playstore-all-arch.aab
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Spotube-Release-Binaries
@@ -276,7 +276,7 @@ jobs:
runs-on: macos-12
steps:
- uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2.10.0
+ - uses: subosito/flutter-action@v2.12.0
with:
cache: true
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -363,7 +363,7 @@ jobs:
ln -sf ./build/ios/iphoneos Payload
zip -r9 Spotube-iOS.ipa Payload/${{ inputs.channel }}.app
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Spotube-Release-Binaries
@@ -386,7 +386,7 @@ jobs:
- macos
- iOS
steps:
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: Spotube-Release-Binaries
path: ./Spotube-Release-Binaries
@@ -401,7 +401,7 @@ jobs:
sha256sum Spotube-Release-Binaries/* >> RELEASE.sha256sum
sed -i 's|Spotube-Release-Binaries/||' RELEASE.sha256sum RELEASE.md5sum
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Spotube-Release-Binaries
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9add0735..7a1e8b9b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,6 +6,12 @@
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
+ },
+ {
+ "name": "spotube (mobile)",
+ "type": "dart",
+ "request": "launch",
+ "program": "lib/main.dart",
"args": [
"--flavor",
"dev"
diff --git a/README.md b/README.md
index 2736d1f1..791d5da0 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,10 @@
An open source, cross-platform Spotify client compatible across multiple platforms
-utilizing Spotify's data API and YouTube (or Piped.video or JioSaavn) as an audio source,
+utilizing Spotify's data API and YouTube, Piped.video or JioSaavn as an audio source,
eliminating the need for Spotify Premium
-Btw it's not another Electron app😉
+Btw it's not just another Electron app 😉
@@ -26,7 +26,7 @@ Btw it's not another Electron app😉
## 🌃 Features
- 🚫 No ads, thanks to the use of public & free Spotify and YT Music APIs¹
-- ⬇️ Downloadable tracks
+- ⬇️ Freely downloadable tracks
- 🖥️ 📱 Cross-platform support
- 🪶 Small size & less data usage
- 🕵️ Anonymous/guest login
@@ -36,17 +36,17 @@ Btw it's not another Electron app😉
- 📖 Open source/libre software
- 🔉 Playback control is done locally, not on the server
-**¹** It is still **recommended** to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify (or purchasing a Spotify Premium subscription too).
+**¹** It is still **recommended** to support creators by engaging with their YouTube channels/Spotify tracks (or preferably by buying their merch/concert tickets/physical media).
### ❌ Unsupported features
-- 🗣️ **Spotify Shows & Podcasts:** Shows and Podcasts can **never be supported** because the audio tracks are _only_ available on Spotify and accessing them would require Spotify Premium.
+- 🗣️ **Spotify Shows & Podcasts:** Shows and Podcasts will **never be supported** because the audio tracks are _only_ available on Spotify and accessing them would require Spotify Premium.
- 🎧 **Spotify Listen Along:** [Coming soon!](https://github.com/KRTirtho/spotube/issues/8)
## 📜 ⬇️ Installation guide
-New releases usually appear after 3-4 months.
-This handy table lists all methods you can use to install Spotube:
+New versions usually release every 3-4 months.
+This handy table lists all the methods you can use to install Spotube:
@@ -304,4 +304,4 @@ If you are concerned, you can [read the reason of choosing this license](https:/
1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.
-© Copyright Spotube 2023
+© Copyright Spotube 2024
diff --git a/assets/jiosaavn.png b/assets/jiosaavn.png
new file mode 100644
index 00000000..4d2d46e4
Binary files /dev/null and b/assets/jiosaavn.png differ
diff --git a/assets/liked-tracks.jpg b/assets/liked-tracks.jpg
new file mode 100644
index 00000000..62dad65e
Binary files /dev/null and b/assets/liked-tracks.jpg differ
diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart
index ac39cf68..2587800e 100644
--- a/lib/collections/assets.gen.dart
+++ b/lib/collections/assets.gen.dart
@@ -34,6 +34,9 @@ class Assets {
AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.png');
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png');
+ static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
+ static const AssetGenImage likedTracks =
+ AssetGenImage('assets/liked-tracks.jpg');
static const AssetGenImage placeholder =
AssetGenImage('assets/placeholder.png');
static const AssetGenImage spotubeHeroBanner =
@@ -74,6 +77,8 @@ class Assets {
bengaliPatternsBg,
branding,
emptyBox,
+ jiosaavn,
+ likedTracks,
placeholder,
spotubeHeroBanner,
spotubeLogoForeground,
diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart
index 10cf2819..8f5f9e8b 100644
--- a/lib/collections/fake.dart
+++ b/lib/collections/fake.dart
@@ -1,5 +1,6 @@
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/track.dart';
+import 'package:spotube/models/spotify_friends.dart';
abstract class FakeData {
static final Image image = Image()
@@ -164,4 +165,35 @@ abstract class FakeData {
..icons = [image]
..id = "1"
..name = "category";
+
+ static final friends = SpotifyFriends(
+ friends: [
+ for (var i = 0; i < 3; i++)
+ SpotifyFriendActivity(
+ user: const SpotifyFriend(
+ name: "name",
+ imageUrl: "imageUrl",
+ uri: "uri",
+ ),
+ track: SpotifyActivityTrack(
+ name: "name",
+ artist: const SpotifyActivityArtist(
+ name: "name",
+ uri: "uri",
+ ),
+ album: const SpotifyActivityAlbum(
+ name: "name",
+ uri: "uri",
+ ),
+ context: SpotifyActivityContext(
+ name: "name",
+ index: i,
+ uri: "uri",
+ ),
+ imageUrl: "imageUrl",
+ uri: "uri",
+ ),
+ ),
+ ],
+ );
}
diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart
index de6f6d1c..4554de63 100644
--- a/lib/collections/language_codes.dart
+++ b/lib/collections/language_codes.dart
@@ -452,10 +452,10 @@ abstract class LanguageLocals {
// name: "North Ndebele",
// nativeName: "isiNdebele",
// ),
- // "ne": const ISOLanguageName(
- // name: "Nepali",
- // nativeName: "नेपाली",
- // ),
+ "ne": const ISOLanguageName(
+ name: "Nepali",
+ nativeName: "नेपाली",
+ ),
// "ng": const ISOLanguageName(
// name: "Ndonga",
// nativeName: "Owambo",
diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart
index 7816f204..3e2c42e0 100644
--- a/lib/collections/routes.dart
+++ b/lib/collections/routes.dart
@@ -17,6 +17,7 @@ import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
+import 'package:spotube/pages/track/track.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/shared/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.dart';
@@ -144,6 +145,15 @@ final router = GoRouter(
);
},
),
+ GoRoute(
+ path: "/track/:id",
+ pageBuilder: (context, state) {
+ final id = state.pathParameters["id"]!;
+ return SpotubePage(
+ child: TrackPage(trackId: id),
+ );
+ },
+ ),
],
),
GoRoute(
diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart
index 00010aae..65e6c1a0 100644
--- a/lib/collections/spotube_icons.dart
+++ b/lib/collections/spotube_icons.dart
@@ -41,6 +41,7 @@ abstract class SpotubeIcons {
static const clock = FeatherIcons.clock;
static const lyrics = Icons.lyrics_rounded;
static const lyricsOff = Icons.lyrics_outlined;
+ static const noLyrics = Icons.music_off_outlined;
static const logout = FeatherIcons.logOut;
static const login = FeatherIcons.logIn;
static const dashboard = FeatherIcons.grid;
@@ -109,4 +110,5 @@ abstract class SpotubeIcons {
static const normalize = FeatherIcons.barChart2;
static const wikipedia = SimpleIcons.wikipedia;
static const discord = SimpleIcons.discord;
+ static const youtube = SimpleIcons.youtube;
}
diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart
index f2b183f4..5abb9524 100644
--- a/lib/components/desktop_login/login_form.dart
+++ b/lib/components/desktop_login/login_form.dart
@@ -17,7 +17,6 @@ class TokenLoginForm extends HookConsumerWidget {
final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier);
final directCodeController = useTextEditingController();
- final keyCodeController = useTextEditingController();
final mounted = useIsMounted();
final isLoading = useState(false);
@@ -37,23 +36,13 @@ class TokenLoginForm extends HookConsumerWidget {
keyboardType: TextInputType.visiblePassword,
),
const SizedBox(height: 10),
- TextField(
- controller: keyCodeController,
- decoration: InputDecoration(
- hintText: context.l10n.spotify_cookie("\"sp_key (or sp_gaid)\""),
- labelText: context.l10n.cookie_name_cookie("sp_key (or sp_gaid)"),
- ),
- keyboardType: TextInputType.visiblePassword,
- ),
- const SizedBox(height: 20),
FilledButton(
onPressed: isLoading.value
? null
: () async {
try {
isLoading.value = true;
- if (keyCodeController.text.isEmpty ||
- directCodeController.text.isEmpty) {
+ if (directCodeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.fill_in_all_fields),
@@ -63,7 +52,7 @@ class TokenLoginForm extends HookConsumerWidget {
return;
}
final cookieHeader =
- "sp_dc=${directCodeController.text.trim()}; sp_key=${keyCodeController.text.trim()}";
+ "sp_dc=${directCodeController.text.trim()}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(
diff --git a/lib/components/home/sections/friends.dart b/lib/components/home/sections/friends.dart
new file mode 100644
index 00000000..ef24b8d5
--- /dev/null
+++ b/lib/components/home/sections/friends.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:skeletonizer/skeletonizer.dart';
+import 'package:spotube/collections/fake.dart';
+import 'package:spotube/components/home/sections/friends/friend_item.dart';
+import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
+import 'package:spotube/models/spotify_friends.dart';
+import 'package:spotube/services/queries/queries.dart';
+
+class HomePageFriendsSection extends HookConsumerWidget {
+ const HomePageFriendsSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final friendsQuery = useQueries.user.friendActivity(ref);
+ final friends = friendsQuery.data?.friends ?? FakeData.friends.friends;
+
+ final groupCount = useBreakpointValue(
+ sm: 3,
+ xs: 2,
+ md: 4,
+ lg: 5,
+ xl: 6,
+ xxl: 7,
+ );
+
+ final friendGroup = friends.fold>>(
+ [],
+ (previousValue, element) {
+ if (previousValue.isEmpty) {
+ return [
+ [element]
+ ];
+ }
+
+ final lastGroup = previousValue.last;
+ if (lastGroup.length < groupCount) {
+ return [
+ ...previousValue.sublist(0, previousValue.length - 1),
+ [...lastGroup, element]
+ ];
+ }
+
+ return [
+ ...previousValue,
+ [element]
+ ];
+ },
+ );
+
+ if (!friendsQuery.isLoading &&
+ (!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) {
+ return const SliverToBoxAdapter(
+ child: SizedBox.shrink(),
+ );
+ }
+
+ return Skeletonizer.sliver(
+ enabled: friendsQuery.isLoading,
+ child: SliverMainAxisGroup(
+ slivers: [
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ 'Friends',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ ),
+ SliverToBoxAdapter(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (final group in friendGroup)
+ Row(
+ children: [
+ for (final friend in group)
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: FriendItem(friend: friend),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart
new file mode 100644
index 00000000..fcdadab7
--- /dev/null
+++ b/lib/components/home/sections/friends/friend_item.dart
@@ -0,0 +1,136 @@
+import 'package:fl_query_hooks/fl_query_hooks.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:go_router/go_router.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/models/spotify_friends.dart';
+import 'package:spotube/provider/spotify_provider.dart';
+
+class FriendItem extends HookConsumerWidget {
+ final SpotifyFriendActivity friend;
+ const FriendItem({
+ Key? key,
+ required this.friend,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final ThemeData(
+ textTheme: textTheme,
+ colorScheme: colorScheme,
+ ) = Theme.of(context);
+
+ final queryClient = useQueryClient();
+ final spotify = ref.watch(spotifyProvider);
+
+ return Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: colorScheme.surfaceVariant.withOpacity(0.3),
+ borderRadius: BorderRadius.circular(15),
+ ),
+ constraints: const BoxConstraints(
+ minWidth: 300,
+ ),
+ height: 80,
+ child: Row(
+ children: [
+ CircleAvatar(
+ backgroundImage: UniversalImage.imageProvider(
+ friend.user.imageUrl,
+ ),
+ ),
+ const Gap(8),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ friend.user.name,
+ style: textTheme.bodyLarge,
+ ),
+ RichText(
+ text: TextSpan(
+ style: textTheme.bodySmall,
+ children: [
+ TextSpan(
+ text: friend.track.name,
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ context.push("/track/${friend.track.id}");
+ },
+ ),
+ const TextSpan(text: " • "),
+ const WidgetSpan(
+ child: Icon(
+ SpotubeIcons.artist,
+ size: 12,
+ ),
+ ),
+ TextSpan(
+ text: " ${friend.track.artist.name}",
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ context.push(
+ "/artist/${friend.track.artist.id}",
+ );
+ },
+ ),
+ const TextSpan(text: "\n"),
+ TextSpan(
+ text: friend.track.context.name,
+ recognizer: TapGestureRecognizer()
+ ..onTap = () async {
+ context.push(
+ "/${friend.track.context.path}",
+ extra: !friend.track.context.path
+ .startsWith("album")
+ ? null
+ : await queryClient.fetchQuery(
+ "album/${friend.track.album.id}",
+ () => spotify.albums.get(
+ friend.track.album.id,
+ ),
+ ),
+ );
+ },
+ ),
+ const TextSpan(text: " • "),
+ const WidgetSpan(
+ child: Icon(
+ SpotubeIcons.album,
+ size: 12,
+ ),
+ ),
+ TextSpan(
+ text: " ${friend.track.album.name}",
+ recognizer: TapGestureRecognizer()
+ ..onTap = () async {
+ final album =
+ await queryClient.fetchQuery(
+ "album/${friend.track.album.id}",
+ () => spotify.albums.get(
+ friend.track.album.id,
+ ),
+ );
+ if (context.mounted) {
+ context.push(
+ "/album/${friend.track.album.id}",
+ extra: album,
+ );
+ }
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/home/sections/new_releases.dart b/lib/components/home/sections/new_releases.dart
index 77481de1..0f4a046a 100644
--- a/lib/components/home/sections/new_releases.dart
+++ b/lib/components/home/sections/new_releases.dart
@@ -21,16 +21,21 @@ class HomeNewReleasesSection extends HookConsumerWidget {
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
final albums = useMemoized(
- () => newReleases.pages
- .whereType>()
- .expand((page) => page.items ?? const [])
- .where((album) {
- return album.artists
- ?.any((artist) => userArtists.contains(artist.id!)) ==
- true;
- })
- .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album))
- .toList(),
+ () {
+ final allReleases = newReleases.pages
+ .whereType>()
+ .expand((page) => page.items ?? const [])
+ .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album));
+
+ final userArtistReleases = allReleases.where((album) {
+ return album.artists
+ ?.any((artist) => userArtists.contains(artist.id!)) ==
+ true;
+ }).toList();
+
+ if (userArtistReleases.isEmpty) return allReleases.toList();
+ return userArtistReleases;
+ },
[newReleases.pages],
);
diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart
index a65c6d0e..32e91ed6 100644
--- a/lib/components/library/user_playlists.dart
+++ b/lib/components/library/user_playlists.dart
@@ -37,21 +37,21 @@ class UserPlaylists extends HookConsumerWidget {
);
final likedTracksPlaylist = useMemoized(
- () => PlaylistSimple()
- ..name = context.l10n.liked_tracks
- ..description = context.l10n.liked_tracks_description
- ..type = "playlist"
- ..collaborative = false
- ..public = false
- ..id = "user-liked-tracks"
- ..images = [
- Image()
- ..height = 300
- ..width = 300
- ..url =
- "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png"
- ],
- [context.l10n]);
+ () => PlaylistSimple()
+ ..name = context.l10n.liked_tracks
+ ..description = context.l10n.liked_tracks_description
+ ..type = "playlist"
+ ..collaborative = false
+ ..public = false
+ ..id = "user-liked-tracks"
+ ..images = [
+ Image()
+ ..height = 300
+ ..width = 300
+ ..url = "assets/liked-tracks.jpg"
+ ],
+ [context.l10n],
+ );
final playlists = useMemoized(
() {
diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart
index d6f275fa..66cb9ef5 100644
--- a/lib/components/player/player_track_details.dart
+++ b/lib/components/player/player_track_details.dart
@@ -4,6 +4,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/components/shared/links/link_text.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/service_utils.dart';
@@ -44,10 +45,12 @@ class PlayerTrackDetails extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
- Text(
+ LinkText(
playback.activeTrack?.name ?? "",
+ "/track/${playback.activeTrack?.id}",
+ push: true,
overflow: TextOverflow.ellipsis,
- style: theme.textTheme.bodyMedium?.copyWith(
+ style: theme.textTheme.bodyMedium!.copyWith(
color: color,
),
),
@@ -66,8 +69,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
flex: 1,
child: Column(
children: [
- Text(
+ LinkText(
playback.activeTrack?.name ?? "",
+ "/track/${playback.activeTrack?.id}",
+ push: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color),
),
diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart
index cf1429b9..181c363a 100644
--- a/lib/components/player/sibling_tracks_sheet.dart
+++ b/lib/components/player/sibling_tracks_sheet.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Offset;
+import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
@@ -19,10 +20,28 @@ import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
+import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
+import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
+final sourceInfoToIconMap = {
+ YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
+ JioSaavnSourceInfo: Container(
+ height: 30,
+ width: 30,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(90),
+ image: DecorationImage(
+ image: Assets.jiosaavn.provider(),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ PipedSourceInfo: const Icon(SpotubeIcons.piped),
+};
+
class SiblingTracksSheet extends HookConsumerWidget {
final bool floating;
const SiblingTracksSheet({
@@ -64,17 +83,34 @@ class SiblingTracksSheet extends HookConsumerWidget {
return [];
}
- final results = await youtubeClient.search.search(searchTerm.trim());
+ final resultsYt = await youtubeClient.search.search(searchTerm.trim());
+ final resultsJioSaavn =
+ await jiosaavnClient.search.songs(searchTerm.trim());
- return await Future.wait(
- results.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async {
+ final searchResults = await Future.wait([
+ ...resultsJioSaavn.results.mapIndexed((i, song) async {
+ final siblingType = JioSaavnSourcedTrack.toSiblingType(song);
+ return siblingType.info;
+ }),
+ ...resultsYt
+ .map(YoutubeVideoInfo.fromVideo)
+ .mapIndexed((i, video) async {
final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video);
return siblingType.info;
}),
- );
+ ]);
+ final activeSourceInfo =
+ (playlist.activeTrack! as SourcedTrack).sourceInfo;
+ return searchResults
+ ..removeWhere((element) => element.id == activeSourceInfo.id)
+ ..insert(
+ 0,
+ activeSourceInfo,
+ );
}, [
searchTerm,
searchMode.value,
+ playlist.activeTrack,
]);
final siblings = useMemoized(
@@ -104,6 +140,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
final itemBuilder = useCallback(
(SourceInfo sourceInfo) {
+ final icon = sourceInfoToIconMap[sourceInfo.runtimeType];
return ListTile(
title: Text(sourceInfo.title),
leading: Padding(
@@ -118,7 +155,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
borderRadius: BorderRadius.circular(5),
),
trailing: Text(sourceInfo.duration.toHumanReadableString()),
- subtitle: Text(sourceInfo.artist),
+ subtitle: Row(
+ children: [
+ if (icon != null) icon,
+ Text(" • ${sourceInfo.artist}"),
+ ],
+ ),
enabled: playlist.isFetching != true,
selected: playlist.isFetching != true &&
sourceInfo.id ==
@@ -137,7 +179,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
[playlist.isFetching, playlist.activeTrack, siblings],
);
- var mediaQuery = MediaQuery.of(context);
+ final mediaQuery = MediaQuery.of(context);
return SafeArea(
child: ClipRRect(
borderRadius: borderRadius,
diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart
index ac5233ed..9b3fd3ed 100644
--- a/lib/components/root/sidebar.dart
+++ b/lib/components/root/sidebar.dart
@@ -159,7 +159,7 @@ class Sidebar extends HookConsumerWidget {
margin: EdgeInsets.only(
bottom: 10,
left: 0,
- top: kIsMacOS ? 35 : 5,
+ top: kIsMacOS ? 0 : 5,
),
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
index d00e5c4b..dc9d30da 100644
--- a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
+++ b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
@@ -55,48 +55,49 @@ class HorizontalPlaybuttonCardView extends HookWidget {
),
SizedBox(
height: height,
- child: ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: items.isEmpty
- ? ListView.builder(
- scrollDirection: Axis.horizontal,
- itemCount: 5,
- itemBuilder: (context, index) {
- return AlbumCard(FakeData.albumSimple);
- },
- )
- : InfiniteList(
- scrollController: scrollController,
- scrollDirection: Axis.horizontal,
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- itemCount: items.length,
- onFetchData: onFetchMore,
- loadingBuilder: (context) => Skeletonizer(
- enabled: true,
- child: AlbumCard(FakeData.albumSimple),
- ),
- isLoading: isLoadingNextPage,
- hasReachedMax: !hasNextPage,
- itemBuilder: (context, index) {
- final item = items[index];
-
- return switch (item.runtimeType) {
- PlaylistSimple =>
- PlaylistCard(item as PlaylistSimple),
- Album => AlbumCard(item as Album),
- Artist => Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 12.0),
- child: ArtistCard(item as Artist),
+ child: NotificationListener(
+ // disable multiple scrollbar to use this
+ onNotification: (notification) => true,
+ child: ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(
+ dragDevices: PointerDeviceKind.values.toSet(),
+ ),
+ child: items.isEmpty
+ ? ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: 5,
+ itemBuilder: (context, index) {
+ return AlbumCard(FakeData.albumSimple);
+ },
+ )
+ : InfiniteList(
+ scrollController: scrollController,
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ itemCount: items.length,
+ onFetchData: onFetchMore,
+ loadingBuilder: (context) => Skeletonizer(
+ enabled: true,
+ child: AlbumCard(FakeData.albumSimple),
),
- _ => const SizedBox.shrink(),
- };
- }),
+ isLoading: isLoadingNextPage,
+ hasReachedMax: !hasNextPage,
+ itemBuilder: (context, index) {
+ final item = items[index];
+
+ return switch (item.runtimeType) {
+ PlaylistSimple =>
+ PlaylistCard(item as PlaylistSimple),
+ Album => AlbumCard(item as Album),
+ Artist => Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12.0),
+ child: ArtistCard(item as Artist),
+ ),
+ _ => const SizedBox.shrink(),
+ };
+ }),
+ ),
),
),
],
diff --git a/lib/components/shared/links/link_text.dart b/lib/components/shared/links/link_text.dart
index 217b247d..d7b00b72 100644
--- a/lib/components/shared/links/link_text.dart
+++ b/lib/components/shared/links/link_text.dart
@@ -8,6 +8,7 @@ class LinkText extends StatelessWidget {
final TextAlign? textAlign;
final TextOverflow? overflow;
final String route;
+ final int? maxLines;
final T? extra;
final bool push;
@@ -19,6 +20,7 @@ class LinkText extends StatelessWidget {
this.extra,
this.overflow,
this.style = const TextStyle(),
+ this.maxLines,
this.push = false,
}) : super(key: key);
@@ -37,6 +39,7 @@ class LinkText extends StatelessWidget {
overflow: overflow,
style: style,
textAlign: textAlign,
+ maxLines: maxLines,
);
}
}
diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart
index d8e20184..4f522e0c 100644
--- a/lib/components/shared/page_window_title_bar.dart
+++ b/lib/components/shared/page_window_title_bar.dart
@@ -2,14 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
-import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
import 'package:spotube/utils/platform.dart';
import 'package:titlebar_buttons/titlebar_buttons.dart';
import 'dart:math';
import 'package:flutter/foundation.dart' show kIsWeb;
-import 'dart:io' show Platform, exit;
+import 'dart:io' show Platform;
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
-import 'package:local_notifier/local_notifier.dart';
class PageWindowTitleBar extends StatefulHookConsumerWidget
implements PreferredSizeWidget {
diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart
index 8405d6ea..724bc029 100644
--- a/lib/components/shared/track_tile/track_options.dart
+++ b/lib/components/shared/track_tile/track_options.dart
@@ -43,12 +43,14 @@ class TrackOptions extends HookConsumerWidget {
final bool userPlaylist;
final String? playlistId;
final ObjectRef?>? showMenuCbRef;
+ final Widget? icon;
const TrackOptions({
Key? key,
required this.track,
this.showMenuCbRef,
this.userPlaylist = false,
this.playlistId,
+ this.icon,
}) : super(key: key);
void actionShare(BuildContext context, Track track) {
@@ -207,7 +209,7 @@ class TrackOptions extends HookConsumerWidget {
break;
}
},
- icon: const Icon(SpotubeIcons.moreHorizontal),
+ icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
headings: [
ListTile(
dense: true,
diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart
index 961f29c9..c3b03f3c 100644
--- a/lib/components/shared/track_tile/track_tile.dart
+++ b/lib/components/shared/track_tile/track_tile.dart
@@ -193,8 +193,10 @@ class TrackTile extends HookConsumerWidget {
children: [
Expanded(
flex: 6,
- child: Text(
+ child: LinkText(
track.name!,
+ "/track/${track.id}",
+ push: true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart
index e16ccbff..19241dc6 100644
--- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart
+++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart
@@ -1,6 +1,5 @@
import 'dart:ui';
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -62,7 +61,7 @@ class TrackViewFlexHeader extends HookConsumerWidget {
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
image: DecorationImage(
- image: CachedNetworkImageProvider(props.image),
+ image: UniversalImage.imageProvider(props.image),
fit: BoxFit.cover,
),
),
diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart
index be6facf9..3b7ec3f3 100644
--- a/lib/hooks/configurators/use_deep_linking.dart
+++ b/lib/hooks/configurators/use_deep_linking.dart
@@ -48,6 +48,11 @@ void useDeepLinking(WidgetRef ref) {
),
);
break;
+ case "track":
+ router.push(
+ "/track/${url.pathSegments.last}",
+ );
+ break;
default:
break;
}
@@ -80,6 +85,9 @@ void useDeepLinking(WidgetRef ref) {
case "spotify:artist":
await router.push("/artist/$endSegment");
break;
+ case "spotify:track":
+ await router.push("/track/$endSegment");
+ break;
case "spotify:playlist":
await router.push(
"/playlist/$endSegment",
diff --git a/lib/hooks/configurators/use_init_sys_tray.dart b/lib/hooks/configurators/use_init_sys_tray.dart
index db4964ce..8080bea6 100644
--- a/lib/hooks/configurators/use_init_sys_tray.dart
+++ b/lib/hooks/configurators/use_init_sys_tray.dart
@@ -25,7 +25,7 @@ void useInitSysTray(WidgetRef ref) {
}
final enabled = !playlist.isFetching;
systemTray.value = await DesktopTools.createSystemTrayMenu(
- title: DesktopTools.platform.isLinux ? "" : "Spotube",
+ title: DesktopTools.platform.isWindows ? "Spotube" : "",
iconPath: "assets/spotube-logo.png",
windowsIconPath: "assets/spotube-logo.ico",
items: [
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 2bdde72a..20b6a47c 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -177,11 +177,9 @@
"step_2": "الخطوة 2",
"step_2_steps": "1. بمجرد تسجيل الدخول، اضغط على F12 أو انقر بزر الماوس الأيمن > فحص لفتح أدوات تطوير المتصفح.\n2. ثم انتقل إلى علامة التبويب \"التطبيقات\" (Chrome وEdge وBrave وما إلى ذلك.) أو علامة التبويب \"التخزين\" (Firefox وPalemoon وما إلى ذلك..)\n3. انتقل إلى قسم \"ملفات تعريف الارتباط\" ثم القسم الفرعي \"https://accounts.spotify.com\"",
"step_3": "الخطوة 3",
- "step_3_steps": "انسخ قيم \"sp_dc\" و \"sp_key\" (أو sp_gaid) الكويز",
"success_emoji": "نجاح 🥳",
"success_message": "لقد قمت الآن بتسجيل الدخول بنجاح باستخدام حساب Spotify الخاص بك. عمل جيد يا صديقي!",
"step_4": "الخطوة 4",
- "step_4_steps": "قم بلصق قيم \"sp_dc\" و \"sp_key\" (أو sp_gaid) المنسوخة في الحقول المعنية",
"something_went_wrong": "هناك خطأ ما",
"piped_instance": "مثيل خادم Piped",
"piped_description": "مثيل خادم Piped الذي سيتم استخدامه لمطابقة المقطوعة",
diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb
index 39f8a1ee..74dc200d 100644
--- a/lib/l10n/app_bn.arb
+++ b/lib/l10n/app_bn.arb
@@ -175,11 +175,9 @@
"step_2": "ধাপ 2",
"step_2_steps": "১. একবার আপনি লগ ইন করলে, ব্রাউজার ডেভটুল খুলতে F12 বা মাউসের রাইট ক্লিক > \"Inspect to open Browser DevTools\" টিপুন।\n২. তারপর \"Application\" ট্যাবে যান (Chrome, Edge, Brave etc..) অথবা \"Storage\" Tab (Firefox, Palemoon etc..)\n৩. \"Cookies \" বিভাগে যান তারপর \"https://accounts.spotify.com\" উপবিভাগে যান",
"step_3": "ধাপ 3",
- "step_3_steps": "\"sp_dc\" এবং \"sp_key\" (অথবা sp_gaid) কুকিজের মান কপি করুন",
"success_emoji": "আমরা সফল🥳",
"success_message": "এখন আপনি সফলভাবে আপনার Spotify অ্যাকাউন্ট দিয়ে লগ ইন করেছেন। সাধুভাত আপনাকে",
"step_4": "ধাপ 4",
- "step_4_steps": "কপি করা \"sp_dc\" এবং \"sp_key\" (অথবা sp_gaid) এর মান সংশ্লিষ্ট ফিল্ডে পেস্ট করুন",
"something_went_wrong": "কিছু ভুল হয়েছে",
"piped_instance": "Piped সার্ভার এড্রেস",
"piped_description": "গান ম্যাচ করার জন্য ব্যবহৃত পাইপড সার্ভার",
diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb
index 15ca9e31..2c952457 100644
--- a/lib/l10n/app_ca.arb
+++ b/lib/l10n/app_ca.arb
@@ -175,11 +175,9 @@
"step_2": "Pas 2",
"step_2_steps": "1. Una vegada que hagi iniciat sessió, premi F12 o faci clic dret amb el ratolí > Inspeccionar per obrir les eines de desenvolulpador del navegador.\n2. Després vagi a la pestanya \"Application\" (Chrome, Edge, Brave, etc.) o \"Storage\" (Firefox, Palemoon, etc.)\n3. Vagi a la secció \"Cookies\" i després a la subsecció \"https://accounts.spotify.com\"",
"step_3": "Pas 3",
- "step_3_steps": "Copiï els valors de les Cookies \"sp_dc\" i \"sp_key\" (o sp_gaid)",
"success_emoji": "Èxit! 🥳",
"success_message": "Ara has iniciat sessió amb èxit al teu compte de Spotify. Bona feina!",
"step_4": "Pas 4",
- "step_4_steps": "Enganxi els valors coppiats de \"sp_dc\" i \"sp_key\" (o sp_gaid) en els camps respectius",
"something_went_wrong": "Quelcom ha sortit malament",
"piped_instance": "Instància del servidor Piped",
"piped_description": "La instància del servidor Piped a utilitzar per la coincidència de cançons",
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 1a13e4a1..59f832ea 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -175,11 +175,9 @@
"step_2": "Schritt 2",
"step_2_steps": "1. Wenn du angemeldet bist, drücke F12 oder klicke mit der rechten Maustaste > Inspektion, um die Browser-Entwicklertools zu öffnen.\n2. Gehe dann zum \"Anwendungs\"-Tab (Chrome, Edge, Brave usw.) oder zum \"Storage\"-Tab (Firefox, Palemoon usw.)\n3. Gehe zum Abschnitt \"Cookies\" und dann zum Unterabschnitt \"https://accounts.spotify.com\"",
"step_3": "Schritt 3",
- "step_3_steps": "Kopiere die Werte der Cookies \"sp_dc\" und \"sp_key\" (oder sp_gaid)",
"success_emoji": "Erfolg🥳",
"success_message": "Jetzt bist du erfolgreich mit deinem Spotify-Konto angemeldet. Gut gemacht, Kumpel!",
"step_4": "Schritt 4",
- "step_4_steps": "Füge die kopierten Werte von \"sp_dc\" und \"sp_key\" (oder sp_gaid) in die entsprechenden Felder ein",
"something_went_wrong": "Etwas ist schiefgelaufen",
"piped_instance": "Piped-Serverinstanz",
"piped_description": "Die Piped-Serverinstanz, die zur Titelzuordnung verwendet werden soll",
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index bebfafac..07df5f06 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -177,11 +177,11 @@
"step_2": "Step 2",
"step_2_steps": "1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection",
"step_3": "Step 3",
- "step_3_steps": "Copy the values of \"sp_dc\" and \"sp_key\" (or sp_gaid) Cookies",
+ "step_3_steps": "Copy the value of \"sp_dc\" Cookie",
"success_emoji": "Success🥳",
- "success_message": "Now you're successfully Logged In with your Spotify account. Good Job, mate!",
+ "success_message": "Now you've successfully Logged in with your Spotify account. Good Job, mate!",
"step_4": "Step 4",
- "step_4_steps": "Paste the copied \"sp_dc\" and \"sp_key\" (or sp_gaid) values in the respective fields",
+ "step_4_steps": "Paste the copied \"sp_dc\" value",
"something_went_wrong": "Something went wrong",
"piped_instance": "Piped Server Instance",
"piped_description": "The Piped server instance to use for track matching",
@@ -284,5 +284,7 @@
"discord_rich_presence": "Discord Rich Presence",
"browse_all": "Browse All",
"genres": "Genres",
- "explore_genres": "Explore Genres"
+ "explore_genres": "Explore Genres",
+ "friends": "Friends",
+ "no_lyrics_available": "Sorry, unable find lyrics for this track"
}
\ No newline at end of file
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 2fecd8f1..e04b4798 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -175,11 +175,9 @@
"step_2": "Paso 2",
"step_2_steps": "1. Una vez que hayas iniciado sesión, presiona F12 o haz clic derecho con el ratón > Inspeccionar para abrir las herramientas de desarrollo del navegador.\n2. Luego ve a la pestaña \"Application\" (Chrome, Edge, Brave, etc.) o \"Storage\" (Firefox, Palemoon, etc.)\n3. Ve a la sección \"Cookies\" y luego la subsección \"https://accounts.spotify.com\"",
"step_3": "Paso 3",
- "step_3_steps": "Copia los valores de las Cookies \"sp_dc\" y \"sp_key\" (o sp_gaid)",
"success_emoji": "¡Éxito! 🥳",
"success_message": "Ahora has iniciado sesión con éxito en tu cuenta de Spotify. ¡Buen trabajo!",
"step_4": "Paso 4",
- "step_4_steps": "Pega los valores copiados de \"sp_dc\" y \"sp_key\" (o sp_gaid) en los campos respectivos",
"something_went_wrong": "Algo salió mal",
"piped_instance": "Instancia del servidor Piped",
"piped_description": "La instancia del servidor Piped a utilizar para la coincidencia de pistas",
diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb
index 84b9b448..c9586cde 100644
--- a/lib/l10n/app_fa.arb
+++ b/lib/l10n/app_fa.arb
@@ -177,11 +177,9 @@
"step_2": "گام 2",
"step_2_steps": "1. پس از ورود به سیستم، F12 یا کلیک راست ماوس > Inspect را فشار دهید تا ابزارهای توسعه مرورگر باز شود..\n2. سپس به تب \"Application\" (Chrome, Edge, Brave etc..) یا \"Storage\" Tab (Firefox, Palemoon etc..)\n3. به قسمت \"Cookies\" و به پخش \"https://accounts.spotify.com\" بروید",
"step_3": "گام 3",
- "step_3_steps": "کپی کردن مقادیر \"sp_dc\" و \"sp_key\" (یا sp_gaid) کوکی",
"success_emoji": "موفقیت🥳",
"success_message": "اکنون با موفقیت با حساب اسپوتیفای خود وارد شده اید",
"step_4": "مرحله 4",
- "step_4_steps": "مقدار کپی شده را \"sp_dc\" and \"sp_key\" (یا sp_gaid) در فیلد مربوط پر کنید",
"something_went_wrong": "اشتباهی رخ داده",
"piped_instance": "مشکل در ارتباط با سرور",
"piped_description": "مشکل در ارتباط با سرور در دریافت آهنگ ها",
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 82997bad..0c3eb653 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -175,11 +175,9 @@
"step_2": "Étape 2",
"step_2_steps": "1. Une fois connecté, appuyez sur F12 ou clic droit de la souris > Inspecter pour ouvrir les outils de développement du navigateur.\n2. Ensuite, allez dans l'onglet \"Application\" (Chrome, Edge, Brave, etc.) ou l'onglet \"Stockage\" (Firefox, Palemoon, etc.)\n3. Allez dans la section \"Cookies\", puis dans la sous-section \"https://accounts.spotify.com\"",
"step_3": "Étape 3",
- "step_3_steps": "Copiez les valeurs des cookies \"sp_dc\" et \"sp_key\" (ou sp_gaid)",
"success_emoji": "Succès🥳",
"success_message": "Vous êtes maintenant connecté avec succès à votre compte Spotify. Bon travail, mon ami!",
"step_4": "Étape 4",
- "step_4_steps": "Collez les valeurs copiées de \"sp_dc\" et \"sp_key\" (ou sp_gaid) dans les champs respectifs",
"something_went_wrong": "Quelque chose s'est mal passé",
"piped_instance": "Instance pipée",
"piped_description": "L'instance de serveur Piped à utiliser pour la correspondance des pistes",
diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb
index 4bfff3da..dd27dabf 100644
--- a/lib/l10n/app_hi.arb
+++ b/lib/l10n/app_hi.arb
@@ -175,11 +175,9 @@
"step_2": "2 चरण",
"step_2_steps": "1. जब आप लॉगिन हो जाएँ, तो F12 दबाएं या माउस राइट क्लिक> निरीक्षण करें ताकि ब्राउज़र डेवटूल्स खुलें।\n2. फिर ब्राउज़र के \"एप्लिकेशन\" टैब (Chrome, Edge, Brave आदि) या \"स्टोरेज\" टैब (Firefox, Palemoon आदि) में जाएं\n3. \"कुकीज़\" अनुभाग में जाएं फिर \"https: //accounts.spotify.com\" उप-अनुभाग में जाएं",
"step_3": "स्टेप 3",
- "step_3_steps": "\"sp_dc\" और \"sp_key\" (या sp_gaid) कुकीज़ के मान कॉपी करें",
"success_emoji": "सफलता🥳",
"success_message": "अब आप अपने स्पॉटिफाई अकाउंट से सफलतापूर्वक लॉगइन हो गए हैं। अच्छा काम किया!",
"step_4": "स्टेप 4",
- "step_4_steps": "कॉपी की गई \"sp_dc\" और \"sp_key\" (या sp_gaid) मानों को संबंधित फील्ड में पेस्ट करें",
"something_went_wrong": "कुछ गलत हो गया",
"piped_instance": "पाइप्ड सर्वर",
"piped_description": "पाइप किए गए सर्वर",
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index 033bb516..3680933a 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -177,11 +177,9 @@
"step_2": "Passo 2",
"step_2_steps": "1. Quando sei acceduto premi F12 o premi il tasto destro del Mouse > Ispeziona per aprire gli strumenti di sviluppo del browser.\n2. Vai quindi nel tab \"Applicazione\" (Chrome, Edge, Brave etc..) o tab \"Archiviazione\" (Firefox, Palemoon etc..)\n3. Vai nella sezione \"Cookies\" quindi nella sezione \"https://accounts.spotify.com\"",
"step_3": "Passo 3",
- "step_3_steps": "Copia il valore dei cookie \"sp_dc\" e \"sp_key\" (o sp_gaid)",
"success_emoji": "Successo🥳",
"success_message": "Ora hai correttamente effettuato il login al tuo account Spotify. Bel lavoro, amico!",
"step_4": "Passo 4",
- "step_4_steps": "Incolla i valori copiati di \"sp_dc\" e \"sp_key\" (o sp_gaid) nei campi rispettivi",
"something_went_wrong": "Qualcosa è andato storto",
"piped_instance": "Istanza Server Piped",
"piped_description": "L'istanza server Piped da usare per il match della tracccia",
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index ac23728b..39e0dad8 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -175,11 +175,9 @@
"step_2": "ステップ 2",
"step_2_steps": "1. ログインしたら、F12を押すか、マウス右クリック > 調査(検証)でブラウザの開発者ツール (devtools) を開きます。\n2. アプリケーション (Application) タブ (Chrome, Edge, Brave など) またはストレージタブ (Firefox, Palemoon など)\n3. Cookies 欄を選択し、https://accounts.spotify.com の枝を選びます",
"step_3": "ステップ 3",
- "step_3_steps": "sp_dc と sp_key (または or sp_gaid) の値 (Value) をコピーします",
"success_emoji": "成功🥳",
"success_message": "アカウントへのログインに成功しました。よくできました!",
"step_4": "ステップ 4",
- "step_4_steps": "コピーした sp_dc と sp_key (または or sp_gaid) の値をそれぞれの入力欄に貼り付けます",
"something_went_wrong": "何か誤りがあります",
"piped_instance": "Piped サーバーのインスタンス",
"piped_description": "曲の一致に使う Piped サーバーのインスタンス",
diff --git a/lib/l10n/app_ne.arb b/lib/l10n/app_ne.arb
new file mode 100644
index 00000000..9fca9ea4
--- /dev/null
+++ b/lib/l10n/app_ne.arb
@@ -0,0 +1,288 @@
+{
+ "guest": "अतिथि",
+ "browse": "ब्राउज़ गर्नुहोस्",
+ "search": "खोजी गर्नुहोस्",
+ "library": "पुस्तकालय",
+ "lyrics": "गीतको शब्द",
+ "settings": "सेटिङ",
+ "genre_categories_filter": "शैली वा शैलीहरू फिल्टर गर्नुहोस्...",
+ "genre": "शैली",
+ "personalized": "व्यक्तिगत",
+ "featured": "विशेष",
+ "new_releases": "नयाँ रिलिज",
+ "songs": "गीतहरू",
+ "playing_track": "{track} बज्यो",
+ "queue_clear_alert": "यो हालको कतारलाई हटाउँछ। {track_length} ट्र्याकहरू हटाईन्छ\nके तपाईं जारी राख्न चाहनुहुन्छ?",
+ "load_more": "थप लोड गर्नुहोस्",
+ "playlists": "प्लेलिस्टहरू",
+ "artists": "कलाकारहरू",
+ "albums": "आल्बमहरू",
+ "tracks": "ट्र्याकहरू",
+ "downloads": "डाउनलोडहरू",
+ "filter_playlists": "तपाईंको प्लेलिस्टहरू फिल्टर गर्नुहोस्...",
+ "liked_tracks": "मन परेका ट्र्याकहरू",
+ "liked_tracks_description": "तपाईंको मन परेका सबै ट्र्याकहरू",
+ "create_playlist": "प्लेलिस्ट बनाउनुहोस्",
+ "create_a_playlist": "प्लेलिस्ट बनाउनुहोस्",
+ "update_playlist": "प्लेलिस्ट अपडेट गर्नुहोस्",
+ "create": "बनाउनुहोस्",
+ "cancel": "रद्द गर्नुहोस्",
+ "update": "अपडेट गर्नुहोस्",
+ "playlist_name": "प्लेलिस्टको नाम",
+ "name_of_playlist": "प्लेलिस्टको नाम",
+ "description": "विवरण",
+ "public": "सार्वजनिक",
+ "collaborative": "सहकारी",
+ "search_local_tracks": "स्थानीय ट्र्याकहरू खोजी गर्नुहोस्...",
+ "play": "बजाउनुहोस्",
+ "delete": "मेटाउनुहोस्",
+ "none": "कुनै पनि होइन",
+ "sort_a_z": "A-Zमा क्रमबद्ध गर्नुहोस्",
+ "sort_z_a": "Z-Aमा क्रमबद्ध गर्नुहोस्",
+ "sort_artist": "कलाकारबाट क्रमबद्ध गर्नुहोस्",
+ "sort_album": "आल्बमबाट क्रमबद्ध गर्नुहोस्",
+ "sort_tracks": "ट्र्याकहरूलाई क्रमबद्ध गर्नुहोस्",
+ "currently_downloading": "हाल डाउनलोड गर्दैछ ({tracks_length})",
+ "cancel_all": "सब रद्द गर्नुहोस्",
+ "filter_artist": "कलाकारहरूलाई फिल्टर गर्नुहोस्...",
+ "followers": "{followers} अनुयायीहरू",
+ "add_artist_to_blacklist": "कलाकारलाई कालोसूचीमा थप्नुहोस्",
+ "top_tracks": "शीर्ष ट्र्याकहरू",
+ "fans_also_like": "अनुयायीहरू पनि लाइक गर्छन्",
+ "loading": "लोड हुँदैछ...",
+ "artist": "कलाकार",
+ "blacklisted": "कालोसूचीमा",
+ "following": "फल्लो गर्दै",
+ "follow": "फल्लो गर्नुहोस्",
+ "artist_url_copied": "कलाकार URL क्लिपबोर्डमा प्रतिलिपि गरिएको छ",
+ "added_to_queue": "{tracks} ट्र्याकहरूलाई कतारमा थपिएको छ",
+ "filter_albums": "आल्बमहरूलाई फिल्टर गर्नुहोस्...",
+ "synced": "सिङ्क गरिएको",
+ "plain": "साधा",
+ "shuffle": "शफल",
+ "search_tracks": "ट्र्याकहरू खोजी गर्नुहोस्...",
+ "released": "रिलिज गरिएको",
+ "error": "त्रुटि {error}",
+ "title": "शीर्षक",
+ "time": "समय",
+ "more_actions": "थप कार्यहरू",
+ "download_count": "डाउनलोड ({count})",
+ "add_count_to_playlist": "प्लेलिस्टमा थप्नुहोस् ({count})",
+ "add_count_to_queue": "कतारमा थप्नुहोस् ({count})",
+ "play_count_next": "प्लेगरी गर्नुहोस् ({count})",
+ "album": "आल्बम",
+ "copied_to_clipboard": "{data} क्लिपबोर्डमा प्रतिलिपि गरिएको छ",
+ "add_to_following_playlists": "{track} लाई तलका प्लेलिस्टमा थप्नुहोस्",
+ "add": "थप्नुहोस्",
+ "added_track_to_queue": "{track} लाई कतारमा थपिएको छ",
+ "add_to_queue": "कतारमा थप्नुहोस्",
+ "track_will_play_next": "{track} अरूलाई पहिलोमा बज्नेछ",
+ "play_next": "पछिबजाउनुहोस्",
+ "removed_track_from_queue": "{track} लाई कतारबाट हटाइएको छ",
+ "remove_from_queue": "कतारबाट हटाउनुहोस्",
+ "remove_from_favorites": "पसन्दीदामा बाट हटाउनुहोस्",
+ "save_as_favorite": "पसन्दीदा बनाउनुहोस्",
+ "add_to_playlist": "प्लेलिस्टमा थप्नुहोस्",
+ "remove_from_playlist": "प्लेलिस्टबाट हटाउनुहोस्",
+ "add_to_blacklist": "कालोसूचीमा थप्नुहोस्",
+ "remove_from_blacklist": "कालोसूचीबाट हटाउनुहोस्",
+ "share": "साझा गर्नुहोस्",
+ "mini_player": "मिनि प्लेयर",
+ "slide_to_seek": "अगाडि वा पछाडि खोजी गर्नका लागि स्लाइड गर्नुहोस्",
+ "shuffle_playlist": "प्लेलिस्ट शफल गर्नुहोस्",
+ "unshuffle_playlist": "प्लेलिस्ट शफल नगर्नुहोस्",
+ "previous_track": "पूर्व ट्र्याक",
+ "next_track": "अरू ट्र्याक",
+ "pause_playback": "प्लेब्याक रोक्नुहोस्",
+ "resume_playback": "प्लेब्याक पुनः सुरु गर्नुहोस्",
+ "loop_track": "ट्र्याकलाई दोहोरोपट्टी बजाउनुहोस्",
+ "repeat_playlist": "प्लेलिस्ट पुनः बजाउनुहोस्",
+ "queue": "कतार",
+ "alternative_track_sources": "वैकल्पिक ट्र्याक स्रोतहरू",
+ "download_track": "ट्र्याक डाउनलोड गर्नुहोस्",
+ "tracks_in_queue": "कतारमा {tracks} ट्र्याकहरू",
+ "clear_all": "सब मेटाउनुहोस्",
+ "show_hide_ui_on_hover": "हवर गरेपछि UI देखाउनुहोस्/लुकाउनुहोस्",
+ "always_on_top": "सधैं टपमा राख्नुहोस्",
+ "exit_mini_player": "मिनि प्लेयर बाट बाहिर निस्कनुहोस्",
+ "download_location": "डाउनलोड स्थान",
+ "account": "खाता",
+ "login_with_spotify": "तपाईंको Spotify खातासँग लगइन गर्नुहोस्",
+ "connect_with_spotify": "Spotify सँग जडान गर्नुहोस्",
+ "logout": "बाहिर निस्कनुहोस्",
+ "logout_of_this_account": "यो खाताबाट बाहिर निस्कनुहोस्",
+ "language_region": "भाषा र क्षेत्र",
+ "language": "भाषा",
+ "system_default": "सिस्टम पूर्वनिर्धारित",
+ "market_place_region": "बजार स्थान",
+ "recommendation_country":"सिफारिस गरिएको देश",
+ "appearance": "दृष्टिकोण",
+ "layout_mode": "लेआउट मोड",
+ "override_layout_settings": "अनुकूलित प्रतिकृयात्मक लेआउट मोड सेटिङ्गहरू",
+ "adaptive": "अनुकूलित",
+ "compact": "संकुचित",
+ "extended": "बढाइएको",
+ "theme": "थिम",
+ "dark": "गाढा",
+ "light": "प्रकाश",
+ "system": "सिस्टम",
+ "accent_color": "एक्सेन्ट रङ्ग",
+ "sync_album_color": "एल्बम रङ्ग सिङ्क गर्नुहोस्",
+ "sync_album_color_description": "एल्बम कला को प्रमुख रङ्गलाई एक्सेन्ट रङ्गको रूपमा प्रयोग गर्दछ",
+ "playback": "प्लेब्याक",
+ "audio_quality": "आडियो गुणस्तर",
+ "high": "उच्च",
+ "low": "न्यून",
+ "pre_download_play": "पूर्व-डाउनलोड र प्ले गर्नुहोस्",
+ "pre_download_play_description": "आडियो स्ट्रिम गर्नु नगरी बाइटहरू डाउनलोड गरी बजाउँछ (उच्च ब्यान्डविथ उपयोगकर्ताहरूको लागि सिफारिस गरिएको)",
+ "skip_non_music": "गीतहरू बाहेक कुनै अनुष्ठान छोड्नुहोस् (स्पन्सरब्लक)",
+ "blacklist_description": "कालोसूची गीत र कलाकारहरू",
+ "wait_for_download_to_finish": "कृपया हालको डाउनलोड समाप्त हुन लागि पर्खनुहोस्",
+ "desktop": "डेस्कटप",
+ "close_behavior": "बन्द व्यवहार",
+ "close": "बन्द गर्नुहोस्",
+ "minimize_to_tray": "ट्रेमा कम गर्नुहोस्",
+ "show_tray_icon": "सिस्टम ट्रे आइकन देखाउनुहोस्",
+ "about": "बारेमा",
+ "u_love_spotube": "हामीले थाहा पारेका छौं तपाईंलाई Spotube मन पर्छ",
+ "check_for_updates": "अपडेटहरूको लागि जाँच गर्नुहोस्",
+ "about_spotube": "Spotube को बारेमा",
+ "blacklist": "कालोसूची",
+ "please_sponsor": "कृपया स्पन्सर/डोनेट गर्नुहोस्",
+ "spotube_description": "Spotube, एक हल्का, समृद्ध, स्वतन्त्र Spotify क्लाइयन",
+ "version": "संस्करण",
+ "build_number": "निर्माण नम्बर",
+ "founder": "संस्थापक",
+ "repository": "पुनरावलोकन स्थल",
+ "bug_issues": "त्रुटि + समस्याहरू",
+ "made_with": "❤️ 2021-2024 बाट बनाइएको",
+ "kingkor_roy_tirtho": "किङ्कोर राय तिर्थो",
+ "copyright": "© 2021-{current_year} किङ्कोर राय तिर्थो",
+ "license": "लाइसेन्स",
+ "add_spotify_credentials": "सुरु हुनका लागि तपाईंको स्पटिफाई क्रेडेन्शियल थप्नुहोस्",
+ "credentials_will_not_be_shared_disclaimer": "चिन्ता नगर्नुहोस्, तपाईंको कुनै पनि क्रेडेन्शियलहरूले कसैले संग्रह वा साझा गर्नेछैन",
+ "know_how_to_login": "कसरी लगिन गर्ने भन्ने थाहा छैन?",
+ "follow_step_by_step_guide": "चरणबद्ध मार्गदर्शनमा साथी बनाउनुहोस्",
+ "spotify_cookie": "Spotify {name} कुकी",
+ "cookie_name_cookie": "{name} कुकी",
+ "fill_in_all_fields": "कृपया सबै क्षेत्रहरू भर्नुहोस्",
+ "submit": "पेश गर्नुहोस्",
+ "exit": "बाहिर निस्कनुहोस्",
+ "previous": "पूर्ववत",
+ "next": "अरू",
+ "done": "गरिएको",
+ "step_1": "कदम 1",
+ "first_go_to": "पहिलो, जानुहोस्",
+ "login_if_not_logged_in": "र लगइन/साइनअप गर्नुहोस् जुन तपाईंले लगइन गरेनन्",
+ "step_2": "कदम 2",
+ "step_2_steps": "1. एकबार तपाईं लगइन गरे पछि, F12 थिच्नुहोस् वा माउस राइट क्लिक गर्नुहोस् > इन्स्पेक्ट गर्नुहोस् भने ब्राउजर डेभटुलहरू खुलाउनका लागि।\n2. तपाईंको \"एप्लिकेसन\" ट्याबमा जानुहोस् (Chrome, Edge, Brave इत्यादि) वा \"स्टोरेज\" ट्याबमा जानुहोस् (Firefox, Palemoon इत्यादि)\n3. तपाईंको इन्सेक्ट गरेको ब्राउजर डेभटुलहरूमा \"कुकीहरू\" खण्डमा जानुहोस् अनि \"https://accounts.spotify.com\" उपकोणमा जानुहोस्",
+ "step_3": "कदम 3",
+ "step_3_steps": "\"sp_dc\" र \"sp_key\" (वा sp_gaid) कुकीहरूको मानहरू प्रतिलिपि गर्नुहोस्",
+ "success_emoji": "सफलता 🥳",
+ "success_message": "हाम्रो सानो भाइ, अब तपाईं सफलतापूर्वक आफ्नो Spotify खातामा लगइन गरेका छौं। राम्रो काम गरेको!",
+ "step_4": "कदम 4",
+ "step_4_steps": "प्रतिलिपि गरेको \"sp_dc\" र \"sp_key\" (वा sp_gaid) मानहरूलाई आफ्नो ठाउँमा पेस्ट गर्नुहोस्",
+ "something_went_wrong": "केहि गल्ति भएको छ",
+ "piped_instance": "पाइपड सर्भर इन्स्ट्यान्स",
+ "piped_description": "गीत मिलाउको लागि प्रयोग गर्ने पाइपड सर्भर इन्स्ट्यान्स",
+ "piped_warning": "तिनीहरूमध्ये केहि ठिक गर्न सक्छ। यसलाई आफ्नो जोखिममा प्रयोग गर्नुहोस्",
+ "generate_playlist": "प्लेलिस्ट बनाउनुहोस्",
+ "track_exists": "ट्र्याक {track} पहिले नै छ",
+ "replace_downloaded_tracks": "सबै डाउनलोड गरिएका ट्र्याकहरूलाई परिवर्तन गर्नुहोस्",
+ "skip_download_tracks": "सबै डाउनलोड गरिएका ट्र्याकहरूलाई छोड्नुहोस्",
+ "do_you_want_to_replace": "के तपाईंले वर्तमान ट्र्याकलाई परिवर्तन गर्न चाहनुहुन्छ?",
+ "replace": "परिवर्तन गर्नुहोस्",
+ "skip": "छोड्नुहोस्",
+ "select_up_to_count_type": "{count} {type} सम्म चयन गर्नुहोस्",
+ "select_genres": "जनरहरू चयन गर्नुहोस्",
+ "add_genres": "जनरहरू थप्नुहोस्",
+ "country": "देश",
+ "number_of_tracks_generate": "बनाउनका लागि ट्र्याकहरूको संख्या",
+ "acousticness": "एकोस्टिकनेस",
+ "danceability": "नृत्यक्षमता",
+ "energy": "ऊर्जा",
+ "instrumentalness": "साजा रहेकोता",
+ "liveness": "प्राणिकता",
+ "loudness": "शोर",
+ "speechiness": "भाषण",
+ "valence": "मानसिक स्वभाव",
+ "popularity": "लोकप्रियता",
+ "key": "कुञ्जी",
+ "duration": "अवधि (सेकेण्ड)",
+ "tempo": "गति (बीपीएम)",
+ "mode": "मोड",
+ "time_signature": "समय हस्ताक्षर",
+ "short": "सानो",
+ "medium": "मध्यम",
+ "long": "लामो",
+ "min": "न्यून",
+ "max": "अधिक",
+ "target": "लक्ष्य",
+ "moderate": "मध्यस्थ",
+ "deselect_all": "सबै छान्नुहोस्",
+ "select_all": "सबै चयन गर्नुहोस्",
+ "are_you_sure": "के तपाईं सुनिश्चित हुनुहुन्छ?",
+ "generating_playlist": "तपाईंको विशेष प्लेलिस्ट बनाइएको छ...",
+ "selected_count_tracks": "{count} ट्र्याकहरू छन् चयन गरिएका",
+ "download_warning": "यदि तपाईं सबै ट्र्याकहरूलाई बल्कमा डाउनलोड गर्छनु हो भने तपाईं स्पष्ट रूपमा साङ्गीत चोरी गरिरहेका छन् र यो साङ्गीतको रचनात्मक समाजलाई क्षति पनि पुर्याउँछ। उमेराइएको छ कि तपाईं यसको बारेमा जागरूक छिनुहुन्छ। सधैं, कला गर्दै र कलाकारको कडा परम्परा समर्थन गर्दै आइन्छ।",
+ "download_ip_ban_warning": "बितिएका डाउनलोड अनुरोधहरूका कारण तपाईंको आइपीले YouTube मा ब्लक हुन सक्छ। आइपी ब्लक भनेको कम्तीमा 2-3 महिनासम्म तपाईं त्यस आइपी यन्त्रबाट YouTube प्रयोग गर्न सक्नुहुन्छ। र यदि यो हुँदैछ भने स्पट्यूबले यसलाई कसैले गरेको बारेमा कुनै दायित्व लिन्छैन।",
+ "by_clicking_accept_terms": "'स्वीकृत' गरेर तपाईं निम्नलिखित निर्वाचन गर्दैछिन्:",
+ "download_agreement_1": "म मन्ने छु कि म साङ्गीत चोरी गरिरहेको छु। म बुरो हुँ",
+ "download_agreement_2": "म कहिल्यै कहिल्यै तिनीहरूलाई समर्थन गर्नेछु र म यो तिनीहरूको कला किन्ने पैसा छैन भने मा मात्र यो गरेको छु",
+ "download_agreement_3": "म पूरा रूपमा जान्छु कि मेरो आइपी YouTube मा ब्लक हुन सक्छ र म मन्छेहरूले मेरो चासोबाट भएको कुनै दुर्घटनामा स्पट्यूब वा तिनीहरूको मालिकहरू/सहयोगीहरूलाई दायित्वी ठान्छुँभन्ने पूर्ण जानकारी छैन",
+ "decline": "अस्वीकृत",
+ "accept": "स्वीकृत",
+ "details": "विवरण",
+ "youtube": "YouTube",
+ "channel": "च्यानल",
+ "likes": "लाइकहरू",
+ "dislikes": "असुनुहरू",
+ "views": "हेरिएको",
+ "streamUrl": "स्ट्रिम यूआरएल",
+ "stop": "रोक्नुहोस्",
+ "sort_newest": "नयाँ थपिएकोमा क्रमबद्ध गर्नुहोस्",
+ "sort_oldest": "पुरानो थपिएकोमा क्रमबद्ध गर्नुहोस्",
+ "sleep_timer": "सुत्ने टाइमर",
+ "mins": "{minutes} मिनेटहरू",
+ "hours": "{hours} घण्टाहरू",
+ "hour": "{hours} घण्टा",
+ "custom_hours": "कस्टम घण्टाहरू",
+ "logs": "लगहरू",
+ "developers": "डेभेलपर्स",
+ "not_logged_in": "तपाईंले लगइन गरेका छैनौं",
+ "search_mode": "खोज मोड",
+ "audio_source": "अडियो स्रोत",
+ "ok": "ठिक छ",
+ "failed_to_encrypt": "एन्क्रिप्ट गर्न सकिएन",
+ "encryption_failed_warning": "स्पट्यूबले तपाईंको डेटा सुरक्षित रूपमा स्टोर गर्नका लागि एन्क्रिप्ट गर्न खोजेको छ। तर यसले गरेको छैन। यसले असुरक्षित स्टोरेजमा फल्लब्याक गर्दछ\nयदि तपाईंले लिनक्स प्रयोग गरिरहेका छन् भने कृपया सुनिश्चित गर्नुहोस् कि तपाईंले कुनै सीक्रेट-सर्भिस (गोनोम-किरिङ, केडीइ-वालेट, किपासेक्ससि इत्यादि) इन्स्टल गरेका छौं",
+ "querying_info": "जानकारी हेर्दै...",
+ "piped_api_down": "पाइपड एपीआई डाउन छ",
+ "piped_down_error_instructions": "पाइपड इन्स्ट्यान्स {pipedInstance} हाल डाउन छ\n\nजीसनै इन्स्ट्यान्स परिवर्तन गर्नुहोस् वा 'एपीआई प्रकार' लाइ YouTube आफिसियल एपीआईमा परिवर्तन गर्नुहोस्\n\nपरिवर्तनपछि एप्लिकेसन पुन: सुरु गर्नुहोस्",
+ "you_are_offline": "तपाईं वर्तमान अफलाइन हुनुहुन्छ",
+ "connection_restored": "तपाईंको इन्टरनेट कनेक्सन पुन: स्थापित भएको छ",
+ "use_system_title_bar": "सिस्टम शीर्षक पट्टी प्रयोग गर्नुहोस्",
+ "crunching_results": "परिणामहरू कपालबाट पीस्दै...",
+ "search_to_get_results": "परिणामहरू प्राप्त गर्नका लागि खोज्नुहोस्",
+ "use_amoled_mode": "कृष्ण ब्ल्याक गाढा थिम प्रयोग गर्नुहोस्",
+ "pitch_dark_theme": "एमोलेड मोड",
+ "normalize_audio": "अडियो सामान्य गर्नुहोस्",
+ "change_cover": "कवर परिवर्तन गर्नुहोस्",
+ "add_cover": "कवर थप्नुहोस्",
+ "restore_defaults": "पूर्वनिर्धारितहरू पुनः स्थापित गर्नुहोस्",
+ "download_music_codec": "साङ्गीत कोडेक डाउनलोड गर्नुहोस्",
+ "streaming_music_codec": "स्ट्रिमिङ साङ्गीत कोडेक",
+ "login_with_lastfm": "लास्ट.एफ.एम सँग लगइन गर्नुहोस्",
+ "connect": "जडान गर्नुहोस्",
+ "disconnect_lastfm": "लास्ट.एफ.एम डिसकनेक्ट गर्नुहोस्",
+ "disconnect": "डिसकनेक्ट",
+ "username": "प्रयोगकर्ता नाम",
+ "password": "पासवर्ड",
+ "login": "लगइन",
+ "login_with_your_lastfm": "तपाईंको लास्ट.एफ.एम खातामा लगइन गर्नुहोस्",
+ "scrobble_to_lastfm": "लास्ट.एफ.एम मा स्क्रबल गर्नुहोस्",
+ "go_to_album": "आल्बममा जानुहोस्",
+ "discord_rich_presence": "डिस्कर्ड धनी उपस्थिति",
+ "browse_all": "सबै हेर्नुहोस्",
+ "genres": "शैलीहरू",
+ "explore_genres": "शैलीहरू अन्वेषण गर्नुहोस्"
+}
\ No newline at end of file
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 6e50c461..23eee51b 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -177,11 +177,9 @@
"step_2": "Stap 2",
"step_2_steps": "1. Zodra je bent aangemeld, druk je op F12 of klik je met de rechtermuisknop > Inspect om de Browser devtools te openen.\n2. Ga vervolgens naar het tabblad \"Toepassing\" (Chrome, Edge, Brave enz..) of naar het tabblad \"Opslag\" (Firefox, Palemoon enz..).\n3. Ga naar de sectie \"Cookies\" en vervolgens naar de subsectie \"https://accounts.spotify.com\".",
"step_3": "Stap 3",
- "step_3_steps": "Kopieer de waarden van \"sp_dc\" en \"sp_key\" (of sp_gaid) Cookies",
"success_emoji": "Succes🥳",
"success_message": "Je bent nu succesvol ingelogd met je Spotify account. Goed gedaan, maat!",
"step_4": "Stap 4",
- "step_4_steps": "Plak de gekopieerde \"sp_dc\" en \"sp_key\" (of sp_gaid) waarden in de respectievelijke velden",
"something_went_wrong": "Er ging iets mis",
"piped_instance": "Piped-serverinstantie",
"piped_description": "De Piped-serverinstantie die moet worden gebruikt voor het matchen van sporen",
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index dd173a37..4ae48338 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -175,11 +175,9 @@
"step_2": "Krok 2",
"step_2_steps": "1. Jeśli jesteś zalogowany, naciśnij klawisz F12 lub Kliknij prawym przyciskiem myszy > Zbadaj, aby odtworzyć narzędzia developerskie.\n2. Następnie przejdź do zakładki \"Application\" (Chrome, Edge, Brave etc..) lub zakładki \"Storage\" (Firefox, Palemoon etc..)\n3. Przejdź do sekcji \"Cookies\" a następnie do pod-sekcji \"https://accounts.spotify.com\"",
"step_3": "Krok 3",
- "step_3_steps": "Skopiuj wartości \"sp_dc\" i \"sp_key\" (lub sp_gaid) Ciasteczek",
"success_emoji": "Sukces!🥳",
"success_message": "Udało ci się zalogować! Dobra robota, stary!",
"step_4": "Krok 4",
- "step_4_steps": "Wklej wartości \"sp_dc\" i \"sp_key\" (lub sp_gaid) do odpowiednich pul.",
"something_went_wrong": "Coś poszło nie tak 🙁",
"piped_instance": "Instancja serwera Piped",
"piped_description": "Instancja serwera Piped używana jest do dopasowania utworów.",
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 705217c1..5ea4cca0 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -175,11 +175,9 @@
"step_2": "Passo 2",
"step_2_steps": "1. Uma vez logado, pressione F12 ou clique com o botão direito do mouse > Inspecionar para abrir as ferramentas de desenvolvimento do navegador.\n2. Em seguida, vá para a guia \"Aplicativo\" (Chrome, Edge, Brave, etc.) ou \"Armazenamento\" (Firefox, Palemoon, etc.)\n3. Acesse a seção \"Cookies\" e depois a subseção \"https://accounts.spotify.com\"",
"step_3": "Passo 3",
- "step_3_steps": "Copie os valores dos Cookies \"sp_dc\" e \"sp_key\" (ou sp_gaid)",
"success_emoji": "Sucesso🥳",
"success_message": "Agora você está logado com sucesso em sua conta do Spotify. Bom trabalho!",
"step_4": "Passo 4",
- "step_4_steps": "Cole os valores copiados \"sp_dc\" e \"sp_key\" (ou sp_gaid) nos campos correspondentes",
"something_went_wrong": "Algo deu errado",
"piped_instance": "Instância do Servidor Piped",
"piped_description": "A instância do servidor Piped a ser usada para correspondência de faixas",
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 32415863..24120d62 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -175,11 +175,9 @@
"step_2": "Шаг 2",
"step_2_steps": "1. После входа в систему нажмите F12 или щелкните правой кнопкой мыши > «Проверить», чтобы открыть инструменты разработчика браузера.\n2. Затем перейдите на вкладку \"Application\" (Chrome, Edge, Brave и т.д..) or \"Storage\" (Firefox, Palemoon и т.д..)\n3. Перейдите в раздел \"Cookies\", а затем в подраздел \"https://accounts.spotify.com\"",
"step_3": "Шаг 3",
- "step_3_steps": "Скопируйте значения \"sp_dc\" и \"sp_key\" (или sp_gaid) Cookies",
"success_emoji": "Успешно 🥳",
"success_message": "Теперь вы успешно вошли в свою учетную запись Spotify. Отличная работа, приятель!",
"step_4": "Шаг 4",
- "step_4_steps": "Вставьте скопированные \"sp_dc\" и \"sp_key\" (или sp_gaid) значения в соответствующие поля",
"something_went_wrong": "Что-то пошло не так",
"piped_instance": "Экземпляр сервера Piped",
"piped_description": "Серверный экземпляр Piped для сопоставления треков",
diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb
index 63646af6..e6b0ce34 100644
--- a/lib/l10n/app_tr.arb
+++ b/lib/l10n/app_tr.arb
@@ -177,11 +177,9 @@
"step_2": "2. Adım",
"step_2_steps": "1. Giriş yaptıktan sonra, Tarayıcı devtools.\n2'yi açmak için F12'ye basın veya Fare Sağ Tıklaması > İncele'ye basın. Ardından \"Uygulama\" Sekmesine (Chrome, Edge, Brave vb.) veya \"Depolama\" Sekmesine (Firefox, Palemoon vb.) gidin\n3. \"Çerezler\" bölümüne ve ardından \"https://accounts.spotify.com\" alt bölümüne gidin",
"step_3": "3. Adım",
- "step_3_steps": "\"sp_dc\" ve \"sp_key\" (veya sp_gaid) Çerezlerinin değerlerini kopyalayın",
"success_emoji": "Başarılı🥳",
"success_message": "Şimdi Spotify hesabınızla başarılı bir şekilde oturum açtınız. İyi iş, dostum!",
"step_4": "4. Adım",
- "step_4_steps": "Kopyalanan \"sp_dc\" ve \"sp_key\" (veya sp_gaid) değerlerini ilgili alanlara yapıştırın",
"something_went_wrong": "Bir şeyler ters gitti",
"piped_instance": "Piped Sunucu Örneği",
"piped_description": "Parça eşleştirme için kullanılacak Piped sunucu örneği",
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index 2ae29237..a5199a04 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -177,11 +177,9 @@
"step_2": "Крок 2",
"step_2_steps": "1. Після входу натисніть F12 або клацніть правою кнопкою миші > Інспектувати, щоб відкрити інструменти розробки браузера.\n2. Потім перейдіть на вкладку 'Програма' (Chrome, Edge, Brave тощо) або вкладку 'Сховище' (Firefox, Palemoon тощо).\n3. Перейдіть до розділу 'Кукі-файли', а потім до підрозділу 'https://accounts.spotify.com'",
"step_3": "Крок 3",
- "step_3_steps": "Скопіюйте значення кукі-файлів 'sp_dc' та 'sp_key' (або sp_gaid)",
"success_emoji": "Успіх🥳",
"success_message": "Тепер ви успішно ввійшли у свій обліковий запис Spotify. Гарна робота, друже!",
"step_4": "Крок 4",
- "step_4_steps": "Вставте скопійовані значення 'sp_dc' та 'sp_key' (або sp_gaid) у відповідні поля",
"something_went_wrong": "Щось пішло не так",
"piped_instance": "Примірник сервера Piped",
"piped_description": "Примірник сервера Piped, який використовуватиметься для зіставлення треків",
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 85b57724..30f4a82c 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -175,11 +175,9 @@
"step_2": "步骤 2",
"step_2_steps": "1. 一旦你已经完成登录, 按 F12 键或者鼠标右击网页空白区域 > 选择“检查”以打开浏览器开发者工具(DevTools)\n2. 然后选择 \"应用(Application)\" 标签页(Chrome, Edge, Brave 等基于 Chromium 的浏览器) 或 \"存储(Storage)\" 标签页 (Firefox, Palemoon 等基于 Firefox 的浏览器))\n3. 选择 \"Cookies\" 栏目然后选择 \"https://accounts.spotify.com\" 子栏目",
"step_3": "步骤 3",
- "step_3_steps": "复制名称为 \"sp_dc\" 和 \"sp_key\" (或 sp_gaid) 的值(Cookie Value)",
"success_emoji": "成功🥳",
"success_message": "你已经成功使用 Spotify 登录。干得漂亮!",
"step_4": "步骤 4",
- "step_4_steps": "将 \"sp_dc\" 与 \"sp_key\" (或 sp_gaid) 的值分别复制后粘贴到对应的区域",
"something_went_wrong": "某些地方出现了问题",
"piped_instance": "管道服务器实例",
"piped_description": "管道服务器实例用于匹配歌曲",
diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart
index 47e5eb99..335be545 100644
--- a/lib/l10n/l10n.dart
+++ b/lib/l10n/l10n.dart
@@ -21,6 +21,7 @@ class L10n {
const Locale('es', 'ES'),
const Locale("fa", "IR"),
const Locale('fr', 'FR'),
+ const Locale('ne', 'NP'),
const Locale('hi', 'IN'),
const Locale('it', 'IT'),
const Locale('ja', 'JP'),
diff --git a/lib/main.dart b/lib/main.dart
index f96920a1..b6afa85c 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -228,7 +228,9 @@ class SpotubeState extends ConsumerState {
builder: (context, child) {
return DevicePreview.appBuilder(
context,
- DragToResizeArea(child: child!),
+ DesktopTools.platform.isDesktop
+ ? DragToResizeArea(child: child!)
+ : child,
);
},
themeMode: themeMode,
diff --git a/lib/models/spotify_friends.dart b/lib/models/spotify_friends.dart
new file mode 100644
index 00000000..b386fb81
--- /dev/null
+++ b/lib/models/spotify_friends.dart
@@ -0,0 +1,111 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'spotify_friends.g.dart';
+
+@JsonSerializable(createToJson: false)
+class SpotifyFriend {
+ final String uri;
+ final String name;
+ final String imageUrl;
+
+ const SpotifyFriend({
+ required this.uri,
+ required this.name,
+ required this.imageUrl,
+ });
+
+ factory SpotifyFriend.fromJson(Map json) =>
+ _$SpotifyFriendFromJson(json);
+
+ String get id => uri.split(":").last;
+}
+
+@JsonSerializable(createToJson: false)
+class SpotifyActivityArtist {
+ final String uri;
+ final String name;
+
+ const SpotifyActivityArtist({required this.uri, required this.name});
+
+ factory SpotifyActivityArtist.fromJson(Map json) =>
+ _$SpotifyActivityArtistFromJson(json);
+
+ String get id => uri.split(":").last;
+}
+
+@JsonSerializable(createToJson: false)
+class SpotifyActivityAlbum {
+ final String uri;
+ final String name;
+
+ const SpotifyActivityAlbum({required this.uri, required this.name});
+
+ factory SpotifyActivityAlbum.fromJson(Map json) =>
+ _$SpotifyActivityAlbumFromJson(json);
+
+ String get id => uri.split(":").last;
+}
+
+@JsonSerializable(createToJson: false)
+class SpotifyActivityContext {
+ final String uri;
+ final String name;
+ final num index;
+
+ const SpotifyActivityContext({
+ required this.uri,
+ required this.name,
+ required this.index,
+ });
+
+ factory SpotifyActivityContext.fromJson(Map json) =>
+ _$SpotifyActivityContextFromJson(json);
+
+ String get id => uri.split(":").last;
+ String get path => uri.split(":").skip(1).join("/");
+}
+
+@JsonSerializable(createToJson: false)
+class SpotifyActivityTrack {
+ final String uri;
+ final String name;
+ final String imageUrl;
+ final SpotifyActivityArtist artist;
+ final SpotifyActivityAlbum album;
+ final SpotifyActivityContext context;
+
+ const SpotifyActivityTrack({
+ required this.uri,
+ required this.name,
+ required this.imageUrl,
+ required this.artist,
+ required this.album,
+ required this.context,
+ });
+
+ factory SpotifyActivityTrack.fromJson(Map json) =>
+ _$SpotifyActivityTrackFromJson(json);
+
+ String get id => uri.split(":").last;
+}
+
+@JsonSerializable(createToJson: false)
+class SpotifyFriendActivity {
+ SpotifyFriend user;
+ SpotifyActivityTrack track;
+
+ SpotifyFriendActivity({required this.user, required this.track});
+
+ factory SpotifyFriendActivity.fromJson(Map json) =>
+ _$SpotifyFriendActivityFromJson(json);
+}
+
+@JsonSerializable(createToJson: false)
+class SpotifyFriends {
+ List friends;
+
+ SpotifyFriends({required this.friends});
+
+ factory SpotifyFriends.fromJson(Map json) =>
+ _$SpotifyFriendsFromJson(json);
+}
diff --git a/lib/models/spotify_friends.g.dart b/lib/models/spotify_friends.g.dart
new file mode 100644
index 00000000..4a32dd09
--- /dev/null
+++ b/lib/models/spotify_friends.g.dart
@@ -0,0 +1,65 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'spotify_friends.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SpotifyFriend _$SpotifyFriendFromJson(Map json) =>
+ SpotifyFriend(
+ uri: json['uri'] as String,
+ name: json['name'] as String,
+ imageUrl: json['imageUrl'] as String,
+ );
+
+SpotifyActivityArtist _$SpotifyActivityArtistFromJson(
+ Map json) =>
+ SpotifyActivityArtist(
+ uri: json['uri'] as String,
+ name: json['name'] as String,
+ );
+
+SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson(
+ Map json) =>
+ SpotifyActivityAlbum(
+ uri: json['uri'] as String,
+ name: json['name'] as String,
+ );
+
+SpotifyActivityContext _$SpotifyActivityContextFromJson(
+ Map json) =>
+ SpotifyActivityContext(
+ uri: json['uri'] as String,
+ name: json['name'] as String,
+ index: json['index'] as num,
+ );
+
+SpotifyActivityTrack _$SpotifyActivityTrackFromJson(
+ Map json) =>
+ SpotifyActivityTrack(
+ uri: json['uri'] as String,
+ name: json['name'] as String,
+ imageUrl: json['imageUrl'] as String,
+ artist: SpotifyActivityArtist.fromJson(
+ json['artist'] as Map),
+ album:
+ SpotifyActivityAlbum.fromJson(json['album'] as Map),
+ context: SpotifyActivityContext.fromJson(
+ json['context'] as Map),
+ );
+
+SpotifyFriendActivity _$SpotifyFriendActivityFromJson(
+ Map json) =>
+ SpotifyFriendActivity(
+ user: SpotifyFriend.fromJson(json['user'] as Map),
+ track:
+ SpotifyActivityTrack.fromJson(json['track'] as Map),
+ );
+
+SpotifyFriends _$SpotifyFriendsFromJson(Map json) =>
+ SpotifyFriends(
+ friends: (json['friends'] as List)
+ .map((e) => SpotifyFriendActivity.fromJson(e as Map))
+ .toList(),
+ );
diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart
index 92470397..d511cb97 100644
--- a/lib/pages/artist/artist.dart
+++ b/lib/pages/artist/artist.dart
@@ -35,7 +35,7 @@ class ArtistPage extends HookConsumerWidget {
),
extendBodyBehindAppBar: true,
body: Builder(builder: (context) {
- if (artistQuery.hasError) {
+ if (artistQuery.hasError && artistQuery.data == null) {
return Center(child: Text(artistQuery.error.toString()));
}
return Skeletonizer(
diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart
index 9b33a66c..eb2ddb94 100644
--- a/lib/pages/home/home.dart
+++ b/lib/pages/home/home.dart
@@ -3,6 +3,7 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/home/sections/featured.dart';
+import 'package:spotube/components/home/sections/friends.dart';
import 'package:spotube/components/home/sections/genres.dart';
import 'package:spotube/components/home/sections/made_for_user.dart';
import 'package:spotube/components/home/sections/new_releases.dart';
@@ -31,6 +32,7 @@ class HomePage extends HookConsumerWidget {
HomeNewReleasesSection(),
],
),
+ const HomePageFriendsSection(),
const SliverSafeArea(sliver: HomeMadeForUserSection()),
],
),
diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart
index f6eaa5d5..bee5114d 100644
--- a/lib/pages/lyrics/plain_lyrics.dart
+++ b/lib/pages/lyrics/plain_lyrics.dart
@@ -1,12 +1,15 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart';
+import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/lyrics/zoom_controls.dart';
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
@@ -72,10 +75,22 @@ class PlainLyrics extends HookConsumerWidget {
if (lyricsQuery.isLoading || lyricsQuery.isRefreshing) {
return const ShimmerLyrics();
} else if (lyricsQuery.hasError) {
- return Text(
- "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${lyricsQuery.error.toString()}",
- style: textTheme.bodyLarge?.copyWith(
- color: palette.bodyTextColor,
+ return Container(
+ alignment: Alignment.center,
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ context.l10n.no_lyrics_available,
+ style: textTheme.bodyLarge?.copyWith(
+ color: palette.bodyTextColor,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const Gap(26),
+ const Icon(SpotubeIcons.noLyrics, size: 60),
+ ],
),
);
}
diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart
index 04d7c04a..ddef1c65 100644
--- a/lib/pages/lyrics/synced_lyrics.dart
+++ b/lib/pages/lyrics/synced_lyrics.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart' hide Offset;
@@ -7,6 +8,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/lyrics/zoom_controls.dart';
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/components/lyrics/use_synced_lyrics.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
@@ -188,12 +190,19 @@ class SyncedLyrics extends HookConsumerWidget {
child: ShimmerLyrics(),
)
else if (playlist.activeTrack != null &&
- (timedLyricsQuery.hasError))
- Text(
- "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${timedLyricsQuery.error.toString()}",
- style: bodyTextTheme,
- )
- else if (isUnSyncLyric == true)
+ (timedLyricsQuery.hasError)) ...[
+ Container(
+ alignment: Alignment.center,
+ padding: const EdgeInsets.all(16),
+ child: Text(
+ context.l10n.no_lyrics_available,
+ style: bodyTextTheme,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ const Gap(26),
+ const Icon(SpotubeIcons.noLyrics, size: 60),
+ ] else if (isUnSyncLyric == true)
Expanded(
child: Center(
child: RichText(
diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart
index 7ab0ea2a..8b9bce4c 100644
--- a/lib/pages/mobile_login/mobile_login.dart
+++ b/lib/pages/mobile_login/mobile_login.dart
@@ -55,12 +55,7 @@ class WebViewLogin extends HookConsumerWidget {
final cookies =
await CookieManager.instance().getCookies(url: action);
final cookieHeader =
- cookies.fold("", (previousValue, element) {
- if (element.name == "sp_dc" || element.name == "sp_key") {
- return "$previousValue; ${element.name}=${element.value}";
- }
- return previousValue;
- });
+ "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(cookieHeader),
diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart
index 5972a303..1fb2e1dc 100644
--- a/lib/pages/playlist/liked_playlist.dart
+++ b/lib/pages/playlist/liked_playlist.dart
@@ -4,7 +4,6 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/tracks_view/track_view.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
import 'package:spotube/services/queries/queries.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
class LikedPlaylistPage extends HookConsumerWidget {
final PlaylistSimple playlist;
@@ -20,10 +19,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
return InheritedTrackView(
collectionId: playlist.id!,
- image: TypeConversionUtils.image_X_UrlString(
- playlist.images,
- placeholder: ImagePlaceholder.collection,
- ),
+ image: "assets/liked-tracks.jpg",
pagination: PaginationProps(
hasNextPage: false,
isLoading: false,
diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart
new file mode 100644
index 00000000..14052c10
--- /dev/null
+++ b/lib/pages/track/track.dart
@@ -0,0 +1,227 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:skeletonizer/skeletonizer.dart';
+import 'package:spotube/collections/fake.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/shared/heart_button.dart';
+import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/components/shared/links/link_text.dart';
+import 'package:spotube/components/shared/page_window_title_bar.dart';
+import 'package:spotube/components/shared/track_tile/track_options.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
+import 'package:spotube/services/audio_player/audio_player.dart';
+import 'package:spotube/services/queries/queries.dart';
+import 'package:spotube/utils/type_conversion_utils.dart';
+import 'package:spotube/extensions/constrains.dart';
+
+class TrackPage extends HookConsumerWidget {
+ final String trackId;
+ const TrackPage({
+ Key? key,
+ required this.trackId,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
+ final mediaQuery = MediaQuery.of(context);
+
+ final playlist = ref.watch(ProxyPlaylistNotifier.provider);
+ final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
+
+ final isActive = playlist.activeTrack?.id == trackId;
+
+ final trackQuery = useQueries.tracks.track(ref, trackId);
+
+ final track = trackQuery.data ?? FakeData.track;
+
+ void onPlay() async {
+ if (isActive) {
+ audioPlayer.pause();
+ } else {
+ await playlistNotifier.load([track], autoPlay: true);
+ }
+ }
+
+ return Scaffold(
+ appBar: const PageWindowTitleBar(
+ automaticallyImplyLeading: true,
+ backgroundColor: Colors.transparent,
+ ),
+ extendBodyBehindAppBar: true,
+ body: Stack(
+ children: [
+ Positioned.fill(
+ child: Container(
+ decoration: BoxDecoration(
+ image: DecorationImage(
+ image: UniversalImage.imageProvider(
+ TypeConversionUtils.image_X_UrlString(
+ track.album!.images,
+ placeholder: ImagePlaceholder.albumArt,
+ ),
+ ),
+ fit: BoxFit.cover,
+ colorFilter: ColorFilter.mode(
+ colorScheme.surface.withOpacity(0.5),
+ BlendMode.srcOver,
+ ),
+ alignment: Alignment.topCenter,
+ ),
+ ),
+ ),
+ ),
+ Positioned.fill(
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
+ child: Skeletonizer(
+ enabled: trackQuery.isLoading,
+ child: Container(
+ alignment: Alignment.topCenter,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ colorScheme.surface,
+ Colors.transparent,
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ stops: const [0.2, 1],
+ ),
+ ),
+ child: SafeArea(
+ child: Wrap(
+ spacing: 20,
+ runSpacing: 20,
+ alignment: WrapAlignment.center,
+ crossAxisAlignment: WrapCrossAlignment.center,
+ runAlignment: WrapAlignment.center,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: UniversalImage(
+ path: TypeConversionUtils.image_X_UrlString(
+ track.album!.images,
+ placeholder: ImagePlaceholder.albumArt,
+ ),
+ height: 200,
+ width: 200,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Column(
+ crossAxisAlignment: mediaQuery.smAndDown
+ ? CrossAxisAlignment.center
+ : CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ track.name!,
+ style: textTheme.titleLarge,
+ ),
+ const Gap(10),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(SpotubeIcons.album),
+ const Gap(5),
+ Flexible(
+ child: LinkText(
+ track.album!.name!,
+ '/album/${track.album!.id}',
+ push: true,
+ extra: track.album,
+ ),
+ ),
+ ],
+ ),
+ const Gap(10),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(SpotubeIcons.artist),
+ const Gap(5),
+ TypeConversionUtils
+ .artists_X_ClickableArtists(
+ track.artists!,
+ ),
+ ],
+ ),
+ const Gap(10),
+ ConstrainedBox(
+ constraints:
+ const BoxConstraints(maxWidth: 350),
+ child: Row(
+ mainAxisSize: mediaQuery.smAndDown
+ ? MainAxisSize.max
+ : MainAxisSize.min,
+ children: [
+ const Gap(5),
+ if (!isActive &&
+ !playlist.tracks.contains(track))
+ OutlinedButton.icon(
+ icon: const Icon(SpotubeIcons.queueAdd),
+ label: Text(context.l10n.queue),
+ onPressed: () {
+ playlistNotifier.addTrack(track);
+ },
+ ),
+ const Gap(5),
+ if (!isActive &&
+ !playlist.tracks.contains(track))
+ IconButton.outlined(
+ icon:
+ const Icon(SpotubeIcons.lightning),
+ tooltip: context.l10n.play_next,
+ onPressed: () {
+ playlistNotifier
+ .addTracksAtFirst([track]);
+ },
+ ),
+ const Gap(5),
+ IconButton.filled(
+ tooltip: isActive
+ ? context.l10n.pause_playback
+ : context.l10n.play,
+ icon: Icon(
+ isActive
+ ? SpotubeIcons.pause
+ : SpotubeIcons.play,
+ color: colorScheme.onPrimary,
+ ),
+ onPressed: onPlay,
+ ),
+ const Gap(5),
+ if (mediaQuery.smAndDown)
+ const Spacer()
+ else
+ const Gap(20),
+ TrackHeartButton(track: track),
+ TrackOptions(
+ track: track,
+ userPlaylist: false,
+ ),
+ const Gap(5),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart
index 4a55130a..e27b701b 100644
--- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart
+++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:spotify/spotify.dart';
+import 'package:spotube/models/spotify_friends.dart';
class CustomSpotifyEndpoints {
static const _baseUrl = 'https://api.spotify.com/v1';
@@ -162,4 +163,71 @@ class CustomSpotifyEndpoints {
result["tracks"].map((track) => Track.fromJson(track)).toList(),
);
}
+
+ Future getFriendActivity() async {
+ final res = await _client.get(
+ Uri.parse("https://guc-spclient.spotify.com/presence-view/v1/buddylist"),
+ headers: {
+ "content-type": "application/json",
+ "authorization": "Bearer $accessToken",
+ "accept": "application/json",
+ },
+ );
+ return SpotifyFriends.fromJson(jsonDecode(res.body));
+ }
+
+ Future artist({required String id}) async {
+ final pathQuery = "$_baseUrl/artists/$id";
+
+ final res = await _client.get(
+ Uri.parse(pathQuery),
+ headers: {
+ "content-type": "application/json",
+ if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken",
+ "accept": "application/json",
+ },
+ );
+ final data = jsonDecode(res.body);
+
+ return Artist.fromJson(_purifyArtistResponse(data));
+ }
+
+ Future> relatedArtists({required String id}) async {
+ final pathQuery = "$_baseUrl/artists/$id/related-artists";
+
+ final res = await _client.get(
+ Uri.parse(pathQuery),
+ headers: {
+ "content-type": "application/json",
+ if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken",
+ "accept": "application/json",
+ },
+ );
+
+ final data = jsonDecode(res.body);
+
+ return List.castFrom(
+ data["artists"]
+ .map((artist) => Artist.fromJson(_purifyArtistResponse(artist)))
+ .toList(),
+ );
+ }
+
+ Map _purifyArtistResponse(Map data) {
+ if (data["popularity"] != null) {
+ data["popularity"] = data["popularity"].toInt();
+ }
+ if (data["followers"]?["total"] != null) {
+ data["followers"]["total"] = data["followers"]["total"].toInt();
+ }
+ if (data["images"] != null) {
+ data["images"] = data["images"].map((e) {
+ e["height"] = e["height"].toInt();
+ e["width"] = e["width"].toInt();
+ return e;
+ }).toList();
+ }
+
+ return data;
+ }
}
diff --git a/lib/services/download_manager/chunked_download.dart b/lib/services/download_manager/chunked_download.dart
index b2849a3c..9e5e0a98 100644
--- a/lib/services/download_manager/chunked_download.dart
+++ b/lib/services/download_manager/chunked_download.dart
@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
-import 'package:flutter/foundation.dart';
import 'package:spotube/models/logger.dart';
final logger = getLogger("ChunkedDownload");
diff --git a/lib/services/download_manager/download_manager.dart b/lib/services/download_manager/download_manager.dart
index 904f06cf..d7a42430 100644
--- a/lib/services/download_manager/download_manager.dart
+++ b/lib/services/download_manager/download_manager.dart
@@ -6,6 +6,8 @@ import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
+import 'package:path/path.dart' as path;
+import 'package:path_provider/path_provider.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/download_manager/chunked_download.dart';
import 'package:spotube/services/download_manager/download_request.dart';
@@ -77,7 +79,18 @@ class DownloadManager {
logger.d("[DownloadManager] $url");
final file = File(savePath.toString());
- partialFilePath = savePath + partialExtension;
+
+ final tmpDirPath = await Directory(
+ path.join(
+ (await getTemporaryDirectory()).path,
+ "spotube-downloads",
+ ),
+ ).create(recursive: true);
+
+ partialFilePath = path.join(
+ tmpDirPath.path,
+ path.basename(savePath) + partialExtension,
+ );
partialFile = File(partialFilePath);
final fileExist = await file.exists();
@@ -111,7 +124,9 @@ class DownloadManager {
await ioSink.addStream(partialChunkFile.openRead());
await partialChunkFile.delete();
await ioSink.close();
- await partialFile.rename(savePath);
+
+ await partialFile.copy(savePath);
+ await partialFile.delete();
setStatus(task, DownloadStatus.completed);
}
@@ -125,7 +140,8 @@ class DownloadManager {
);
if (response.statusCode == HttpStatus.ok) {
- await partialFile.rename(savePath);
+ await partialFile.copy(savePath);
+ await partialFile.delete();
setStatus(task, DownloadStatus.completed);
}
}
diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart
index 1b939c82..5ccc4955 100644
--- a/lib/services/queries/artist.dart
+++ b/lib/services/queries/artist.dart
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
+import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/wikipedia/wikipedia.dart';
import 'package:wikipedia_api/wikipedia_api.dart';
@@ -15,9 +16,10 @@ class ArtistQueries {
WidgetRef ref,
String artist,
) {
+ final customSpotify = ref.watch(customSpotifyEndpointProvider);
return useSpotifyQuery(
"artist-profile/$artist",
- (spotify) => spotify.artists.get(artist),
+ (spotify) => customSpotify.artist(id: artist),
ref: ref,
);
}
@@ -125,10 +127,11 @@ class ArtistQueries {
WidgetRef ref,
String artist,
) {
+ final customSpotify = ref.watch(customSpotifyEndpointProvider);
return useSpotifyQuery, dynamic>(
"artist-related-artist-query/$artist",
(spotify) {
- return spotify.artists.relatedArtists(artist);
+ return customSpotify.relatedArtists(id: artist);
},
ref: ref,
);
diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart
index faa5bdec..618f960f 100644
--- a/lib/services/queries/lyrics.dart
+++ b/lib/services/queries/lyrics.dart
@@ -63,8 +63,8 @@ class LyricsQueries {
/// Special thanks to [raptag](https://github.com/raptag) for discovering this
/// jem
- Query spotifySynced(WidgetRef ref, Track? track) {
- return useSpotifyQuery(
+ Query spotifySynced(WidgetRef ref, Track? track) {
+ return useSpotifyQuery(
"spotify-synced-lyrics/${track?.id}}",
(spotify) async {
if (track == null) {
diff --git a/lib/services/queries/queries.dart b/lib/services/queries/queries.dart
index cc3ce132..30c23268 100644
--- a/lib/services/queries/queries.dart
+++ b/lib/services/queries/queries.dart
@@ -4,6 +4,7 @@ import 'package:spotube/services/queries/category.dart';
import 'package:spotube/services/queries/lyrics.dart';
import 'package:spotube/services/queries/playlist.dart';
import 'package:spotube/services/queries/search.dart';
+import 'package:spotube/services/queries/tracks.dart';
import 'package:spotube/services/queries/user.dart';
import 'package:spotube/services/queries/views.dart';
@@ -17,6 +18,7 @@ class Queries {
final search = const SearchQueries();
final user = const UserQueries();
final views = const ViewsQueries();
+ final tracks = const TracksQueries();
}
const useQueries = Queries._();
diff --git a/lib/services/queries/tracks.dart b/lib/services/queries/tracks.dart
new file mode 100644
index 00000000..52bab984
--- /dev/null
+++ b/lib/services/queries/tracks.dart
@@ -0,0 +1,16 @@
+import 'package:fl_query/fl_query.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/hooks/spotify/use_spotify_query.dart';
+
+class TracksQueries {
+ const TracksQueries();
+
+ Query