Android SafeArea issues fixed

configurations for different plugin for android added
adjusted platform bound opertations
This commit is contained in:
Kingkor Roy Tirtho 2022-03-15 19:47:29 +06:00
parent 39a92a56f3
commit c64f329c42
19 changed files with 793 additions and 736 deletions

View File

@ -1,34 +1,30 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube">
package="oss.krtirtho.spotube">
<application <uses-permission android:name="android.permission.INTERNET" />
android:label="spotube"
android:name="${applicationName}" <queries>
android:icon="@mipmap/ic_launcher"> <!-- If your app opens https URLs -->
<activity <intent>
android:name=".MainActivity" <action android:name="android.intent.action.VIEW" />
android:exported="true" <data android:scheme="https" />
android:launchMode="singleTop" </intent>
android:theme="@style/LaunchTheme" </queries>
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" <application android:label="spotube" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true">
android:windowSoftInputMode="adjustResize"> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
android:name="io.flutter.embedding.android.NormalTheme" <intent-filter>
android:resource="@style/NormalTheme" <action android:name="android.intent.action.MAIN" />
/> <category android:name="android.intent.category.LAUNCHER" />
<intent-filter> </intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> </activity>
</intent-filter> <!-- Don't delete the meta-data below.
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data android:name="flutterEmbedding" android:value="2" />
android:name="flutterEmbedding" </application>
android:value="2" />
</application>
</manifest> </manifest>

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.6.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@ -37,5 +37,11 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'AUDIO_SESSION_MICROPHONE=0'
]
end
end end
end end

View File

@ -1,47 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Sptube</string> <string>Sptube</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>spotube</string> <string>spotube</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true />
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true />
</dict> <key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
<key>NSAllowsArbitraryLoadsForMedia</key>
<true />
</dict>
</dict>
</plist> </plist>

View File

@ -38,63 +38,65 @@ class AlbumView extends ConsumerWidget {
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; var isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
SpotifyApi spotify = ref.watch(spotifyProvider); SpotifyApi spotify = ref.watch(spotifyProvider);
return Scaffold( return SafeArea(
body: FutureBuilder<Iterable<TrackSimple>>( child: Scaffold(
future: spotify.albums.getTracks(album.id!).all(), body: FutureBuilder<Iterable<TrackSimple>>(
builder: (context, snapshot) { future: spotify.albums.getTracks(album.id!).all(),
List<Track> tracks = snapshot.data?.map((trackSmp) { builder: (context, snapshot) {
return simpleTrackToTrack(trackSmp, album); List<Track> tracks = snapshot.data?.map((trackSmp) {
}).toList() ?? return simpleTrackToTrack(trackSmp, album);
[]; }).toList() ??
return Column( [];
children: [ return Column(
PageWindowTitleBar( children: [
leading: Row( PageWindowTitleBar(
children: [ leading: Row(
// nav back children: [
const BackButton(), // nav back
// heart playlist const BackButton(),
IconButton( // heart playlist
icon: const Icon(Icons.favorite_outline_rounded), IconButton(
onPressed: () {}, icon: const Icon(Icons.favorite_outline_rounded),
), onPressed: () {},
// play playlist
IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
), ),
onPressed: snapshot.hasData // play playlist
? () => playPlaylist(playback, tracks) IconButton(
: null, icon: Icon(
) isPlaylistPlaying
], ? Icons.stop_rounded
), : Icons.play_arrow_rounded,
),
Center(
child: Text(album.name!,
style: Theme.of(context).textTheme.headline4),
),
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
), ),
], onPressed: snapshot.hasData
); ? () => playPlaylist(playback, tracks)
}), : null,
)
],
),
),
Center(
child: Text(album.name!,
style: Theme.of(context).textTheme.headline4),
),
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
),
],
);
}),
),
); );
} }
} }

View File

@ -59,32 +59,34 @@ class _ArtistAlbumViewState extends ConsumerState<ArtistAlbumView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return SafeArea(
appBar: const PageWindowTitleBar(leading: BackButton()), child: Scaffold(
body: Column( appBar: const PageWindowTitleBar(leading: BackButton()),
children: [ body: Column(
Text( children: [
widget.artistName, Text(
style: Theme.of(context).textTheme.headline4, widget.artistName,
), style: Theme.of(context).textTheme.headline4,
Expanded( ),
child: PagedGridView( Expanded(
pagingController: _pagingController, child: PagedGridView(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( pagingController: _pagingController,
maxCrossAxisExtent: 260, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
childAspectRatio: 9 / 13, maxCrossAxisExtent: 260,
crossAxisSpacing: 20, childAspectRatio: 9 / 13,
mainAxisSpacing: 20, crossAxisSpacing: 20,
), mainAxisSpacing: 20,
padding: const EdgeInsets.all(10), ),
builderDelegate: PagedChildBuilderDelegate<Album>( padding: const EdgeInsets.all(10),
itemBuilder: (context, item, index) { builderDelegate: PagedChildBuilderDelegate<Album>(
return AlbumCard(item); itemBuilder: (context, item, index) {
}, return AlbumCard(item);
},
),
), ),
), ),
), ],
], ),
), ),
); );
} }

