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"
package="oss.krtirtho.spotube">
<application
android:label="spotube"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<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
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube">
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
<application android:label="spotube" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true">
<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
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
<meta-data android:name="flutterEmbedding" android:value="2" />
</application>
</manifest>

View File

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

View File

@ -37,5 +37,11 @@ end
post_install do |installer|
installer.pods_project.targets.each do |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

View File

@ -1,47 +1,54 @@
<?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">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Sptube</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>spotube</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
</plist>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Sptube</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>spotube</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
<key>NSAllowsArbitraryLoadsForMedia</key>
<true />
</dict>
</dict>
</plist>

View File

@ -38,63 +38,65 @@ class AlbumView extends ConsumerWidget {
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
SpotifyApi spotify = ref.watch(spotifyProvider);
return Scaffold(
body: FutureBuilder<Iterable<TrackSimple>>(
future: spotify.albums.getTracks(album.id!).all(),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList() ??
[];
return Column(
children: [
PageWindowTitleBar(
leading: Row(
children: [
// nav back
const BackButton(),
// heart playlist
IconButton(
icon: const Icon(Icons.favorite_outline_rounded),
onPressed: () {},
),
// play playlist
IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
return SafeArea(
child: Scaffold(
body: FutureBuilder<Iterable<TrackSimple>>(
future: spotify.albums.getTracks(album.id!).all(),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList() ??
[];
return Column(
children: [
PageWindowTitleBar(
leading: Row(
children: [
// nav back
const BackButton(),
// heart playlist
IconButton(
icon: const Icon(Icons.favorite_outline_rounded),
onPressed: () {},
),
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,
),
// play playlist
IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
],
);
}),
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
Widget build(BuildContext context) {
return Scaffold(
appBar: const PageWindowTitleBar(leading: BackButton()),
body: Column(
children: [
Text(
widget.artistName,
style: Theme.of(context).textTheme.headline4,
),
Expanded(
child: PagedGridView(
pagingController: _pagingController,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 260,
childAspectRatio: 9 / 13,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
builderDelegate: PagedChildBuilderDelegate<Album>(
itemBuilder: (context, item, index) {
return AlbumCard(item);
},
return SafeArea(
child: Scaffold(
appBar: const PageWindowTitleBar(leading: BackButton()),
body: Column(
children: [
Text(
widget.artistName,
style: Theme.of(context).textTheme.headline4,
),
Expanded(
child: PagedGridView(
pagingController: _pagingController,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 260,
childAspectRatio: 9 / 13,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
builderDelegate: PagedChildBuilderDelegate<Album>(
itemBuilder: (context, item, index) {
return AlbumCard(item);
},
),
),
),
),
],
],
),
),
);
}

View File

@ -45,270 +45,273 @@ class ArtistProfile extends HookConsumerWidget {
final breakpoint = useBreakpoints();
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
body: FutureBuilder<Artist>(
future: spotify.artists.get(artistId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive());
}
return SafeArea(
child: Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
body: FutureBuilder<Artist>(
future: spotify.artists.get(artistId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive());
}
return SingleChildScrollView(
controller: parentScrollController,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider(
imageToUrlString(snapshot.data!.images),
return SingleChildScrollView(
controller: parentScrollController,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider(
imageToUrlString(snapshot.data!.images),
),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Text(snapshot.data!.type!.toUpperCase(),
style: chipTextVariant?.copyWith(
color: Colors.white)),
),
Text(
snapshot.data!.name!,
style: breakpoint.isSm
? textTheme.headline4
: textTheme.headline2,
),
Text(
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
style: breakpoint.isSm
? textTheme.bodyText1
: textTheme.headline5,
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// TODO: Implement check if user follows this artist
// LIMITATION: spotify-dart lib
FutureBuilder(
future: Future.value(true),
builder: (context, snapshot) {
return OutlinedButton(
onPressed: () async {
// TODO: make `follow/unfollow` artists button work
// LIMITATION: spotify-dart lib
},
child: Text(snapshot.data == true
? "Following"
: "Follow"),
);
}),
IconButton(
icon: const Icon(Icons.share_rounded),
onPressed: () {
Clipboard.setData(
ClipboardData(
text: snapshot
.data?.externalUrls?.spotify),
).then((val) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Artist URL copied to clipboard",
textAlign: TextAlign.center,
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Text(snapshot.data!.type!.toUpperCase(),
style: chipTextVariant?.copyWith(
color: Colors.white)),
),
Text(
snapshot.data!.name!,
style: breakpoint.isSm
? textTheme.headline4
: textTheme.headline2,
),
Text(
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
style: breakpoint.isSm
? textTheme.bodyText1
: textTheme.headline5,
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// TODO: Implement check if user follows this artist
// LIMITATION: spotify-dart lib
FutureBuilder(
future: Future.value(true),
builder: (context, snapshot) {
return OutlinedButton(
onPressed: () async {
// TODO: make `follow/unfollow` artists button work
// LIMITATION: spotify-dart lib
},
child: Text(snapshot.data == true
? "Following"
: "Follow"),
);
}),
IconButton(
icon: const Icon(Icons.share_rounded),
onPressed: () {
Clipboard.setData(
ClipboardData(
text: snapshot
.data?.externalUrls?.spotify),
).then((val) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Artist URL copied to clipboard",
textAlign: TextAlign.center,
),
),
),
);
});
},
)
],
)
],
);
});
},
)
],
)
],
),
),
),
],
),
const SizedBox(height: 50),
FutureBuilder<Iterable<Track>>(
future:
spotify.artists.getTopTracks(snapshot.data!.id!, "US"),
builder: (context, trackSnapshot) {
if (!trackSnapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
Playback playback = ref.watch(playbackProvider);
var isPlaylistPlaying =
playback.currentPlaylist?.id == snapshot.data?.id;
playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: snapshot.data!.id!,
name: "${snapshot.data!.name!} To Tracks",
thumbnail: imageToUrlString(snapshot.data?.images),
);
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) {
playback.setCurrentTrack = currentTrack;
],
),
const SizedBox(height: 50),
FutureBuilder<Iterable<Track>>(
future:
spotify.artists.getTopTracks(snapshot.data!.id!, "US"),
builder: (context, trackSnapshot) {
if (!trackSnapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
Playback playback = ref.watch(playbackProvider);
var isPlaylistPlaying =
playback.currentPlaylist?.id == snapshot.data?.id;
playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: snapshot.data!.id!,
name: "${snapshot.data!.name!} To Tracks",
thumbnail: imageToUrlString(snapshot.data?.images),
);
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) {
playback.setCurrentTrack = currentTrack;
}
await playback.startPlaying();
}
await playback.startPlaying();
}
return Column(children: [
Row(
children: [
Text(
"Top Tracks",
style: Theme.of(context).textTheme.headline4,
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(50),
return Column(children: [
Row(
children: [
Text(
"Top Tracks",
style: Theme.of(context).textTheme.headline4,
),
child: IconButton(
icon: Icon(isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded),
color: Colors.white,
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,
Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(50),
),
);
}) ??
[],
]);
},
),
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,
child: SingleChildScrollView(
child: IconButton(
icon: Icon(isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded),
color: Colors.white,
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,
),
);
}) ??
[],
]);
},
),
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,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
child: SingleChildScrollView(
controller: scrollController,
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
?.map((album) => AlbumCard(album))
?.map((artist) => ArtistCard(artist))
.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 Scaffold(
body: Column(
children: [
WindowTitleBarBox(
child: Row(
children: [
Expanded(
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)
return SafeArea(
child: Scaffold(
body: Column(
children: [
WindowTitleBarBox(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PagedListView(
pagingController: pagingController,
builderDelegate: PagedChildBuilderDelegate<Category>(
itemBuilder: (context, item, index) {
return CategoryCard(item);
},
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(
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 == 2) const UserLibrary(),
if (_selectedIndex.value == 3) const Lyrics(),
],
if (_selectedIndex.value == 1) const Search(),
if (_selectedIndex.value == 2) const UserLibrary(),
if (_selectedIndex.value == 3) const Lyrics(),
],
),
),
),
// player itself
const Player(),
SpotubeNavigationBar(
selectedIndex: _selectedIndex.value,
onSelectedIndexChanged: _onSelectedIndexChanged,
),
],
// player itself
const Player(),
SpotubeNavigationBar(
selectedIndex: _selectedIndex.value,
onSelectedIndexChanged: _onSelectedIndexChanged,
),
],
),
),
);
}

View File

@ -42,57 +42,59 @@ class PlayerView extends HookConsumerWidget {
[currentTrack?.album?.images],
);
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
backgroundColor: paletteColor.color,
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Text(
currentTrack?.name ?? "Not playing",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headline4?.copyWith(
fontWeight: FontWeight.bold,
color: paletteColor.titleTextColor,
),
),
artistsToClickableArtists(
currentTrack?.artists ?? [],
mainAxisAlignment: MainAxisAlignment.center,
textStyle: Theme.of(context).textTheme.headline6!.copyWith(
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),
return SafeArea(
child: Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
backgroundColor: paletteColor.color,
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Text(
currentTrack?.name ?? "Not playing",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headline4?.copyWith(
fontWeight: FontWeight.bold,
color: paletteColor.titleTextColor,
),
),
artistsToClickableArtists(
currentTrack?.artists ?? [],
mainAxisAlignment: MainAxisAlignment.center,
textStyle: Theme.of(context).textTheme.headline6!.copyWith(
fontWeight: FontWeight.bold,
color: paletteColor.bodyTextColor,
),
),
],
),
);
}),
PlayerControls(iconColor: 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),
),
);
}),
PlayerControls(iconColor: paletteColor.bodyTextColor),
],
),
),
);
}

