mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: add user profile page
This commit is contained in:
parent
f82253c6ba
commit
39e97eef34
@ -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/lyrics/mini_lyrics.dart';
|
||||||
import 'package:spotube/pages/playlist/liked_playlist.dart';
|
import 'package:spotube/pages/playlist/liked_playlist.dart';
|
||||||
import 'package:spotube/pages/playlist/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/search/search.dart';
|
||||||
import 'package:spotube/pages/settings/blacklist.dart';
|
import 'package:spotube/pages/settings/blacklist.dart';
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
import 'package:spotube/pages/settings/about.dart';
|
||||||
@ -175,20 +176,26 @@ final routerProvider = Provider((ref) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/connect",
|
path: "/connect",
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: ConnectPage(),
|
child: ConnectPage(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "control",
|
path: "control",
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
return const SpotubePage(
|
return const SpotubePage(
|
||||||
child: ConnectControlPage(),
|
child: ConnectControlPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
])
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/profile",
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: ProfilePage()),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
@ -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_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
final int? selectedIndex;
|
final int? selectedIndex;
|
||||||
@ -275,29 +276,35 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
const CircularProgressIndicator()
|
const CircularProgressIndicator()
|
||||||
else if (data != null)
|
else if (data != null)
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Row(
|
child: InkWell(
|
||||||
children: [
|
onTap: () {
|
||||||
CircleAvatar(
|
ServiceUtils.push(context, "/profile");
|
||||||
backgroundImage:
|
},
|
||||||
UniversalImage.imageProvider(avatarImg),
|
borderRadius: BorderRadius.circular(30),
|
||||||
onBackgroundImageError: (exception, stackTrace) =>
|
child: Row(
|
||||||
Assets.userPlaceholder.image(
|
children: [
|
||||||
height: 16,
|
CircleAvatar(
|
||||||
width: 16,
|
backgroundImage:
|
||||||
|
UniversalImage.imageProvider(avatarImg),
|
||||||
|
onBackgroundImageError: (exception, stackTrace) =>
|
||||||
|
Assets.userPlaceholder.image(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 10),
|
||||||
const SizedBox(width: 10),
|
Flexible(
|
||||||
Flexible(
|
child: Text(
|
||||||
child: Text(
|
data.displayName ?? context.l10n.guest,
|
||||||
data.displayName ?? context.l10n.guest,
|
maxLines: 1,
|
||||||
maxLines: 1,
|
softWrap: false,
|
||||||
softWrap: false,
|
overflow: TextOverflow.fade,
|
||||||
overflow: TextOverflow.fade,
|
style: theme.textTheme.bodyMedium
|
||||||
style: theme.textTheme.bodyMedium
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
@ -3,16 +3,19 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/connect/connect_device.dart';
|
||||||
import 'package:spotube/components/home/sections/featured.dart';
|
import 'package:spotube/components/home/sections/featured.dart';
|
||||||
import 'package:spotube/components/home/sections/friends.dart';
|
import 'package:spotube/components/home/sections/friends.dart';
|
||||||
import 'package:spotube/components/home/sections/genres.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/made_for_user.dart';
|
||||||
import 'package:spotube/components/home/sections/new_releases.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/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/extensions/constrains.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/platform.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
@ -34,10 +37,26 @@ class HomePage extends HookConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
const ConnectDeviceButton(),
|
const ConnectDeviceButton(),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
IconButton.filledTonal(
|
Consumer(builder: (context, ref, _) {
|
||||||
icon: const Icon(SpotubeIcons.user),
|
final me = ref.watch(meProvider);
|
||||||
onPressed: () {},
|
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),
|
const Gap(10),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
144
lib/pages/profile/profile.dart
Normal file
144
lib/pages/profile/profile.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@ import 'package:spotube/extensions/track.dart';
|
|||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/services/audio_player/custom_player.dart';
|
import 'package:spotube/services/audio_player/custom_player.dart';
|
||||||
// import 'package:just_audio/just_audio.dart' as ja;
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:media_kit/media_kit.dart' as mk;
|
import 'package:media_kit/media_kit.dart' as mk;
|
||||||
@ -43,7 +42,6 @@ class SpotubeMedia extends mk.Media {
|
|||||||
|
|
||||||
abstract class AudioPlayerInterface {
|
abstract class AudioPlayerInterface {
|
||||||
final CustomPlayer _mkPlayer;
|
final CustomPlayer _mkPlayer;
|
||||||
// final ja.AudioPlayer? _justAudxio;
|
|
||||||
|
|
||||||
AudioPlayerInterface()
|
AudioPlayerInterface()
|
||||||
: _mkPlayer = CustomPlayer(
|
: _mkPlayer = CustomPlayer(
|
||||||
@ -51,9 +49,7 @@ abstract class AudioPlayerInterface {
|
|||||||
title: "Spotube",
|
title: "Spotube",
|
||||||
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
|
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
|
||||||
),
|
),
|
||||||
)
|
) {
|
||||||
// _justAudio = !_mkSupportedPlatform ? ja.AudioPlayer() : null
|
|
||||||
{
|
|
||||||
_mkPlayer.stream.error.listen((event) {
|
_mkPlayer.stream.error.listen((event) {
|
||||||
Catcher2.reportCheckedError(event, StackTrace.current);
|
Catcher2.reportCheckedError(event, StackTrace.current);
|
||||||
});
|
});
|
||||||
@ -61,33 +57,19 @@ abstract class AudioPlayerInterface {
|
|||||||
|
|
||||||
/// Whether the current platform supports the audioplayers plugin
|
/// Whether the current platform supports the audioplayers plugin
|
||||||
static const bool _mkSupportedPlatform = true;
|
static const bool _mkSupportedPlatform = true;
|
||||||
// DesktopTools.platform.isWindows || DesktopTools.platform.isLinux;
|
|
||||||
|
|
||||||
bool get mkSupportedPlatform => _mkSupportedPlatform;
|
bool get mkSupportedPlatform => _mkSupportedPlatform;
|
||||||
|
|
||||||
Future<Duration?> get duration async {
|
Future<Duration?> get duration async {
|
||||||
return _mkPlayer.state.duration;
|
return _mkPlayer.state.duration;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.duration;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Duration?> get position async {
|
Future<Duration?> get position async {
|
||||||
return _mkPlayer.state.position;
|
return _mkPlayer.state.position;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.position;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Duration?> get bufferedPosition async {
|
Future<Duration?> get bufferedPosition async {
|
||||||
if (mkSupportedPlatform) {
|
return _mkPlayer.state.buffer;
|
||||||
// audioplayers doesn't have the capability to get buffered position
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<mk.AudioDevice> get selectedDevice async {
|
Future<mk.AudioDevice> get selectedDevice async {
|
||||||
@ -100,86 +82,39 @@ abstract class AudioPlayerInterface {
|
|||||||
|
|
||||||
bool get hasSource {
|
bool get hasSource {
|
||||||
return _mkPlayer.state.playlist.medias.isNotEmpty;
|
return _mkPlayer.state.playlist.medias.isNotEmpty;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return _mkPlayer.state.playlist.medias.isNotEmpty;
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.audioSource != null;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// states
|
// states
|
||||||
bool get isPlaying {
|
bool get isPlaying {
|
||||||
return _mkPlayer.state.playing;
|
return _mkPlayer.state.playing;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return _mkPlayer.state.playing;
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.playing;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isPaused {
|
bool get isPaused {
|
||||||
return !_mkPlayer.state.playing;
|
return !_mkPlayer.state.playing;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return !_mkPlayer.state.playing;
|
|
||||||
// } else {
|
|
||||||
// return !isPlaying;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isStopped {
|
bool get isStopped {
|
||||||
return !hasSource;
|
return !hasSource;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return !hasSource;
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.processingState == ja.ProcessingState.idle;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> get isCompleted async {
|
Future<bool> get isCompleted async {
|
||||||
return _mkPlayer.state.completed;
|
return _mkPlayer.state.completed;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return _mkPlayer.state.completed;
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.processingState == ja.ProcessingState.completed;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> get isShuffled async {
|
Future<bool> get isShuffled async {
|
||||||
return _mkPlayer.shuffled;
|
return _mkPlayer.shuffled;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return _mkPlayer.shuffled;
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.shuffleModeEnabled;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackLoopMode get loopMode {
|
PlaybackLoopMode get loopMode {
|
||||||
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode);
|
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
|
/// Returns the current volume of the player, between 0 and 1
|
||||||
double get volume {
|
double get volume {
|
||||||
return _mkPlayer.state.volume / 100;
|
return _mkPlayer.state.volume / 100;
|
||||||
// if (mkSupportedPlatform) {
|
|
||||||
// return _mkPlayer.state.volume / 100;
|
|
||||||
// } else {
|
|
||||||
// return _justAudio!.volume;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isBuffering {
|
bool get isBuffering {
|
||||||
return false;
|
return _mkPlayer.state.buffering;
|
||||||
// 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;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user