View File

@ -45,270 +45,273 @@ class ArtistProfile extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
return Scaffold( return SafeArea(
appBar: const PageWindowTitleBar( child: Scaffold(
leading: BackButton(), appBar: const PageWindowTitleBar(
), leading: BackButton(),
body: FutureBuilder<Artist>( ),
future: spotify.artists.get(artistId), body: FutureBuilder<Artist>(
builder: (context, snapshot) { future: spotify.artists.get(artistId),
if (!snapshot.hasData) { builder: (context, snapshot) {
return const Center(child: CircularProgressIndicator.adaptive()); if (!snapshot.hasData) {
} return const Center(child: CircularProgressIndicator.adaptive());
}
return SingleChildScrollView( return SingleChildScrollView(
controller: parentScrollController, controller: parentScrollController,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Wrap( Wrap(
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
children: [ children: [
const SizedBox(width: 50), const SizedBox(width: 50),
CircleAvatar( CircleAvatar(
radius: avatarWidth, radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
imageToUrlString(snapshot.data!.images), imageToUrlString(snapshot.data!.images),
),
), ),
), Padding(
Padding( padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(20), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Container(
Container( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5),
horizontal: 10, vertical: 5), decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.blue,
color: Colors.blue, borderRadius: BorderRadius.circular(50)),
borderRadius: BorderRadius.circular(50)), child: Text(snapshot.data!.type!.toUpperCase(),
child: Text(snapshot.data!.type!.toUpperCase(), style: chipTextVariant?.copyWith(
style: chipTextVariant?.copyWith( color: Colors.white)),
color: Colors.white)), ),
), Text(
Text( snapshot.data!.name!,
snapshot.data!.name!, style: breakpoint.isSm
style: breakpoint.isSm ? textTheme.headline4
? textTheme.headline4 : textTheme.headline2,
: textTheme.headline2, ),
), Text(
Text( "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", style: breakpoint.isSm
style: breakpoint.isSm ? textTheme.bodyText1
? textTheme.bodyText1 : textTheme.headline5,
: textTheme.headline5, ),
), const SizedBox(height: 20),
const SizedBox(height: 20), Row(
Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ // TODO: Implement check if user follows this artist
// TODO: Implement check if user follows this artist // LIMITATION: spotify-dart lib
// LIMITATION: spotify-dart lib FutureBuilder(
FutureBuilder( future: Future.value(true),
future: Future.value(true), builder: (context, snapshot) {
builder: (context, snapshot) { return OutlinedButton(
return OutlinedButton( onPressed: () async {
onPressed: () async { // TODO: make `follow/unfollow` artists button work
// TODO: make `follow/unfollow` artists button work // LIMITATION: spotify-dart lib
// LIMITATION: spotify-dart lib },
}, child: Text(snapshot.data == true
child: Text(snapshot.data == true ? "Following"
? "Following" : "Follow"),
: "Follow"), );
); }),
}), IconButton(
IconButton( icon: const Icon(Icons.share_rounded),
icon: const Icon(Icons.share_rounded), onPressed: () {
onPressed: () { Clipboard.setData(
Clipboard.setData( ClipboardData(
ClipboardData( text: snapshot
text: snapshot .data?.externalUrls?.spotify),
.data?.externalUrls?.spotify), ).then((val) {
).then((val) { ScaffoldMessenger.of(context)
ScaffoldMessenger.of(context).showSnackBar( .showSnackBar(
const SnackBar( const SnackBar(
width: 300, width: 300,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
content: Text( content: Text(
"Artist URL copied to clipboard", "Artist URL copied to clipboard",
textAlign: TextAlign.center, textAlign: TextAlign.center,
),
), ),
), );
); });
}); },
}, )
) ],
], )
) ],
], ),
), ),
), ],
], ),
), const SizedBox(height: 50),
const SizedBox(height: 50), FutureBuilder<Iterable<Track>>(
FutureBuilder<Iterable<Track>>( future:
future: spotify.artists.getTopTracks(snapshot.data!.id!, "US"),
spotify.artists.getTopTracks(snapshot.data!.id!, "US"), builder: (context, trackSnapshot) {
builder: (context, trackSnapshot) { if (!trackSnapshot.hasData) {
if (!trackSnapshot.hasData) { return const Center(
return const Center( child: CircularProgressIndicator.adaptive());
child: CircularProgressIndicator.adaptive()); }
} Playback playback = ref.watch(playbackProvider);
Playback playback = ref.watch(playbackProvider); var isPlaylistPlaying =
var isPlaylistPlaying = playback.currentPlaylist?.id == snapshot.data?.id;
playback.currentPlaylist?.id == snapshot.data?.id; playPlaylist(List<Track> tracks,
playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
{Track? currentTrack}) async { currentTrack ??= tracks.first;
currentTrack ??= tracks.first; if (!isPlaylistPlaying) {
if (!isPlaylistPlaying) { playback.setCurrentPlaylist = CurrentPlaylist(
playback.setCurrentPlaylist = CurrentPlaylist( tracks: tracks,
tracks: tracks, id: snapshot.data!.id!,
id: snapshot.data!.id!, name: "${snapshot.data!.name!} To Tracks",
name: "${snapshot.data!.name!} To Tracks", thumbnail: imageToUrlString(snapshot.data?.images),
thumbnail: imageToUrlString(snapshot.data?.images), );
); playback.setCurrentTrack = currentTrack;
playback.setCurrentTrack = currentTrack; } else if (isPlaylistPlaying &&
} else if (isPlaylistPlaying && currentTrack.id != null &&
currentTrack.id != null && currentTrack.id != playback.currentTrack?.id) {
currentTrack.id != playback.currentTrack?.id) { playback.setCurrentTrack = currentTrack;
playback.setCurrentTrack = currentTrack; }
await playback.startPlaying();
} }
await playback.startPlaying();
}
return Column(children: [ return Column(children: [
Row( Row(
children: [ children: [
Text( Text(
"Top Tracks", "Top Tracks",
style: Theme.of(context).textTheme.headline4, style: Theme.of(context).textTheme.headline4,
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(50),
), ),
child: IconButton( Container(
icon: Icon(isPlaylistPlaying margin: const EdgeInsets.symmetric(horizontal: 5),
? Icons.stop_rounded decoration: BoxDecoration(
: Icons.play_arrow_rounded), color: Theme.of(context).primaryColor,
color: Colors.white, borderRadius: BorderRadius.circular(50),
onPressed: trackSnapshot.hasData
? () =>
playPlaylist(trackSnapshot.data!.toList())
: null,
),
)
],
),
...trackSnapshot.data
?.toList()
.asMap()
.entries
.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
String? thumbnailUrl = imageToUrlString(
track.value.album?.images,
index:
(track.value.album?.images?.length ?? 1) -
1);
return TrackTile(
playback,
duration: duration,
track: track,
thumbnailUrl: thumbnailUrl,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
trackSnapshot.data!.toList(),
currentTrack: track.value,
), ),
); child: IconButton(
}) ?? icon: Icon(isPlaylistPlaying
[], ? Icons.stop_rounded
]); : Icons.play_arrow_rounded),
}, color: Colors.white,
), onPressed: trackSnapshot.hasData
const SizedBox(height: 50), ? () => playPlaylist(
Row( trackSnapshot.data!.toList())
mainAxisAlignment: MainAxisAlignment.spaceBetween, : null,
children: [ ),
Text( )
"Albums", ],
style: Theme.of(context).textTheme.headline4, ),
), ...trackSnapshot.data
TextButton( ?.toList()
child: const Text("See All"), .asMap()
onPressed: () { .entries
GoRouter.of(context).push( .map((track) {
"/artist-album/$artistId", String duration =
extra: snapshot.data?.name ?? "KRTX", "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
); String? thumbnailUrl = imageToUrlString(
}, track.value.album?.images,
) index:
], (track.value.album?.images?.length ?? 1) -
), 1);
const SizedBox(height: 10), return TrackTile(
FutureBuilder<List<Album>>( playback,
future: spotify.artists duration: duration,
.albums(snapshot.data!.id!) track: track,
.getPage(5, 0) thumbnailUrl: thumbnailUrl,
.then((al) => al.items?.toList() ?? []), onTrackPlayButtonPressed: (currentTrack) =>
builder: (context, snapshot) { playPlaylist(
if (!snapshot.hasData) { trackSnapshot.data!.toList(),
return const Center( currentTrack: track.value,
child: CircularProgressIndicator.adaptive()); ),
} );
return Scrollbar( }) ??
controller: scrollController, [],
child: SingleChildScrollView( ]);
},
),
const SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Albums",
style: Theme.of(context).textTheme.headline4,
),
TextButton(
child: const Text("See All"),
onPressed: () {
GoRouter.of(context).push(
"/artist-album/$artistId",
extra: snapshot.data?.name ?? "KRTX",
);
},
)
],
),
const SizedBox(height: 10),
FutureBuilder<List<Album>>(
future: spotify.artists
.albums(snapshot.data!.id!)
.getPage(5, 0)
.then((al) => al.items?.toList() ?? []),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
return Scrollbar(
controller: scrollController, controller: scrollController,
scrollDirection: Axis.horizontal, child: SingleChildScrollView(
child: Row( controller: scrollController,
mainAxisAlignment: MainAxisAlignment.spaceAround, scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: snapshot.data
?.map((album) => AlbumCard(album))
.toList() ??
[],
),
),
);
},
),
const SizedBox(height: 20),
Text(
"Fans also likes",
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 10),
FutureBuilder<Iterable<Artist>>(
future: spotify.artists.getRelatedArtists(artistId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: snapshot.data children: snapshot.data
?.map((album) => AlbumCard(album)) ?.map((artist) => ArtistCard(artist))
.toList() ?? .toList() ??
[], [],
), ),
), );
); },
}, )
), ],
const SizedBox(height: 20), ),
Text( );
"Fans also likes", },
style: Theme.of(context).textTheme.headline4, ),
),
const SizedBox(height: 10),
FutureBuilder<Iterable<Artist>>(
future: spotify.artists.getRelatedArtists(artistId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: snapshot.data
?.map((artist) => ArtistCard(artist))
.toList() ??
[],
),
);
},
)
],
),
);
},
), ),
); );
} }

