From be44aadc07faffa678caa6d7f54ec82403320b1f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 29 Aug 2024 01:45:33 +0800 Subject: [PATCH] :iphone: Large screen support --- lib/screens/explore.dart | 78 +-- lib/screens/player/view.dart | 458 ++++++++++-------- lib/screens/playlist/view.dart | 236 ++++----- lib/screens/search/view.dart | 13 +- lib/screens/settings.dart | 18 +- lib/shells/nav_shell.dart | 1 + lib/widgets/sized_container.dart | 46 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 64 +++ pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 14 files changed, 560 insertions(+), 369 deletions(-) create mode 100644 lib/widgets/sized_container.dart diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index c5e6da7..ea9a055 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:go_router/go_router.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; +import 'package:rhythm_box/widgets/sized_container.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; @@ -39,45 +40,48 @@ class _ExploreScreenState extends State { child: Scaffold( appBar: AppBar( title: Text('explore'.tr), + centerTitle: MediaQuery.of(context).size.width >= 720, ), - body: Skeletonizer( - enabled: _isLoading, - child: ListView.builder( - itemCount: _featuredPlaylist?.length ?? 20, - itemBuilder: (context, idx) { - final item = _featuredPlaylist?[idx]; - return ListTile( - leading: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: item != null - ? AutoCacheImage( - item.images!.first.url!, - width: 64.0, - height: 64.0, - ) - : const SizedBox( - width: 64, - height: 64, - child: Center( - child: Icon(Icons.image), + body: CenteredContainer( + child: Skeletonizer( + enabled: _isLoading, + child: ListView.builder( + itemCount: _featuredPlaylist?.length ?? 20, + itemBuilder: (context, idx) { + final item = _featuredPlaylist?[idx]; + return ListTile( + leading: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: item != null + ? AutoCacheImage( + item.images!.first.url!, + width: 64.0, + height: 64.0, + ) + : const SizedBox( + width: 64, + height: 64, + child: Center( + child: Icon(Icons.image), + ), ), - ), - ), - title: Text(item?.name ?? 'Loading...'), - subtitle: Text( - item?.description ?? 'Please stand by...', - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - if (item == null) return; - GoRouter.of(context).pushNamed( - 'playlistView', - pathParameters: {'id': item.id!}, - ); - }, - ); - }, + ), + title: Text(item?.name ?? 'Loading...'), + subtitle: Text( + item?.description ?? 'Please stand by...', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + if (item == null) return; + GoRouter.of(context).pushNamed( + 'playlistView', + pathParameters: {'id': item.id!}, + ); + }, + ); + }, + ), ), ), ), diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index 4660f0b..99a1177 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -16,6 +16,7 @@ import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:rhythm_box/services/audio_services/image.dart'; +import 'package:rhythm_box/widgets/lyrics/synced.dart'; import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; class PlayerScreen extends StatefulWidget { @@ -50,9 +51,14 @@ class _PlayerScreenState extends State { double? _draggingValue; + static const double maxAlbumSize = 360; + @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; + final albumSize = max(size.shortestSide, maxAlbumSize).toDouble(); + + final isLargeScreen = size.width >= 720; return DismissiblePage( backgroundColor: Theme.of(context).colorScheme.surface, @@ -63,230 +69,260 @@ class _PlayerScreenState extends State { child: Material( color: Colors.transparent, child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: Row( children: [ - Hero( - tag: const Key('current-active-track-album-art'), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: AspectRatio( - aspectRatio: 1, - child: _albumArt != null - ? AutoCacheImage( - _albumArt!, - width: size.width, - height: size.width, - ) - : Container( - color: Theme.of(context) - .colorScheme - .surfaceContainerHigh, - width: 64, - height: 64, - child: const Center(child: Icon(Icons.image)), - ), - ), - ).marginSymmetric(horizontal: 24), - ), - const Gap(24), - Text( - _playback.state.value.activeTrack?.name ?? 'Not playing', - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - _playback.state.value.activeTrack?.artists?.asString() ?? - 'No author', - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - ), - const Gap(24), - Obx( - () => Column( + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - SliderTheme( - data: SliderThemeData( - trackHeight: 2, - trackShape: _PlayerProgressTrackShape(), - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 8, - ), - overlayShape: SliderComponentShape.noOverlay, - ), - child: Slider( - secondaryTrackValue: _playback - .durationBuffered.value.inMilliseconds - .abs() - .toDouble(), - value: _draggingValue?.abs() ?? - _playback.durationCurrent.value.inMilliseconds - .toDouble() - .abs(), - min: 0, - max: max( - _playback.durationCurrent.value.inMilliseconds.abs(), - _playback.durationTotal.value.inMilliseconds.abs(), - ).toDouble(), - onChanged: (value) { - setState(() => _draggingValue = value); - }, - onChangeEnd: (value) { - audioPlayer - .seek(Duration(milliseconds: value.toInt())); - }, + LimitedBox( + maxHeight: maxAlbumSize, + maxWidth: maxAlbumSize, + child: Hero( + tag: const Key('current-active-track-album-art'), + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(16)), + child: AspectRatio( + aspectRatio: 1, + child: _albumArt != null + ? AutoCacheImage( + _albumArt!, + width: albumSize, + height: albumSize, + ) + : Container( + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + width: 64, + height: 64, + child: + const Center(child: Icon(Icons.image)), + ), + ), + ).marginSymmetric(horizontal: 24), ), ), + const Gap(24), + Text( + _playback.state.value.activeTrack?.name ?? 'Not playing', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + _playback.state.value.activeTrack?.artists?.asString() ?? + 'No author', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + const Gap(24), + Obx( + () => Column( + children: [ + SliderTheme( + data: SliderThemeData( + trackHeight: 2, + trackShape: _PlayerProgressTrackShape(), + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + overlayShape: SliderComponentShape.noOverlay, + ), + child: Slider( + secondaryTrackValue: _playback + .durationBuffered.value.inMilliseconds + .abs() + .toDouble(), + value: _draggingValue?.abs() ?? + _playback.durationCurrent.value.inMilliseconds + .toDouble() + .abs(), + min: 0, + max: max( + _playback.durationCurrent.value.inMilliseconds + .abs(), + _playback.durationTotal.value.inMilliseconds + .abs(), + ).toDouble(), + onChanged: (value) { + setState(() => _draggingValue = value); + }, + onChangeEnd: (value) { + audioPlayer.seek( + Duration(milliseconds: value.toInt())); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _playback.durationCurrent.value + .toHumanReadableString(), + style: GoogleFonts.robotoMono(fontSize: 12), + ), + Text( + _playback.durationTotal.value + .toHumanReadableString(), + style: GoogleFonts.robotoMono(fontSize: 12), + ), + ], + ).paddingSymmetric(horizontal: 8, vertical: 4), + ], + ).paddingSymmetric(horizontal: 24), + ), + const Gap(24), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - _playback.durationCurrent.value - .toHumanReadableString(), - style: GoogleFonts.robotoMono(fontSize: 12), + StreamBuilder( + stream: audioPlayer.shuffledStream, + builder: (context, snapshot) { + final shuffled = snapshot.data ?? false; + return IconButton( + icon: Icon( + shuffled + ? Icons.shuffle_on_outlined + : Icons.shuffle, + ), + onPressed: _isFetchingActiveTrack + ? null + : () { + if (shuffled) { + audioPlayer.setShuffle(false); + } else { + audioPlayer.setShuffle(true); + } + }, + ); + }, ), - Text( - _playback.durationTotal.value.toHumanReadableString(), - style: GoogleFonts.robotoMono(fontSize: 12), + IconButton( + icon: const Icon(Icons.skip_previous), + onPressed: _isFetchingActiveTrack + ? null + : audioPlayer.skipToPrevious, + ), + const Gap(8), + SizedBox( + width: 56, + height: 56, + child: IconButton.filled( + icon: _isFetchingActiveTrack + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, + ), + ) + : Icon( + !_isPlaying + ? Icons.play_arrow + : Icons.pause, + size: 28, + ), + onPressed: _isFetchingActiveTrack + ? null + : _togglePlayState, + ), + ), + const Gap(8), + IconButton( + icon: const Icon(Icons.skip_next), + onPressed: _isFetchingActiveTrack + ? null + : audioPlayer.skipToNext, + ), + Obx( + () => IconButton( + icon: Icon( + _loopMode == PlaylistMode.none + ? Icons.repeat + : _loopMode == PlaylistMode.loop + ? Icons.repeat_on_outlined + : Icons.repeat_one_on_outlined, + ), + onPressed: _isFetchingActiveTrack + ? null + : () async { + await audioPlayer.setLoopMode( + switch (_loopMode) { + PlaylistMode.loop => + PlaylistMode.single, + PlaylistMode.single => + PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, + }, + ); + }, + ), ), ], - ).paddingSymmetric(horizontal: 8, vertical: 4), - ], - ).paddingSymmetric(horizontal: 24), - ), - const Gap(24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamBuilder( - stream: audioPlayer.shuffledStream, - builder: (context, snapshot) { - final shuffled = snapshot.data ?? false; - return IconButton( - icon: Icon( - shuffled ? Icons.shuffle_on_outlined : Icons.shuffle, - ), - onPressed: _isFetchingActiveTrack - ? null - : () { - if (shuffled) { - audioPlayer.setShuffle(false); - } else { - audioPlayer.setShuffle(true); + ), + const Gap(20), + Row( + children: [ + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.queue_music), + label: const Text('Queue'), + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + isScrollControlled: true, + context: context, + builder: (context) => const PlayerQueuePopup(), + ).then((_) { + if (mounted) { + setState(() {}); } - }, - ); - }, - ), - IconButton( - icon: const Icon(Icons.skip_previous), - onPressed: _isFetchingActiveTrack - ? null - : audioPlayer.skipToPrevious, - ), - const Gap(8), - SizedBox( - width: 56, - height: 56, - child: IconButton.filled( - icon: _isFetchingActiveTrack - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.5, - ), - ) - : Icon( - !_isPlaying ? Icons.play_arrow : Icons.pause, - size: 28, - ), - onPressed: - _isFetchingActiveTrack ? null : _togglePlayState, - ), - ), - const Gap(8), - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: - _isFetchingActiveTrack ? null : audioPlayer.skipToNext, - ), - Obx( - () => IconButton( - icon: Icon( - _loopMode == PlaylistMode.none - ? Icons.repeat - : _loopMode == PlaylistMode.loop - ? Icons.repeat_on_outlined - : Icons.repeat_one_on_outlined, - ), - onPressed: _isFetchingActiveTrack - ? null - : () async { - await audioPlayer.setLoopMode( - switch (_loopMode) { - PlaylistMode.loop => PlaylistMode.single, - PlaylistMode.single => PlaylistMode.none, - PlaylistMode.none => PlaylistMode.loop, - }, - ); + }); }, + ), + ), + if (!isLargeScreen) const Gap(4), + if (!isLargeScreen) + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.lyrics), + label: const Text('Lyrics'), + onPressed: () { + GoRouter.of(context).pushNamed('playerLyrics'); + }, + ), + ), + const Gap(4), + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.merge), + label: const Text('Sources'), + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + isScrollControlled: true, + context: context, + builder: (context) => + const SiblingTracksPopup(), + ).then((_) { + if (mounted) { + setState(() {}); + } + }); + }, + ), + ), + ], ), - ), - ], - ), - const Gap(20), - Row( - children: [ - Expanded( - child: TextButton.icon( - icon: const Icon(Icons.queue_music), - label: const Text('Queue'), - onPressed: () { - showModalBottomSheet( - useRootNavigator: true, - isScrollControlled: true, - context: context, - builder: (context) => const PlayerQueuePopup(), - ).then((_) { - if (mounted) { - setState(() {}); - } - }); - }, - ), - ), - const Gap(4), - Expanded( - child: TextButton.icon( - icon: const Icon(Icons.lyrics), - label: const Text('Lyrics'), - onPressed: () { - GoRouter.of(context).pushNamed('playerLyrics'); - }, - ), - ), - const Gap(4), - Expanded( - child: TextButton.icon( - icon: const Icon(Icons.merge), - label: const Text('Sources'), - onPressed: () { - showModalBottomSheet( - useRootNavigator: true, - isScrollControlled: true, - context: context, - builder: (context) => const SiblingTracksPopup(), - ).then((_) { - if (mounted) { - setState(() {}); - } - }); - }, - ), - ), - ], + ], + ), ), + if (isLargeScreen) const Gap(24), + if (isLargeScreen) + const Expanded( + child: SyncedLyrics(defaultTextZoom: 67), + ) ], ), ).marginAll(24), diff --git a/lib/screens/playlist/view.dart b/lib/screens/playlist/view.dart index 419f164..d434034 100644 --- a/lib/screens/playlist/view.dart +++ b/lib/screens/playlist/view.dart @@ -10,6 +10,7 @@ import 'package:rhythm_box/providers/history.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; +import 'package:rhythm_box/widgets/sized_container.dart'; import 'package:rhythm_box/widgets/tracks/playlist_track_list.dart'; import 'package:spotify/spotify.dart'; @@ -60,6 +61,7 @@ class _PlaylistViewScreenState extends State { child: Scaffold( appBar: AppBar( title: const Text('Playlist'), + centerTitle: MediaQuery.of(context).size.width >= 720, ), body: Builder( builder: (context) { @@ -69,86 +71,115 @@ class _PlaylistViewScreenState extends State { ); } - return CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Material( - borderRadius: radius, - elevation: 2, - child: ClipRRect( + return CenteredContainer( + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Material( borderRadius: radius, - child: Hero( - tag: Key('playlist-cover-${_playlist!.id}'), - child: AutoCacheImage( - _playlist!.images!.first.url!, - width: 160.0, - height: 160.0, + elevation: 2, + child: ClipRRect( + borderRadius: radius, + child: Hero( + tag: Key('playlist-cover-${_playlist!.id}'), + child: AutoCacheImage( + _playlist!.images!.first.url!, + width: 160.0, + height: 160.0, + ), ), ), ), - ), - const Gap(24), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _playlist!.name ?? 'Playlist', - style: - Theme.of(context).textTheme.headlineSmall, - maxLines: 2, - overflow: TextOverflow.fade, - ), - Text( - _playlist!.description ?? 'A Playlist', - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - const Gap(8), - Text( - "${NumberFormat.compactCurrency(symbol: '', decimalDigits: 2).format(_playlist!.followers!.total!)} saves", - ), - Text( - '#${_playlist!.id}', - style: GoogleFonts.robotoMono(fontSize: 10), - ), - ], + const Gap(24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _playlist!.name ?? 'Playlist', + style: Theme.of(context) + .textTheme + .headlineSmall, + maxLines: 2, + overflow: TextOverflow.fade, + ), + Text( + _playlist!.description ?? 'A Playlist', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + const Gap(8), + Text( + "${NumberFormat.compactCurrency(symbol: '', decimalDigits: 2).format(_playlist!.followers!.total!)} saves", + ), + Text( + '#${_playlist!.id}', + style: GoogleFonts.robotoMono(fontSize: 10), + ), + ], + ), ), - ), - ], - ).paddingOnly(left: 24, right: 24, top: 24), - const Gap(8), - Wrap( - spacing: 8, - children: [ - Obx( - () => ElevatedButton.icon( - icon: (_isCurrentPlaylist && - _playback.isPlaying.value) - ? const Icon(Icons.pause_outlined) - : const Icon(Icons.play_arrow), - label: const Text('Play'), + ], + ).paddingOnly(left: 24, right: 24, top: 24), + const Gap(8), + Wrap( + spacing: 8, + children: [ + Obx( + () => ElevatedButton.icon( + icon: (_isCurrentPlaylist && + _playback.isPlaying.value) + ? const Icon(Icons.pause_outlined) + : const Icon(Icons.play_arrow), + label: const Text('Play'), + onPressed: _isUpdating + ? null + : () async { + if (_isCurrentPlaylist && + _playback.isPlaying.value) { + audioPlayer.pause(); + return; + } else if (_isCurrentPlaylist && + !_playback.isPlaying.value) { + audioPlayer.resume(); + return; + } + + setState(() => _isUpdating = true); + + final tracks = (await _spotify + .api.playlists + .getTracksByPlaylistId( + widget.playlistId) + .all()) + .toList(); + + await _playback.load(tracks, + autoPlay: true); + _playback.addCollection(_playlist!.id!); + Get.find() + .addPlaylists([_playlist!]); + + setState(() => _isUpdating = false); + }, + ), + ), + TextButton.icon( + icon: const Icon(Icons.shuffle), + label: const Text('Shuffle'), onPressed: _isUpdating ? null : () async { - if (_isCurrentPlaylist && - _playback.isPlaying.value) { - audioPlayer.pause(); - return; - } else if (_isCurrentPlaylist && - !_playback.isPlaying.value) { - audioPlayer.resume(); - return; - } - setState(() => _isUpdating = true); + audioPlayer.setShuffle(true); + final tracks = (await _spotify .api.playlists .getTracksByPlaylistId( @@ -156,8 +187,12 @@ class _PlaylistViewScreenState extends State { .all()) .toList(); - await _playback.load(tracks, - autoPlay: true); + await _playback.load( + tracks, + autoPlay: true, + initialIndex: + Random().nextInt(tracks.length), + ); _playback.addCollection(_playlist!.id!); Get.find() .addPlaylists([_playlist!]); @@ -165,50 +200,21 @@ class _PlaylistViewScreenState extends State { setState(() => _isUpdating = false); }, ), - ), - TextButton.icon( - icon: const Icon(Icons.shuffle), - label: const Text('Shuffle'), - onPressed: _isUpdating - ? null - : () async { - setState(() => _isUpdating = true); - - audioPlayer.setShuffle(true); - - final tracks = (await _spotify.api.playlists - .getTracksByPlaylistId( - widget.playlistId) - .all()) - .toList(); - - await _playback.load( - tracks, - autoPlay: true, - initialIndex: - Random().nextInt(tracks.length), - ); - _playback.addCollection(_playlist!.id!); - Get.find() - .addPlaylists([_playlist!]); - - setState(() => _isUpdating = false); - }, - ), - ], - ).paddingSymmetric(horizontal: 24), - const Gap(24), - ], + ], + ).paddingSymmetric(horizontal: 24), + const Gap(24), + ], + ), ), - ), - SliverToBoxAdapter( - child: Text( - 'Songs (${_playlist!.tracks!.total})', - style: Theme.of(context).textTheme.titleLarge, - ).paddingOnly(left: 28, right: 28, bottom: 4), - ), - PlaylistTrackList(playlistId: widget.playlistId), - ], + SliverToBoxAdapter( + child: Text( + 'Songs (${_playlist!.tracks!.total})', + style: Theme.of(context).textTheme.titleLarge, + ).paddingOnly(left: 28, right: 28, bottom: 4), + ), + PlaylistTrackList(playlistId: widget.playlistId), + ], + ), ); }, ), diff --git a/lib/screens/search/view.dart b/lib/screens/search/view.dart index 457fe27..29d6b54 100644 --- a/lib/screens/search/view.dart +++ b/lib/screens/search/view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/providers/user_preferences.dart'; +import 'package:rhythm_box/widgets/sized_container.dart'; import 'package:rhythm_box/widgets/tracks/track_list.dart'; import 'package:spotify/spotify.dart'; @@ -61,11 +62,13 @@ class _SearchScreenState extends State { FocusManager.instance.primaryFocus?.unfocus(), ).paddingSymmetric(horizontal: 24, vertical: 8), Expanded( - child: CustomScrollView( - slivers: [ - if (_searchResult != null) - TrackSliverList(tracks: List.from(_searchResult!)), - ], + child: CenteredContainer( + child: CustomScrollView( + slivers: [ + if (_searchResult != null) + TrackSliverList(tracks: List.from(_searchResult!)), + ], + ), ), ), ], diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 3776d03..24229d0 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -10,6 +10,22 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { @override Widget build(BuildContext context) { - return const Placeholder(); + return Material( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Icons.login), + title: const Text('Connect with Spotify'), + subtitle: const Text('To explore your own library and more'), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ], + ), + ), + ); } } diff --git a/lib/shells/nav_shell.dart b/lib/shells/nav_shell.dart index 1948265..d9244d4 100644 --- a/lib/shells/nav_shell.dart +++ b/lib/shells/nav_shell.dart @@ -40,6 +40,7 @@ class _NavShellState extends State { const BottomPlayer(key: Key('app-wide-bottom-player')), const Divider(height: 0.3, thickness: 0.3), BottomNavigationBar( + landscapeLayout: BottomNavigationBarLandscapeLayout.centered, elevation: 0, showUnselectedLabels: false, currentIndex: _focusDestination, diff --git a/lib/widgets/sized_container.dart b/lib/widgets/sized_container.dart new file mode 100644 index 0000000..dfc1859 --- /dev/null +++ b/lib/widgets/sized_container.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class SizedContainer extends StatelessWidget { + final Widget child; + final double maxWidth; + final double maxHeight; + + const SizedContainer({ + super.key, + required this.child, + this.maxWidth = 720, + this.maxHeight = double.infinity, + }); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight), + child: child, + ), + ); + } +} + +class CenteredContainer extends StatelessWidget { + final Widget child; + final double maxWidth; + + const CenteredContainer({ + super.key, + required this.child, + this.maxWidth = 720, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth), + child: child, + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a1a26e0..3501420 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,9 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3988661..7a7f996 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window flutter_secure_storage_linux media_kit_libs_linux screen_retriever diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index bd48a67..dff91bc 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,7 +7,9 @@ import Foundation import audio_service import audio_session +import desktop_webview_window import device_info_plus +import flutter_inappwebview_macos import flutter_secure_storage_macos import media_kit_libs_macos_audio import package_info_plus @@ -21,7 +23,9 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index a462b7f..5098386 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -302,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + desktop_webview_window: + dependency: "direct main" + description: + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + url: "https://pub.dev" + source: hosted + version: "0.2.3" device_info_plus: dependency: "direct main" description: @@ -443,6 +451,62 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_launcher_icons: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 186ce8f..e98e420 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,8 @@ dependencies: animations: ^2.0.11 flutter_animate: ^4.5.0 duration: ^4.0.3 + desktop_webview_window: ^0.2.3 + flutter_inappwebview: ^6.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 29db5d3..a747303 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a39a49c..d45225c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window flutter_secure_storage_windows media_kit_libs_windows_audio screen_retriever