feat: add user profile page

This commit is contained in:
Kingkor Roy Tirtho 2024-04-13 13:05:41 +06:00
parent f82253c6ba
commit 39e97eef34
5 changed files with 220 additions and 108 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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),
],
)

View File

@ -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),
),
),
],
)
],
),
),
),
),
),
],
),
),
),
);
}
}

View File

@ -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<Duration?> get duration async {
return _mkPlayer.state.duration;
// if (mkSupportedPlatform) {
// } else {
// return _justAudio!.duration;
// }
}
Future<Duration?> get position async {
return _mkPlayer.state.position;
// if (mkSupportedPlatform) {
// } else {
// return _justAudio!.position;
// }
}
Future<Duration?> 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<mk.AudioDevice> 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<bool> get isCompleted async {
return _mkPlayer.state.completed;
// if (mkSupportedPlatform) {
// return _mkPlayer.state.completed;
// } else {
// return _justAudio!.processingState == ja.ProcessingState.completed;
// }
}
Future<bool> 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;
}
}