View File

@ -152,65 +152,68 @@ class Home extends HookConsumerWidget {
return const Login(); return const Login();
} }
return Scaffold( return SafeArea(
body: Column( child: Scaffold(
children: [ body: Column(
WindowTitleBarBox( children: [
child: Row( WindowTitleBarBox(
children: [ child: Row(
Expanded( children: [
child: Row(
children: [
Container(
constraints: BoxConstraints(
maxWidth: titleBarDragMaxWidth.toDouble(),
),
color:
Theme.of(context).navigationRailTheme.backgroundColor,
child: MoveWindow(),
),
Expanded(child: MoveWindow()),
if (!Platform.isMacOS) const TitleBarActionButtons(),
],
))
],
),
),
Expanded(
child: Row(
children: [
Sidebar(
selectedIndex: _selectedIndex.value,
onSelectedIndexChanged: _onSelectedIndexChanged,
),
// contents of the spotify
if (_selectedIndex.value == 0)
Expanded( Expanded(
child: Padding( child: Row(
padding: const EdgeInsets.all(8.0), children: [
child: PagedListView( Container(
pagingController: pagingController, constraints: BoxConstraints(
builderDelegate: PagedChildBuilderDelegate<Category>( maxWidth: titleBarDragMaxWidth.toDouble(),
itemBuilder: (context, item, index) { ),
return CategoryCard(item); color: Theme.of(context)
}, .navigationRailTheme
.backgroundColor,
child: MoveWindow(),
),
Expanded(child: MoveWindow()),
if (!Platform.isMacOS) const TitleBarActionButtons(),
],
))
],
),
),
Expanded(
child: Row(
children: [
Sidebar(
selectedIndex: _selectedIndex.value,
onSelectedIndexChanged: _onSelectedIndexChanged,
),
// contents of the spotify
if (_selectedIndex.value == 0)
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PagedListView(
pagingController: pagingController,
builderDelegate: PagedChildBuilderDelegate<Category>(
itemBuilder: (context, item, index) {
return CategoryCard(item);
},
),
), ),
), ),
), ),
), if (_selectedIndex.value == 1) const Search(),
if (_selectedIndex.value == 1) const Search(), if (_selectedIndex.value == 2) const UserLibrary(),
if (_selectedIndex.value == 2) const UserLibrary(), if (_selectedIndex.value == 3) const Lyrics(),
if (_selectedIndex.value == 3) const Lyrics(), ],
], ),
), ),
), // player itself
// player itself const Player(),
const Player(), SpotubeNavigationBar(
SpotubeNavigationBar( selectedIndex: _selectedIndex.value,
selectedIndex: _selectedIndex.value, onSelectedIndexChanged: _onSelectedIndexChanged,
onSelectedIndexChanged: _onSelectedIndexChanged, ),
), ],
], ),
), ),
); );
} }

