Skip to content

Commit

Permalink
feat: add playlist sharing functionality
Browse files Browse the repository at this point in the history
fixes playlist state issues
  • Loading branch information
gokadzev committed Mar 2, 2025
1 parent 1513ad0 commit 36ab270
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 66 deletions.
13 changes: 13 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,24 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />


<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />

<!-- Main activity intent filter -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="musify"
android:host="playlist"
android:pathPattern="/custom/.*" />
</intent-filter>
</activity>

<!-- Flutter embedding meta-data -->
Expand Down
28 changes: 15 additions & 13 deletions lib/API/musify.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ List globalSongs = [];

List playlists = [...playlistsDB, ...albumsDB];
List userPlaylists = Hive.box('user').get('playlists', defaultValue: []);
List userCustomPlaylists = Hive.box(
'user',
).get('customPlaylists', defaultValue: []);
final userCustomPlaylists = ValueNotifier<List>(
Hive.box('user').get('customPlaylists', defaultValue: []),
);
List userLikedSongsList = Hive.box('user').get('likedSongs', defaultValue: []);
List userLikedPlaylists = Hive.box(
'user',
Expand Down Expand Up @@ -126,8 +126,8 @@ Future<List> getRecommendedSongs() async {
}
playlistSongs.addAll(globalSongs.take(10));

if (userCustomPlaylists.isNotEmpty) {
for (final userPlaylist in userCustomPlaylists) {
if (userCustomPlaylists.value.isNotEmpty) {
for (final userPlaylist in userCustomPlaylists.value) {
final _list = (userPlaylist['list'] as List)..shuffle();
playlistSongs.addAll(_list.take(5));
}
Expand All @@ -145,7 +145,7 @@ Future<List> getRecommendedSongs() async {
}

Future<List<dynamic>> getUserPlaylists() async {
final playlistsByUser = [...userCustomPlaylists];
final playlistsByUser = [...userCustomPlaylists.value];
for (final playlistID in userPlaylists) {
try {
final plist = await _yt.playlists.get(playlistID);
Expand Down Expand Up @@ -231,8 +231,8 @@ String createCustomPlaylist(
if (image != null) 'image': image,
'list': [],
};
userCustomPlaylists.add(customPlaylist);
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists);
userCustomPlaylists.value = [...userCustomPlaylists.value, customPlaylist];
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists.value);
return '${context.l10n!.addedSuccess}!';
}

Expand All @@ -242,7 +242,7 @@ String addSongInCustomPlaylist(
Map song, {
int? indexToInsert,
}) {
final customPlaylist = userCustomPlaylists.firstWhere(
final customPlaylist = userCustomPlaylists.value.firstWhere(
(playlist) => playlist['title'] == playlistName,
orElse: () => null,
);
Expand All @@ -257,7 +257,7 @@ String addSongInCustomPlaylist(
indexToInsert != null
? playlistSongs.insert(indexToInsert, song)
: playlistSongs.add(song);
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists);
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists.value);
return context.l10n!.songAdded;
} else {
logger.log('Custom playlist not found: $playlistName', null, null);
Expand Down Expand Up @@ -288,7 +288,7 @@ bool removeSongFromPlaylist(
playlist['list'] = playlistSongs;

if (playlist['source'] == 'user-created') {
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists);
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists.value);
} else {
addOrUpdateData('user', 'playlists', userPlaylists);
}
Expand All @@ -306,8 +306,10 @@ void removeUserPlaylist(String playlistId) {
}

void removeUserCustomPlaylist(dynamic playlist) {
userCustomPlaylists.remove(playlist);
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists);
final updatedPlaylists = List.from(userCustomPlaylists.value)
..remove(playlist);
userCustomPlaylists.value = updatedPlaylists;
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists.value);
}

Future<void> updateSongLikeStatus(dynamic songId, bool add) async {
Expand Down
44 changes: 44 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import 'dart:async';

import 'package:app_links/app_links.dart';
import 'package:audio_service/audio_service.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart';
Expand All @@ -30,19 +31,23 @@ import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:musify/API/musify.dart';
import 'package:musify/extensions/l10n.dart';
import 'package:musify/localization/app_localizations.dart';
import 'package:musify/services/audio_service.dart';
import 'package:musify/services/data_manager.dart';
import 'package:musify/services/logger_service.dart';
import 'package:musify/services/playlist_sharing.dart';
import 'package:musify/services/router_service.dart';
import 'package:musify/services/settings_manager.dart';
import 'package:musify/services/update_manager.dart';
import 'package:musify/style/app_themes.dart';
import 'package:musify/utilities/flutter_toast.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';

late MusifyAudioHandler audioHandler;

final logger = Logger();
final appLinks = AppLinks();

bool isFdroidBuild = false;
bool isUpdateChecked = false;
Expand Down Expand Up @@ -235,7 +240,46 @@ Future<void> initialisation() async {
}
userChosenClients = chosenClients;
}

try {
// Listen to incoming links while app is running
appLinks.uriLinkStream.listen(
handleIncomingLink,
onError: (err) {
logger.log('URI link error:', err, null);
},
);
} on PlatformException {
logger.log('Failed to get initial uri', null, null);
}
} catch (e, stackTrace) {
logger.log('Initialization Error', e, stackTrace);
}
}

