Skip to content

Commit 7983932

Browse files
authored
feat: add spotify friends activity (#1130)
* feat: add spotify friend endpoint * feat: add friend activity in home screen * fix: when no friends, dummy UI still shows giving the user a false hope of friendship :'(
1 parent 682e88e commit 7983932

File tree

12 files changed

+507
-23
lines changed

12 files changed

+507
-23
lines changed

lib/collections/fake.dart

+32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:spotify/spotify.dart';
22
import 'package:spotube/extensions/track.dart';
3+
import 'package:spotube/models/spotify_friends.dart';
34

45
abstract class FakeData {
56
static final Image image = Image()
@@ -164,4 +165,35 @@ abstract class FakeData {
164165
..icons = [image]
165166
..id = "1"
166167
..name = "category";
168+
169+
static final friends = SpotifyFriends(
170+
friends: [
171+
for (var i = 0; i < 3; i++)
172+
SpotifyFriendActivity(
173+
user: const SpotifyFriend(
174+
name: "name",
175+
imageUrl: "imageUrl",
176+
uri: "uri",
177+
),
178+
track: SpotifyActivityTrack(
179+
name: "name",
180+
artist: const SpotifyActivityArtist(
181+
name: "name",
182+
uri: "uri",
183+
),
184+
album: const SpotifyActivityAlbum(
185+
name: "name",
186+
uri: "uri",
187+
),
188+
context: SpotifyActivityContext(
189+
name: "name",
190+
index: i,
191+
uri: "uri",
192+
),
193+
imageUrl: "imageUrl",
194+
uri: "uri",
195+
),
196+
),
197+
],
198+
);
167199
}
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:hooks_riverpod/hooks_riverpod.dart';
3+
import 'package:skeletonizer/skeletonizer.dart';
4+
import 'package:spotube/collections/fake.dart';
5+
import 'package:spotube/components/home/sections/friends/friend_item.dart';
6+
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
7+
import 'package:spotube/models/spotify_friends.dart';
8+
import 'package:spotube/services/queries/queries.dart';
9+
10+
class HomePageFriendsSection extends HookConsumerWidget {
11+
const HomePageFriendsSection({Key? key}) : super(key: key);
12+
13+
@override
14+
Widget build(BuildContext context, ref) {
15+
final friendsQuery = useQueries.user.friendActivity(ref);
16+
final friends = friendsQuery.data?.friends ?? FakeData.friends.friends;
17+
18+
final groupCount = useBreakpointValue(
19+
sm: 3,
20+
xs: 2,
21+
md: 4,
22+
lg: 5,
23+
xl: 6,
24+
xxl: 7,
25+
);
26+
27+
final friendGroup = friends.fold<List<List<SpotifyFriendActivity>>>(
28+
[],
29+
(previousValue, element) {
30+
if (previousValue.isEmpty) {
31+
return [
32+
[element]
33+
];
34+
}
35+
36+
final lastGroup = previousValue.last;
37+
if (lastGroup.length < groupCount) {
38+
return [
39+
...previousValue.sublist(0, previousValue.length - 1),
40+
[...lastGroup, element]
41+
];
42+
}
43+
44+
return [
45+
...previousValue,
46+
[element]
47+
];
48+
},
49+
);
50+
51+
if (!friendsQuery.isLoading &&
52+
(!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) {
53+
return const SliverToBoxAdapter(
54+
child: SizedBox.shrink(),
55+
);
56+
}
57+
58+
return Skeletonizer.sliver(
59+
enabled: friendsQuery.isLoading,
60+
child: SliverMainAxisGroup(
61+
slivers: [
62+
SliverToBoxAdapter(
63+
child: Padding(
64+
padding: const EdgeInsets.all(8.0),
65+
child: Text(
66+
'Friends',
67+
style: Theme.of(context).textTheme.titleMedium,
68+
),
69+
),
70+
),
71+
SliverToBoxAdapter(
72+
child: SingleChildScrollView(
73+
scrollDirection: Axis.horizontal,
74+
child: Column(
75+
crossAxisAlignment: CrossAxisAlignment.start,
76+
children: [
77+
for (final group in friendGroup)
78+
Row(
79+
children: [
80+
for (final friend in group)
81+
Padding(
82+
padding: const EdgeInsets.all(8.0),
83+
child: FriendItem(friend: friend),
84+
),
85+
],
86+
),
87+
],
88+
),
89+
),
90+
),
91+
],
92+
),
93+
);
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import 'package:fl_query_hooks/fl_query_hooks.dart';
2+
import 'package:flutter/gestures.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:gap/gap.dart';
5+
import 'package:go_router/go_router.dart';
6+
import 'package:hooks_riverpod/hooks_riverpod.dart';
7+
import 'package:spotify/spotify.dart';
8+
import 'package:spotube/collections/spotube_icons.dart';
9+
import 'package:spotube/components/shared/image/universal_image.dart';
10+
import 'package:spotube/models/spotify_friends.dart';
11+
import 'package:spotube/provider/spotify_provider.dart';
12+
13+
class FriendItem extends HookConsumerWidget {
14+
final SpotifyFriendActivity friend;
15+
const FriendItem({
16+
Key? key,
17+
required this.friend,
18+
}) : super(key: key);
19+
20+
@override
21+
Widget build(BuildContext context, ref) {
22+
final ThemeData(
23+
textTheme: textTheme,
24+
colorScheme: colorScheme,
25+
) = Theme.of(context);
26+
27+
final queryClient = useQueryClient();
28+
final spotify = ref.watch(spotifyProvider);
29+
30+
return Container(
31+
padding: const EdgeInsets.all(8),
32+
decoration: BoxDecoration(
33+
color: colorScheme.surfaceVariant.withOpacity(0.3),
34+
borderRadius: BorderRadius.circular(15),
35+
),
36+
constraints: const BoxConstraints(
37+
minWidth: 300,
38+
),
39+
height: 80,
40+
child: Row(
41+
children: [
42+
CircleAvatar(
43+
backgroundImage: UniversalImage.imageProvider(
44+
friend.user.imageUrl,
45+
),
46+
),
47+
const Gap(8),
48+
Column(
49+
crossAxisAlignment: CrossAxisAlignment.start,
50+
children: [
51+
Text(
52+
friend.user.name,
53+
style: textTheme.bodyLarge,
54+
),
55+
RichText(
56+
text: TextSpan(
57+
style: textTheme.bodySmall,
58+
children: [
59+
TextSpan(
60+
text: friend.track.name,
61+
recognizer: TapGestureRecognizer()
62+
..onTap = () {
63+
context.push("/track/${friend.track.id}");
64+
},
65+
),
66+
const TextSpan(text: " • "),
67+
const WidgetSpan(
68+
child: Icon(
69+
SpotubeIcons.artist,
70+
size: 12,
71+
),
72+
),
73+
TextSpan(
74+
text: " ${friend.track.artist.name}",
75+
recognizer: TapGestureRecognizer()
76+
..onTap = () {
77+
context.push(
78+
"/artist/${friend.track.artist.id}",
79+
);
80+
},
81+
),
82+
const TextSpan(text: "\n"),
83+
TextSpan(
84+
text: friend.track.context.name,
85+
recognizer: TapGestureRecognizer()
86+
..onTap = () async {
87+
context.push(
88+
"/${friend.track.context.path}",
89+
extra: !friend.track.context.path
90+
.startsWith("album")
91+
? null
92+
: await queryClient.fetchQuery<Album, dynamic>(
93+
"album/${friend.track.album.id}",
94+
() => spotify.albums.get(
95+
friend.track.album.id,
96+
),
97+
),
98+
);
99+
},
100+
),
101+
const TextSpan(text: " • "),
102+
const WidgetSpan(
103+
child: Icon(
104+
SpotubeIcons.album,
105+
size: 12,
106+
),
107+
),
108+
TextSpan(
109+
text: " ${friend.track.album.name}",
110+
recognizer: TapGestureRecognizer()
111+
..onTap = () async {
112+
final album =
113+
await queryClient.fetchQuery<Album, dynamic>(
114+
"album/${friend.track.album.id}",
115+
() => spotify.albums.get(
116+
friend.track.album.id,
117+
),
118+
);
119+
if (context.mounted) {
120+
context.push(
121+
"/album/${friend.track.album.id}",
122+
extra: album,
123+
);
124+
}
125+
},
126+
),
127+
],
128+
),
129+
),
130+
],
131+
),
132+
],
133+
),
134+
);
135+
}
136+
}

lib/components/shared/page_window_title_bar.dart

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_hooks/flutter_hooks.dart';
33
import 'package:hooks_riverpod/hooks_riverpod.dart';
44
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
5-
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
65
import 'package:spotube/utils/platform.dart';
76
import 'package:titlebar_buttons/titlebar_buttons.dart';
87
import 'dart:math';
98
import 'package:flutter/foundation.dart' show kIsWeb;
10-
import 'dart:io' show Platform, exit;
9+
import 'dart:io' show Platform;
1110
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
12-
import 'package:local_notifier/local_notifier.dart';
1311

1412
class PageWindowTitleBar extends StatefulHookConsumerWidget
1513
implements PreferredSizeWidget {

lib/l10n/app_en.arb

+2-1
Original file line numberDiff line numberDiff line change
@@ -284,5 +284,6 @@
284284
"discord_rich_presence": "Discord Rich Presence",
285285
"browse_all": "Browse All",
286286
"genres": "Genres",
287-
"explore_genres": "Explore Genres"
287+
"explore_genres": "Explore Genres",
288+
"friends": "Friends"
288289
}

lib/main.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ class SpotubeState extends ConsumerState<Spotube> {
228228
builder: (context, child) {
229229
return DevicePreview.appBuilder(
230230
context,
231-
DragToResizeArea(child: child!),
231+
DesktopTools.platform.isDesktop
232+
? DragToResizeArea(child: child!)
233+
: child,
232234
);
233235
},
234236
themeMode: themeMode,

0 commit comments

Comments
 (0)