View File

@ -42,57 +42,59 @@ class PlayerView extends HookConsumerWidget {
[currentTrack?.album?.images], [currentTrack?.album?.images],
); );
return Scaffold( return SafeArea(
appBar: const PageWindowTitleBar( child: Scaffold(
leading: BackButton(), appBar: const PageWindowTitleBar(
), leading: BackButton(),
backgroundColor: paletteColor.color, ),
body: Column( backgroundColor: paletteColor.color,
mainAxisAlignment: MainAxisAlignment.spaceBetween, body: Column(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Padding( children: [
padding: const EdgeInsets.all(10), Padding(
child: Column( padding: const EdgeInsets.all(10),
children: [ child: Column(
Text( children: [
currentTrack?.name ?? "Not playing", Text(
overflow: TextOverflow.ellipsis, currentTrack?.name ?? "Not playing",
style: Theme.of(context).textTheme.headline4?.copyWith( overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.bold, style: Theme.of(context).textTheme.headline4?.copyWith(
color: paletteColor.titleTextColor, fontWeight: FontWeight.bold,
), color: paletteColor.titleTextColor,
), ),
artistsToClickableArtists( ),
currentTrack?.artists ?? [], artistsToClickableArtists(
mainAxisAlignment: MainAxisAlignment.center, currentTrack?.artists ?? [],
textStyle: Theme.of(context).textTheme.headline6!.copyWith( mainAxisAlignment: MainAxisAlignment.center,
fontWeight: FontWeight.bold, textStyle: Theme.of(context).textTheme.headline6!.copyWith(
color: paletteColor.bodyTextColor, fontWeight: FontWeight.bold,
), color: paletteColor.bodyTextColor,
), ),
], ),
), ],
),
HookBuilder(builder: (context) {
final ticker = useSingleTickerProvider();
final controller = useAnimationController(
duration: const Duration(seconds: 10),
vsync: ticker,
)..repeat();
return RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(controller),
child: CircleAvatar(
backgroundImage: CachedNetworkImageProvider(
albumArt,
cacheKey: albumArt,
),
radius: MediaQuery.of(context).size.width *
(breakpoint.isSm ? 0.4 : 0.3),
), ),
); ),
}), HookBuilder(builder: (context) {
PlayerControls(iconColor: paletteColor.bodyTextColor), final ticker = useSingleTickerProvider();
], final controller = useAnimationController(
duration: const Duration(seconds: 10),
vsync: ticker,
)..repeat();
return RotationTransition(
turns: Tween(begin: 0.0, end: 1.0).animate(controller),
child: CircleAvatar(
backgroundImage: CachedNetworkImageProvider(
albumArt,
cacheKey: albumArt,
),
radius: MediaQuery.of(context).size.width *
(breakpoint.isSm ? 0.4 : 0.3),
),
);
}),
PlayerControls(iconColor: paletteColor.bodyTextColor),
],
),
), ),
); );
} }