void handleIncomingLink(Uri? uri) async {
if (uri != null && uri.scheme == 'musify' && uri.host == 'playlist') {
try {
if (uri.pathSegments[0] == 'custom') {
final encodedPlaylist = uri.pathSegments[1];

final playlist = await PlaylistSharingService.decodeAndExpandPlaylist(
encodedPlaylist,
);

if (playlist != null) {
userCustomPlaylists.value = [...userCustomPlaylists.value, playlist];
addOrUpdateData('user', 'customPlaylists', userCustomPlaylists.value);
showToast(
NavigationManager().context,
'${NavigationManager().context.l10n!.addedSuccess}!',
);
} else {
showToast(NavigationManager().context, 'Invalid playlist data');
}
}
} catch (e) {
showToast(NavigationManager().context, 'Failed to load playlist');
}
}
}
57 changes: 9 additions & 48 deletions lib/screens/library_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:musify/API/musify.dart';
import 'package:musify/extensions/l10n.dart';
import 'package:musify/main.dart';
import 'package:musify/services/router_service.dart';
import 'package:musify/utilities/common_variables.dart';
import 'package:musify/utilities/flutter_toast.dart';
Expand All @@ -32,7 +31,6 @@ import 'package:musify/widgets/confirmation_dialog.dart';
import 'package:musify/widgets/playlist_bar.dart';
import 'package:musify/widgets/section_header.dart';
import 'package:musify/widgets/section_title.dart';
import 'package:musify/widgets/spinner.dart';

class LibraryPage extends StatefulWidget {
const LibraryPage({super.key});
Expand All @@ -42,19 +40,6 @@ class LibraryPage extends StatefulWidget {
}

class _LibraryPageState extends State<LibraryPage> {
late Future<List> _userPlaylistsFuture = getUserPlaylists();

Future<void> _refreshUserPlaylists() async {
setState(() {
_userPlaylistsFuture = getUserPlaylists();
});
}

@override
void dispose() {
super.dispose();
}

@override
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).colorScheme.primary;
Expand All @@ -81,7 +66,7 @@ class _LibraryPageState extends State<LibraryPage> {

Widget _buildUserPlaylistsSection(Color primaryColor) {
final isUserPlaylistsEmpty =
userPlaylists.isEmpty && userCustomPlaylists.isEmpty;
userPlaylists.isEmpty && userCustomPlaylists.value.isEmpty;
return Column(
children: [
SectionHeader(
Expand Down Expand Up @@ -124,9 +109,14 @@ class _LibraryPageState extends State<LibraryPage> {
),
],
),
FutureBuilder<List>(
future: _userPlaylistsFuture,
builder: _buildPlaylistsList,
ValueListenableBuilder<List>(
valueListenable: userCustomPlaylists,
builder: (context, playlists, _) {
if (playlists.isEmpty) {
return const SizedBox();
}
return _buildPlaylistListView(context, playlists);
},
),
],
);
Expand All @@ -148,31 +138,6 @@ class _LibraryPageState extends State<LibraryPage> {
);
}

Widget _buildPlaylistsList(
BuildContext context,
AsyncSnapshot<List> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Spinner();
} else if (snapshot.hasError) {
return _handleSnapshotError(context, snapshot);
}

return _buildPlaylistListView(context, snapshot.data!);
}

Widget _handleSnapshotError(
BuildContext context,
AsyncSnapshot<List> snapshot,
) {
logger.log(
'Error while fetching playlists',
snapshot.error,
snapshot.stackTrace,
);
return Center(child: Text(context.l10n!.error));
}

Widget _buildPlaylistListView(BuildContext context, List playlists) {
final isUserPlaylists =
playlists.isNotEmpty &&
Expand Down Expand Up @@ -322,8 +287,6 @@ class _LibraryPageState extends State<LibraryPage> {
}

Navigator.pop(context);

await _refreshUserPlaylists();
},
),
],
Expand Down Expand Up @@ -351,8 +314,6 @@ class _LibraryPageState extends State<LibraryPage> {
} else {
removeUserPlaylist(playlist['ytid']);
}

_refreshUserPlaylists();
},
);
},
Expand Down
23 changes: 21 additions & 2 deletions lib/screens/playlist_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import 'dart:math';

import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:musify/API/musify.dart';
import 'package:musify/extensions/l10n.dart';
import 'package:musify/main.dart';
import 'package:musify/services/data_manager.dart';
import 'package:musify/services/playlist_sharing.dart';
import 'package:musify/utilities/common_variables.dart';
import 'package:musify/utilities/flutter_toast.dart';
import 'package:musify/utilities/utils.dart';
Expand Down Expand Up @@ -130,6 +132,19 @@ class _PlaylistPageState extends State<PlaylistPage> {
if (_playlist != null) ...[
_buildSyncButton(),
const SizedBox(width: 10),
if (_playlist['source'] == 'user-created')
IconButton(
icon: const Icon(FluentIcons.share_24_regular),
onPressed: () async {
final encodedPlaylist = PlaylistSharingService.encodePlaylist(
_playlist,
);

final url = 'musify://playlist/custom/$encodedPlaylist';
await Clipboard.setData(ClipboardData(text: url));
},
),
const SizedBox(width: 10),
],
if (_playlist != null && _playlist['source'] == 'user-created') ...[
_buildEditButton(),
Expand Down Expand Up @@ -286,7 +301,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
child: Text(context.l10n!.add.toUpperCase()),
onPressed: () {
setState(() {
final index = userCustomPlaylists.indexOf(
final index = userCustomPlaylists.value.indexOf(
widget.playlistData,
);

Expand All @@ -297,7 +312,11 @@ class _PlaylistPageState extends State<PlaylistPage> {
if (imageUrl != null) 'image': imageUrl,
'list': widget.playlistData['list'],
};
userCustomPlaylists[index] = newPlaylist;
final updatedPlaylists = List<Map>.from(
userCustomPlaylists.value,
);
updatedPlaylists[index] = newPlaylist;
userCustomPlaylists.value = updatedPlaylists;
addOrUpdateData(
'user',
'customPlaylists',
Expand Down
Loading

0 comments on commit 36ab270

Please sign in to comment.