diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 5b3a8ed7..aeeb4837 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -18,6 +18,7 @@ import 'package:spotube/pages/library/playlist_generate/playlist_generate_result import 'package:spotube/pages/lyrics/mini_lyrics.dart'; import 'package:spotube/pages/playlist/liked_playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart'; +import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; @@ -175,20 +176,26 @@ final routerProvider = Provider((ref) { }, ), GoRoute( - path: "/connect", - pageBuilder: (context, state) => const SpotubePage( - child: ConnectPage(), - ), - routes: [ - GoRoute( - path: "control", - pageBuilder: (context, state) { - return const SpotubePage( - child: ConnectControlPage(), - ); - }, - ) - ]) + path: "/connect", + pageBuilder: (context, state) => const SpotubePage( + child: ConnectPage(), + ), + routes: [ + GoRoute( + path: "control", + pageBuilder: (context, state) { + return const SpotubePage( + child: ConnectControlPage(), + ); + }, + ) + ], + ), + GoRoute( + path: "/profile", + pageBuilder: (context, state) => + const SpotubePage(child: ProfilePage()), + ) ], ), GoRoute( diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index f49a9c0d..a100ca8e 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -23,6 +23,7 @@ import 'package:spotube/provider/spotify/spotify.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:spotube/utils/service_utils.dart'; class Sidebar extends HookConsumerWidget { final int? selectedIndex; @@ -275,29 +276,35 @@ class SidebarFooter extends HookConsumerWidget { const CircularProgressIndicator() else if (data != null) Flexible( - child: Row( - children: [ - CircleAvatar( - backgroundImage: - UniversalImage.imageProvider(avatarImg), - onBackgroundImageError: (exception, stackTrace) => - Assets.userPlaceholder.image( - height: 16, - width: 16, + child: InkWell( + onTap: () { + ServiceUtils.push(context, "/profile"); + }, + borderRadius: BorderRadius.circular(30), + child: Row( + children: [ + CircleAvatar( + backgroundImage: + UniversalImage.imageProvider(avatarImg), + onBackgroundImageError: (exception, stackTrace) => + Assets.userPlaceholder.image( + height: 16, + width: 16, + ), ), - ), - const SizedBox(width: 10), - Flexible( - child: Text( - data.displayName ?? context.l10n.guest, - maxLines: 1, - softWrap: false, - overflow: TextOverflow.fade, - style: theme.textTheme.bodyMedium - ?.copyWith(fontWeight: FontWeight.bold), + const SizedBox(width: 10), + Flexible( + child: Text( + data.displayName ?? context.l10n.guest, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.fade, + style: theme.textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), ), - ), - ], + ], + ), ), ), IconButton( diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 7b70794d..5b959621 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -3,16 +3,19 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/connect/connect_device.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'; +import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/service_utils.dart'; class HomePage extends HookConsumerWidget { const HomePage({super.key}); @@ -34,10 +37,26 @@ class HomePage extends HookConsumerWidget { actions: [ const ConnectDeviceButton(), const Gap(10), - IconButton.filledTonal( - icon: const Icon(SpotubeIcons.user), - onPressed: () {}, - ), + Consumer(builder: (context, ref, _) { + final me = ref.watch(meProvider); + final meData = me.asData?.value; + + return IconButton( + icon: CircleAvatar( + backgroundImage: UniversalImage.imageProvider( + (meData?.images).asUrlString( + placeholder: ImagePlaceholder.artist, + ), + ), + ), + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + ), + onPressed: () { + ServiceUtils.push(context, "/profile"); + }, + ); + }), const Gap(10), ], ) diff --git a/lib/pages/profile/profile.dart b/lib/pages/profile/profile.dart new file mode 100644 index 00000000..52b69835 --- /dev/null +++ b/lib/pages/profile/profile.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:sliver_tools/sliver_tools.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/collections/spotify_markets.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/extensions/image.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class ProfilePage extends HookConsumerWidget { + const ProfilePage({super.key}); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme) = Theme.of(context); + + final me = ref.watch(meProvider); + final meData = me.asData?.value ?? FakeData.user; + + final userProperties = useMemoized( + () => { + "Email": meData.email ?? "N/A", + "Followers": meData.followers?.total.toString() ?? "N/A", + "Birthday": meData.birthdate ?? "Not born", + "Country": spotifyMarkets + .firstWhere((market) => market.$1 == meData.country) + .$2, + "Subscription": meData.product ?? "Hacker", + }, + [meData], + ); + + return SafeArea( + child: Scaffold( + appBar: const PageWindowTitleBar( + title: Text("Profile"), + titleSpacing: 0, + automaticallyImplyLeading: true, + centerTitle: false, + ), + body: Skeletonizer( + enabled: me.isLoading, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(600), + child: UniversalImage( + path: meData.images.asUrlString( + index: 1, + placeholder: ImagePlaceholder.artist, + ), + width: 300, + height: 300, + fit: BoxFit.cover, + ), + ), + ], + ), + ), + const SliverGap(10), + SliverToBoxAdapter( + child: Text( + meData.displayName ?? "No Name", + style: textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ), + const SliverGap(20), + SliverCrossAxisConstrained( + maxCrossAxisExtent: 500, + child: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + label: const Text("Edit"), + icon: const Icon(SpotubeIcons.edit), + onPressed: () { + launchUrlString( + "https://www.spotify.com/account/profile/", + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), + SliverCrossAxisConstrained( + maxCrossAxisExtent: 500, + child: SliverToBoxAdapter( + child: Card( + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Table( + columnWidths: const { + 0: FixedColumnWidth(110), + }, + children: [ + for (final MapEntry(:key, :value) + in userProperties.entries) + TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.all(6), + child: Text( + key, + style: textTheme.titleSmall, + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.all(6), + child: Text(value), + ), + ), + ], + ) + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index d5ebddb4..a81c6c95 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -7,7 +7,6 @@ import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; -// import 'package:just_audio/just_audio.dart' as ja; import 'dart:async'; import 'package:media_kit/media_kit.dart' as mk; @@ -43,7 +42,6 @@ class SpotubeMedia extends mk.Media { abstract class AudioPlayerInterface { final CustomPlayer _mkPlayer; - // final ja.AudioPlayer? _justAudxio; AudioPlayerInterface() : _mkPlayer = CustomPlayer( @@ -51,9 +49,7 @@ abstract class AudioPlayerInterface { title: "Spotube", logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, ), - ) - // _justAudio = !_mkSupportedPlatform ? ja.AudioPlayer() : null - { + ) { _mkPlayer.stream.error.listen((event) { Catcher2.reportCheckedError(event, StackTrace.current); }); @@ -61,33 +57,19 @@ abstract class AudioPlayerInterface { /// Whether the current platform supports the audioplayers plugin static const bool _mkSupportedPlatform = true; - // DesktopTools.platform.isWindows || DesktopTools.platform.isLinux; bool get mkSupportedPlatform => _mkSupportedPlatform; Future get duration async { return _mkPlayer.state.duration; - // if (mkSupportedPlatform) { - // } else { - // return _justAudio!.duration; - // } } Future get position async { return _mkPlayer.state.position; - // if (mkSupportedPlatform) { - // } else { - // return _justAudio!.position; - // } } Future get bufferedPosition async { - if (mkSupportedPlatform) { - // audioplayers doesn't have the capability to get buffered position - return null; - } else { - return null; - } + return _mkPlayer.state.buffer; } Future get selectedDevice async { @@ -100,86 +82,39 @@ abstract class AudioPlayerInterface { bool get hasSource { return _mkPlayer.state.playlist.medias.isNotEmpty; - // if (mkSupportedPlatform) { - // return _mkPlayer.state.playlist.medias.isNotEmpty; - // } else { - // return _justAudio!.audioSource != null; - // } } // states bool get isPlaying { return _mkPlayer.state.playing; - // if (mkSupportedPlatform) { - // return _mkPlayer.state.playing; - // } else { - // return _justAudio!.playing; - // } } bool get isPaused { return !_mkPlayer.state.playing; - // if (mkSupportedPlatform) { - // return !_mkPlayer.state.playing; - // } else { - // return !isPlaying; - // } } bool get isStopped { return !hasSource; - // if (mkSupportedPlatform) { - // return !hasSource; - // } else { - // return _justAudio!.processingState == ja.ProcessingState.idle; - // } } Future get isCompleted async { return _mkPlayer.state.completed; - // if (mkSupportedPlatform) { - // return _mkPlayer.state.completed; - // } else { - // return _justAudio!.processingState == ja.ProcessingState.completed; - // } } Future get isShuffled async { return _mkPlayer.shuffled; - // if (mkSupportedPlatform) { - // return _mkPlayer.shuffled; - // } else { - // return _justAudio!.shuffleModeEnabled; - // } } PlaybackLoopMode get loopMode { return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode); - // if (mkSupportedPlatform) { - // return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode); - // } else { - // return PlaybackLoopMode.fromLoopMode(_justAudio!.loopMode); - // } } /// Returns the current volume of the player, between 0 and 1 double get volume { return _mkPlayer.state.volume / 100; - // if (mkSupportedPlatform) { - // return _mkPlayer.state.volume / 100; - // } else { - // return _justAudio!.volume; - // } } bool get isBuffering { - return false; - // if (mkSupportedPlatform) { - // // audioplayers doesn't have the capability to get buffering state - // return false; - // } else { - // return _justAudio!.processingState == ja.ProcessingState.buffering || - // _justAudio!.processingState == ja.ProcessingState.loading; - // } + return _mkPlayer.state.buffering; } }