View File

@ -38,64 +38,66 @@ class PlaylistView extends ConsumerWidget {
SpotifyApi spotifyApi = ref.watch(spotifyProvider); SpotifyApi spotifyApi = ref.watch(spotifyProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id != null && var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == playlist.id; playback.currentPlaylist?.id == playlist.id;
return Scaffold( return SafeArea(
body: FutureBuilder<Iterable<Track>>( child: Scaffold(
future: playlist.id != "user-liked-tracks" body: FutureBuilder<Iterable<Track>>(
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all() future: playlist.id != "user-liked-tracks"
: spotifyApi.tracks.me.saved ? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
.all() : spotifyApi.tracks.me.saved
.then((tracks) => tracks.map((e) => e.track!)), .all()
builder: (context, snapshot) { .then((tracks) => tracks.map((e) => e.track!)),
List<Track> tracks = snapshot.data?.toList() ?? []; builder: (context, snapshot) {
return Column( List<Track> tracks = snapshot.data?.toList() ?? [];
children: [ return Column(
PageWindowTitleBar( children: [
leading: Row( PageWindowTitleBar(
children: [ leading: Row(
// nav back children: [
const BackButton(), // nav back
// heart playlist const BackButton(),
IconButton( // heart playlist
icon: const Icon(Icons.favorite_outline_rounded), IconButton(
onPressed: () {}, icon: const Icon(Icons.favorite_outline_rounded),
), onPressed: () {},
// play playlist
IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
), ),
onPressed: snapshot.hasData // play playlist
? () => playPlaylist(playback, tracks) IconButton(
: null, icon: Icon(
) isPlaylistPlaying
], ? Icons.stop_rounded
), : Icons.play_arrow_rounded,
),
Center(
child: Text(playlist.name!,
style: Theme.of(context).textTheme.headline4),
),
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
), ),
], onPressed: snapshot.hasData
); ? () => playPlaylist(playback, tracks)
}), : null,
)
],
),
),
Center(
child: Text(playlist.name!,
style: Theme.of(context).textTheme.headline4),
),
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
),
],
);
}),
),
); );
} }
} }