View File

@ -38,64 +38,66 @@ class PlaylistView extends ConsumerWidget {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == playlist.id;
return Scaffold(
body: FutureBuilder<Iterable<Track>>(
future: playlist.id != "user-liked-tracks"
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
: spotifyApi.tracks.me.saved
.all()
.then((tracks) => tracks.map((e) => e.track!)),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.toList() ?? [];
return Column(
children: [
PageWindowTitleBar(
leading: Row(
children: [
// nav back
const BackButton(),
// heart playlist
IconButton(
icon: const Icon(Icons.favorite_outline_rounded),
onPressed: () {},
),
// play playlist
IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
return SafeArea(
child: Scaffold(
body: FutureBuilder<Iterable<Track>>(
future: playlist.id != "user-liked-tracks"
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
: spotifyApi.tracks.me.saved
.all()
.then((tracks) => tracks.map((e) => e.track!)),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.toList() ?? [];
return Column(
children: [
PageWindowTitleBar(
leading: Row(
children: [
// nav back
const BackButton(),
// heart playlist
IconButton(
icon: const Icon(Icons.favorite_outline_rounded),
onPressed: () {},
),
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,
),
// play playlist
IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
],
);
}),
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;
});
return Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
center: Text(
"Settings",
style: Theme.of(context).textTheme.headline5,
return SafeArea(
child: Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
center: Text(
"Settings",
style: Theme.of(context).textTheme.headline5,
),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
flex: 2,
child: Text(
"Genius Access Token",
style: Theme.of(context).textTheme.subtitle1,
),
),
Expanded(
flex: 1,
child: TextField(
controller: textEditingController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
Expanded(
flex: 2,
child: Text(
"Genius Access Token",
style: Theme.of(context).textTheme.subtitle1,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
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"),
),
)
],
),
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",
Expanded(
flex: 1,
child: TextField(
controller: textEditingController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
),
value: ThemeMode.dark,
),
DropdownMenuItem(
child: Text(
"Light",
),
value: ThemeMode.light,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
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,
),
],
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(
)
],
),
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("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 Text("Theme"),
DropdownButton<ThemeMode>(
value: theme,
items: const [
DropdownMenuItem(
child: Text(
"Dark",
),
value: ThemeMode.dark,
),
DropdownMenuItem(
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: 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),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Hyperlink(
"💚 Sponsor/Donate 💚",
"https://opencollective.com/spotube",
),
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")
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Hyperlink(
"💚 Sponsor/Donate 💚",
"https://opencollective.com/spotube",
),
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})
: super(key: key);
@override
Size get preferredSize => Size.fromHeight(appWindow.titleBarHeight);
Size get preferredSize => Size.fromHeight(
!Platform.isIOS && !Platform.isAndroid ? appWindow.titleBarHeight : 0,
);
@override
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(
child: Row(
children: [

View File

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

View File

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

View File

@ -14,21 +14,25 @@ import 'package:spotube/provider/ThemeProvider.dart';
import 'package:spotube/provider/YouTube.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll();
if (!Platform.isAndroid && !Platform.isIOS) {
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()));
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 {
final GoRouter _router = createGoRouter();
MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
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/ArtistProfile.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/Playlist/PlaylistView.dart';
import 'package:spotube/components/Settings.dart';

View File

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

View File

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

View File

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