View File

@ -25,172 +25,174 @@ class Settings extends HookConsumerWidget {
geniusAccessToken.value = textEditingController.value.text; geniusAccessToken.value = textEditingController.value.text;
}); });
return Scaffold( return SafeArea(
appBar: PageWindowTitleBar( child: Scaffold(
leading: const BackButton(), appBar: PageWindowTitleBar(
center: Text( leading: const BackButton(),
"Settings", center: Text(
style: Theme.of(context).textTheme.headline5, "Settings",
style: Theme.of(context).textTheme.headline5,
),
), ),
), body: Padding(
body: Padding( padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0), child: Column(
child: Column( children: [
children: [ Row(
Row( children: [
children: [ Expanded(
Expanded( flex: 2,
flex: 2, child: Text(
child: Text( "Genius Access Token",
"Genius Access Token", style: Theme.of(context).textTheme.subtitle1,
style: Theme.of(context).textTheme.subtitle1,
),
),
Expanded(
flex: 1,
child: TextField(
controller: textEditingController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
), ),
), ),
), Expanded(
Padding( flex: 1,
padding: const EdgeInsets.all(8.0), child: TextField(
child: ElevatedButton( controller: textEditingController,
onPressed: geniusAccessToken.value != null decoration: InputDecoration(
? () async { hintText: preferences.geniusAccessToken,
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences
.setGeniusAccessToken(geniusAccessToken.value);
localStorage.setString(
LocalStorageKeys.geniusAccessToken,
geniusAccessToken.value ?? "");
geniusAccessToken.value = null;
textEditingController.text = "";
}
: null,
child: const Text("Save"),
),
)
],
),
const SizedBox(height: 10),
SettingsHotKeyTile(
title: "Next track global shortcut",
currentHotKey: preferences.nextTrackHotKey,
onHotKeyRecorded: (value) {
preferences.setNextTrackHotKey(value);
},
),
SettingsHotKeyTile(
title: "Prev track global shortcut",
currentHotKey: preferences.prevTrackHotKey,
onHotKeyRecorded: (value) {
preferences.setPrevTrackHotKey(value);
},
),
SettingsHotKeyTile(
title: "Play/Pause global shortcut",
currentHotKey: preferences.playPauseHotKey,
onHotKeyRecorded: (value) {
preferences.setPlayPauseHotKey(value);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Theme"),
DropdownButton<ThemeMode>(
value: theme,
items: const [
DropdownMenuItem(
child: Text(
"Dark",
), ),
value: ThemeMode.dark,
), ),
DropdownMenuItem( ),
child: Text( Padding(
"Light", padding: const EdgeInsets.all(8.0),
), child: ElevatedButton(
value: ThemeMode.light, onPressed: geniusAccessToken.value != null
? () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences.setGeniusAccessToken(
geniusAccessToken.value);
localStorage.setString(
LocalStorageKeys.geniusAccessToken,
geniusAccessToken.value ?? "");
geniusAccessToken.value = null;
textEditingController.text = "";
}
: null,
child: const Text("Save"),
), ),
DropdownMenuItem( )
child: Text("System"), ],
value: ThemeMode.system, ),
), const SizedBox(height: 10),
], SettingsHotKeyTile(
onChanged: (value) { title: "Next track global shortcut",
if (value != null) { currentHotKey: preferences.nextTrackHotKey,
ref.read(themeProvider.notifier).state = value; onHotKeyRecorded: (value) {
} preferences.setNextTrackHotKey(value);
}, },
) ),
], SettingsHotKeyTile(
), title: "Prev track global shortcut",
const SizedBox(height: 10), currentHotKey: preferences.prevTrackHotKey,
Builder(builder: (context) { onHotKeyRecorded: (value) {
Auth auth = ref.watch(authProvider); preferences.setPrevTrackHotKey(value);
return Row( },
),
SettingsHotKeyTile(
title: "Play/Pause global shortcut",
currentHotKey: preferences.playPauseHotKey,
onHotKeyRecorded: (value) {
preferences.setPlayPauseHotKey(value);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Log out of this account"), const Text("Theme"),
ElevatedButton( DropdownButton<ThemeMode>(
child: const Text("Logout"), value: theme,
style: ButtonStyle( items: const [
backgroundColor: MaterialStateProperty.all(Colors.red), DropdownMenuItem(
), child: Text(
onPressed: () async { "Dark",
SharedPreferences localStorage = ),
await SharedPreferences.getInstance(); value: ThemeMode.dark,
await localStorage.clear(); ),
auth.logout(); DropdownMenuItem(
GoRouter.of(context).pop(); child: Text(
"Light",
),
value: ThemeMode.light,
),
DropdownMenuItem(
child: Text("System"),
value: ThemeMode.system,
),
],
onChanged: (value) {
if (value != null) {
ref.read(themeProvider.notifier).state = value;
}
}, },
)
],
),
const SizedBox(height: 10),
Builder(builder: (context) {
Auth auth = ref.watch(authProvider);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Log out of this account"),
ElevatedButton(
child: const Text("Logout"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
await localStorage.clear();
auth.logout();
GoRouter.of(context).pop();
},
),
],
);
}),
const SizedBox(height: 40),
const Text("Spotube v1.2.0"),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text("Author: "),
Hyperlink(
"Kingkor Roy Tirtho",
"https://github.com/KRTirtho",
), ),
], ],
); ),
}), const SizedBox(height: 20),
const SizedBox(height: 40), Row(
const Text("Spotube v1.2.0"), mainAxisAlignment: MainAxisAlignment.center,
const SizedBox(height: 10), children: const [
Row( Hyperlink(
mainAxisAlignment: MainAxisAlignment.center, "💚 Sponsor/Donate 💚",
children: const [ "https://opencollective.com/spotube",
Text("Author: "), ),
Hyperlink( Text(""),
"Kingkor Roy Tirtho", Hyperlink(
"https://github.com/KRTirtho", "BSD-4-Clause LICENSE",
), "https://github.com/KRTirtho/spotube/blob/master/LICENSE",
], ),
), Text(""),
const SizedBox(height: 20), Hyperlink(
Row( "Bug Report",
mainAxisAlignment: MainAxisAlignment.center, "https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=",
children: const [ ),
Hyperlink( ],
"💚 Sponsor/Donate 💚", ),
"https://opencollective.com/spotube", const SizedBox(height: 10),
), const Text("© Spotube 2022. All rights reserved")
Text(""), ],
Hyperlink( ),
"BSD-4-Clause LICENSE",
"https://github.com/KRTirtho/spotube/blob/master/LICENSE",
),
Text(""),
Hyperlink(
"Bug Report",
"https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=",
),
],
),
const SizedBox(height: 10),
const Text("© Spotube 2022. All rights reserved")
],
), ),
), ),
); );

View File

@ -52,10 +52,23 @@ class PageWindowTitleBar extends StatelessWidget
const PageWindowTitleBar({Key? key, this.leading, this.center}) const PageWindowTitleBar({Key? key, this.leading, this.center})
: super(key: key); : super(key: key);
@override @override
Size get preferredSize => Size.fromHeight(appWindow.titleBarHeight); Size get preferredSize => Size.fromHeight(
!Platform.isIOS && !Platform.isAndroid ? appWindow.titleBarHeight : 0,
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (Platform.isIOS || Platform.isAndroid) {
return PreferredSize(
preferredSize: const Size.fromHeight(70),
child: Row(
children: [
if (leading != null) leading!,
Expanded(child: Center(child: center)),
],
),
);
}
return WindowTitleBarBox( return WindowTitleBarBox(
child: Row( child: Row(
children: [ children: [

View File

@ -28,7 +28,6 @@ Future<void> Function() usePreviousTrack(Playback playback) {
Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) { Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
return ([key]) async { return ([key]) async {
print("CLICK CLICK");
try { try {
if (playback.currentTrack == null) return; if (playback.currentTrack == null) return;
playback.isPlaying playback.isPlaying

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
@ -18,6 +20,7 @@ useHotKeys(WidgetRef ref) {
final _playOrPause = useTogglePlayPause(playback); final _playOrPause = useTogglePlayPause(playback);
useEffect(() { useEffect(() {
if (Platform.isIOS || Platform.isAndroid) return null;
_hotKeys = [ _hotKeys = [
GlobalKeyActions( GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp), HotKey(KeyCode.space, scope: HotKeyScope.inapp),

View File

@ -14,21 +14,25 @@ import 'package:spotube/provider/ThemeProvider.dart';
import 'package:spotube/provider/YouTube.dart'; import 'package:spotube/provider/YouTube.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); if (!Platform.isAndroid && !Platform.isIOS) {
await hotKeyManager.unregisterAll(); WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll();
doWhenWindowReady(() {
appWindow.minSize =
Size(Platform.isAndroid || Platform.isIOS ? 280 : 359, 700);
appWindow.size = const Size(900, 700);
appWindow.alignment = Alignment.center;
appWindow.maximize();
appWindow.show();
});
}
runApp(ProviderScope(child: MyApp())); runApp(ProviderScope(child: MyApp()));
doWhenWindowReady(() {
appWindow.minSize =
Size(Platform.isAndroid || Platform.isIOS ? 280 : 359, 700);
appWindow.size = const Size(900, 700);
appWindow.alignment = Alignment.center;
appWindow.maximize();
appWindow.show();
});
} }
class MyApp extends HookConsumerWidget { class MyApp extends HookConsumerWidget {
final GoRouter _router = createGoRouter(); final GoRouter _router = createGoRouter();
MyApp({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
var themeMode = ref.watch(themeProvider); var themeMode = ref.watch(themeProvider);

View File

@ -5,7 +5,6 @@ import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart';
import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart';
import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Player/PlayerView.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Settings.dart';

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:audio_session/audio_session.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
@ -60,9 +61,11 @@ class Playback extends ChangeNotifier {
StreamSubscription<bool>? _playingStreamListener; StreamSubscription<bool>? _playingStreamListener;
StreamSubscription<Duration?>? _durationStreamListener; StreamSubscription<Duration?>? _durationStreamListener;
StreamSubscription<ProcessingState>? _processingStateStreamListener; StreamSubscription<ProcessingState>? _processingStateStreamListener;
StreamSubscription<AudioInterruptionEvent>? _audioInterruptionEventListener;
AudioPlayer player; AudioPlayer player;
YoutubeExplode youtube; YoutubeExplode youtube;
AudioSession? _audioSession;
Playback({ Playback({
required this.player, required this.player,
required this.youtube, required this.youtube,
@ -107,6 +110,14 @@ class Playback extends ChangeNotifier {
print(stack); print(stack);
} }
}); });
AudioSession.instance.then((session) async {
_audioSession = session;
await session.configure(const AudioSessionConfiguration.music());
_audioInterruptionEventListener = session.interruptionEventStream.listen(
(AudioInterruptionEvent event) {},
);
});
} }
CurrentPlaylist? get currentPlaylist => _currentPlaylist; CurrentPlaylist? get currentPlaylist => _currentPlaylist;
@ -175,6 +186,8 @@ class Playback extends ChangeNotifier {
_processingStateStreamListener?.cancel(); _processingStateStreamListener?.cancel();
_durationStreamListener?.cancel(); _durationStreamListener?.cancel();
_playingStreamListener?.cancel(); _playingStreamListener?.cancel();
_audioInterruptionEventListener?.cancel();
_audioSession?.setActive(false);
super.dispose(); super.dispose();
} }
@ -206,7 +219,7 @@ class Playback extends ChangeNotifier {
// the track is already playing so no need to change that // the track is already playing so no need to change that
if (track != null && track.id == _currentTrack?.id) return; if (track != null && track.id == _currentTrack?.id) return;
track ??= _currentTrack; track ??= _currentTrack;
if (track != null) { if (track != null && await _audioSession?.setActive(true) == true) {
Uri? parsedUri = Uri.tryParse(track.uri ?? ""); Uri? parsedUri = Uri.tryParse(track.uri ?? "");
if (parsedUri != null && parsedUri.hasAbsolutePath) { if (parsedUri != null && parsedUri.hasAbsolutePath) {
await player await player

View File

@ -30,7 +30,7 @@ packages:
source: hosted source: hosted
version: "2.8.2" version: "2.8.2"
audio_session: audio_session:
dependency: transitive dependency: "direct main"
description: description:
name: audio_session name: audio_session
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -323,7 +323,7 @@ packages:
name: just_audio_web name: just_audio_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.4" version: "0.4.7"
libwinmedia: libwinmedia:
dependency: transitive dependency: transitive
description: description:

View File

@ -54,6 +54,7 @@ dependencies:
hooks_riverpod: ^1.0.3 hooks_riverpod: ^1.0.3
go_router: ^3.0.4 go_router: ^3.0.4
palette_generator: ^0.3.3 palette_generator: ^0.3.3
audio_session: ^0.1.6+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: