feat(tablet): clamp remaining screens - settings, queue lists, wizard

- SettingsGroup centers itself at 720dp via its incoming constraint
  (not screen width, so groups inside clamped sheets are never
  over-inset; unbounded-width contexts guarded); priority scaffold,
  bespoke hero cards, and donate page get the same inset - all 18+
  settings pages clamp from shared widgets
- queue list-mode, local/downloaded album track lists, favorite
  artists, home recent-access, and extension detail sections adopt
  wideListInset like the screens fixed earlier
- track metadata content clamped at 720dp, which also tames the
  width-derived spectrogram height
- announcement dialog capped at 480dp wide; setup wizard and
  tutorial content at 560dp with a 400dp action button
This commit is contained in:
zarzet
2026-07-14 09:10:16 +07:00
parent 7573d360bf
commit 1893f3faa8
23 changed files with 499 additions and 321 deletions
+24 -17
View File
@@ -9,6 +9,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/services/batch_track_actions.dart';
import 'package:spotiflac_android/models/unified_library_item.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/confirm_and_delete_tracks.dart';
import 'package:spotiflac_android/utils/cover_art_utils.dart';
import 'package:spotiflac_android/utils/file_access.dart';
@@ -515,23 +516,26 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
final discMap = _getDiscGroups(tracks);
if (discMap.length <= 1) {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final track = tracks[index];
return KeyedSubtree(
key: ValueKey(track.id),
child: StaggeredListItem(
index: index,
child: _buildTrackItem(
context,
colorScheme,
track,
tracks,
index,
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final track = tracks[index];
return KeyedSubtree(
key: ValueKey(track.id),
child: StaggeredListItem(
index: index,
child: _buildTrackItem(
context,
colorScheme,
track,
tracks,
index,
),
),
),
);
}, childCount: tracks.length),
);
}, childCount: tracks.length),
),
);
}
@@ -565,7 +569,10 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
}
}
return SliverList(delegate: SliverChildListDelegate(children));
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverList(delegate: SliverChildListDelegate(children)),
);
}
Widget _buildTrackItem(
+60 -56
View File
@@ -5,6 +5,7 @@ import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/screens/artist_screen.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
@@ -58,67 +59,70 @@ class FavoriteArtistsScreen extends ConsumerWidget {
),
)
else
SliverList.separated(
itemCount: artists.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final artist = artists[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
leading: _ArtistThumbnail(artist: artist),
title: Text(
artist.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle:
artist.providerId == null || artist.providerId!.isEmpty
? null
: Text(
artist.providerId!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
tooltip: context.l10n.artistOptionRemoveFromFavorites,
icon: Icon(Icons.favorite, color: colorScheme.error),
onPressed: () async {
await ref
.read(libraryCollectionsProvider.notifier)
.removeFavoriteArtist(artist.key);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.collectionRemovedFromFavoriteArtists(
artist.name,
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverList.separated(
itemCount: artists.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final artist = artists[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
leading: _ArtistThumbnail(artist: artist),
title: Text(
artist.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle:
artist.providerId == null || artist.providerId!.isEmpty
? null
: Text(
artist.providerId!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
tooltip: context.l10n.artistOptionRemoveFromFavorites,
icon: Icon(Icons.favorite, color: colorScheme.error),
onPressed: () async {
await ref
.read(libraryCollectionsProvider.notifier)
.removeFavoriteArtist(artist.key);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.collectionRemovedFromFavoriteArtists(
artist.name,
),
),
),
);
},
),
onTap: () {
Navigator.of(context).push(
slidePageRoute<void>(
page: ArtistScreen(
artistId: artist.artistId,
artistName: artist.name,
coverUrl: artist.imageUrl,
extensionId:
artist.providerId != null &&
artist.providerId!.isNotEmpty
? artist.providerId
: null,
),
),
);
},
),
onTap: () {
Navigator.of(context).push(
slidePageRoute<void>(
page: ArtistScreen(
artistId: artist.artistId,
artistName: artist.name,
coverUrl: artist.imageUrl,
extensionId:
artist.providerId != null &&
artist.providerId!.isNotEmpty
? artist.providerId
: null,
),
),
);
},
);
},
);
},
),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
+3 -1
View File
@@ -2209,7 +2209,9 @@ class _HomeTabState extends ConsumerState<HomeTab>
final hasHiddenDownloads = view.hasHiddenDownloads;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
padding:
const EdgeInsets.fromLTRB(16, 8, 16, 8) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
+5 -1
View File
@@ -7,6 +7,7 @@ import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/confirm_and_delete_tracks.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
@@ -372,7 +373,10 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
);
}
return SliverMainAxisGroup(slivers: slivers);
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverMainAxisGroup(slivers: slivers),
);
}
Widget _buildTrackItem(
+1
View File
@@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
+80 -72
View File
@@ -469,36 +469,52 @@ extension _QueueTabFilterWidgets on _QueueTabState {
),
)
else
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index < collectionCount) {
return _buildAllTabListCollectionItem(
context: context,
colorScheme: colorScheme,
entry: collectionEntries[index],
collectionState: collectionState,
filteredUnifiedItems: filteredUnifiedItems,
);
}
final afterCollections = index - collectionCount;
if (afterCollections < leadCount) {
return leadListCell(afterCollections);
}
final trackIndex = afterCollections - leadCount;
if (trackIndex < filteredUnifiedItems.length) {
final item = filteredUnifiedItems[trackIndex];
return KeyedSubtree(
key: ValueKey(item.id),
child: LongPressDraggable<UnifiedLibraryItem>(
data: item,
feedback: _buildDragFeedback(
context,
item,
colorScheme,
),
childWhenDragging: Opacity(
opacity: 0.4,
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index < collectionCount) {
return _buildAllTabListCollectionItem(
context: context,
colorScheme: colorScheme,
entry: collectionEntries[index],
collectionState: collectionState,
filteredUnifiedItems: filteredUnifiedItems,
);
}
final afterCollections = index - collectionCount;
if (afterCollections < leadCount) {
return leadListCell(afterCollections);
}
final trackIndex = afterCollections - leadCount;
if (trackIndex < filteredUnifiedItems.length) {
final item = filteredUnifiedItems[trackIndex];
return KeyedSubtree(
key: ValueKey(item.id),
child: LongPressDraggable<UnifiedLibraryItem>(
data: item,
feedback: _buildDragFeedback(
context,
item,
colorScheme,
),
childWhenDragging: Opacity(
opacity: 0.4,
child: _buildUnifiedLibraryItem(
context,
item,
colorScheme,
downloadedNavigationItems:
downloadedNavigationItems,
downloadedNavigationIndex:
downloadedNavigationIndexByUnifiedId[item.id],
localNavigationItems: localNavigationItems,
localNavigationIndex:
localNavigationIndexByUnifiedId[item.id],
libraryItems: filteredUnifiedItems,
),
),
child: _buildUnifiedLibraryItem(
context,
item,
@@ -513,25 +529,13 @@ extension _QueueTabFilterWidgets on _QueueTabState {
libraryItems: filteredUnifiedItems,
),
),
child: _buildUnifiedLibraryItem(
context,
item,
colorScheme,
downloadedNavigationItems: downloadedNavigationItems,
downloadedNavigationIndex:
downloadedNavigationIndexByUnifiedId[item.id],
localNavigationItems: localNavigationItems,
localNavigationIndex:
localNavigationIndexByUnifiedId[item.id],
libraryItems: filteredUnifiedItems,
),
),
);
}
return const SizedBox.shrink();
},
childCount:
leadCount + collectionCount + filteredUnifiedItems.length,
);
}
return const SizedBox.shrink();
},
childCount:
leadCount + collectionCount + filteredUnifiedItems.length,
),
),
),
],
@@ -605,28 +609,33 @@ extension _QueueTabFilterWidgets on _QueueTabState {
}, childCount: leadCount + filteredUnifiedItems.length),
),
)
: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index < leadCount) {
return leadListCell(index);
}
final item = filteredUnifiedItems[index - leadCount];
return KeyedSubtree(
key: ValueKey(item.id),
child: _buildUnifiedLibraryItem(
context,
item,
colorScheme,
downloadedNavigationItems: downloadedNavigationItems,
downloadedNavigationIndex:
downloadedNavigationIndexByUnifiedId[item.id],
localNavigationItems: localNavigationItems,
localNavigationIndex:
localNavigationIndexByUnifiedId[item.id],
libraryItems: filteredUnifiedItems,
),
);
}, childCount: leadCount + filteredUnifiedItems.length),
: SliverPadding(
padding: EdgeInsets.symmetric(
horizontal: wideListInset(context),
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index < leadCount) {
return leadListCell(index);
}
final item = filteredUnifiedItems[index - leadCount];
return KeyedSubtree(
key: ValueKey(item.id),
child: _buildUnifiedLibraryItem(
context,
item,
colorScheme,
downloadedNavigationItems: downloadedNavigationItems,
downloadedNavigationIndex:
downloadedNavigationIndexByUnifiedId[item.id],
localNavigationItems: localNavigationItems,
localNavigationIndex:
localNavigationIndexByUnifiedId[item.id],
libraryItems: filteredUnifiedItems,
),
);
}, childCount: leadCount + filteredUnifiedItems.length),
),
),
if (!hasQueueItems &&
@@ -960,5 +969,4 @@ extension _QueueTabFilterWidgets on _QueueTabState {
return selectedItems.isNotEmpty &&
selectedItems.every((item) => item.localItem != null);
}
}
+19 -6
View File
@@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/repo_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
class ExtensionDetailsScreen extends ConsumerStatefulWidget {
@@ -153,7 +154,9 @@ class _ExtensionDetailsScreenState
) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
padding:
const EdgeInsets.all(16) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
child: Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
@@ -295,7 +298,9 @@ class _ExtensionDetailsScreenState
) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
padding:
const EdgeInsets.fromLTRB(20, 8, 20, 8) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
child: Row(
children: [
Icon(icon, size: 20, color: colorScheme.primary),
@@ -320,7 +325,9 @@ class _ExtensionDetailsScreenState
) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 8) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
child: Text(
ext.description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
@@ -339,7 +346,9 @@ class _ExtensionDetailsScreenState
) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 8) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
child: Wrap(
spacing: 8,
runSpacing: 8,
@@ -369,7 +378,9 @@ class _ExtensionDetailsScreenState
ColorScheme colorScheme,
) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverToBoxAdapter(
child: Card(
elevation: 0,
@@ -416,7 +427,9 @@ class _ExtensionDetailsScreenState
final isUtility = ext.category == 'utility';
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8) +
EdgeInsets.symmetric(horizontal: wideListInset(context)),
sliver: SliverToBoxAdapter(
child: Card(
elevation: 0,
+7 -1
View File
@@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
import 'package:spotiflac_android/constants/app_info.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
@@ -23,7 +24,12 @@ class AboutPage extends StatelessWidget {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
padding: EdgeInsets.fromLTRB(
16 + wideListInset(context),
16,
16 + wideListInset(context),
8,
),
child: _AppHeaderCard(),
),
),
@@ -4,6 +4,7 @@ import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/l10n/supported_locales.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
@@ -24,8 +25,8 @@ class AppearanceSettingsPage extends ConsumerWidget {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
padding: EdgeInsets.symmetric(
horizontal: 16 + wideListInset(context),
vertical: 8,
),
child: _ThemePreviewCard(),
+7 -1
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:spotiflac_android/services/app_remote_config_service.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/donate_icons.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
@@ -63,7 +64,12 @@ class _DonatePageState extends State<DonatePage> {
const SettingsSliverAppBar(title: 'Donate'),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.fromLTRB(
16 + wideListInset(context),
16,
16 + wideListInset(context),
16,
),
child: Column(
children: [
_DonateLinksCard(colorScheme: colorScheme, config: _config),
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/discard_changes_dialog.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
@@ -53,6 +54,7 @@ class _DownloadFallbackExtensionsPageState
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final wideInset = wideListInset(context);
return PopScope(
canPop: !_hasChanges,
@@ -92,7 +94,12 @@ class _DownloadFallbackExtensionsPageState
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.fromLTRB(
16 + wideInset,
16,
16 + wideInset,
16,
),
child: Text(
context.l10n.providerPriorityFallbackExtensionsDescription,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
@@ -104,7 +111,7 @@ class _DownloadFallbackExtensionsPageState
if (_extensions.isEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(horizontal: 16 + wideInset),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -124,7 +131,7 @@ class _DownloadFallbackExtensionsPageState
),
if (_extensions.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(horizontal: 16 + wideInset),
sliver: SliverToBoxAdapter(
child: SettingsGroup(
margin: EdgeInsets.zero,
@@ -157,7 +164,12 @@ class _DownloadFallbackExtensionsPageState
if (_extensions.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
padding: EdgeInsets.fromLTRB(
16 + wideInset,
8,
16 + wideInset,
0,
),
child: Text(
context.l10n.providerPriorityFallbackExtensionsHint,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
@@ -8,6 +8,7 @@ import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
@@ -715,7 +716,10 @@ class _LibraryHeroCard extends StatelessWidget {
: itemCount + excludedDownloadedCount;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
margin: EdgeInsets.symmetric(
horizontal: 16 + wideListInset(context),
vertical: 8,
),
constraints: const BoxConstraints(minHeight: 220),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/discard_changes_dialog.dart';
import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart';
import 'package:spotiflac_android/widgets/reorderable_priority_item.dart';
@@ -83,7 +84,9 @@ class _LyricsProviderPriorityPageState
),
if (_enabledProviders.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(
horizontal: 16 + wideListInset(context),
),
sliver: SliverReorderableList(
itemCount: _enabledProviders.length,
itemBuilder: (context, index) {
@@ -127,7 +130,9 @@ class _LyricsProviderPriorityPageState
),
if (disabled.isNotEmpty)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(
horizontal: 16 + wideListInset(context),
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final id = disabled[index];
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/discard_changes_dialog.dart';
import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart';
import 'package:spotiflac_android/widgets/reorderable_priority_item.dart';
@@ -57,7 +58,9 @@ class _MetadataProviderPriorityPageState
onConfirmDiscard: showDiscardChangesDialog,
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(
horizontal: 16 + wideListInset(context),
),
sliver: SliverReorderableList(
itemCount: _providers.length,
itemBuilder: (context, index) {
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/discard_changes_dialog.dart';
import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart';
import 'package:spotiflac_android/widgets/reorderable_priority_item.dart';
@@ -55,7 +56,9 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
onConfirmDiscard: showDiscardChangesDialog,
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.symmetric(
horizontal: 16 + wideListInset(context),
),
sliver: SliverReorderableList(
itemCount: _providers.length,
itemBuilder: (context, index) {
+21 -5
View File
@@ -15,6 +15,7 @@ import 'package:spotiflac_android/screens/settings/cache_management_page.dart';
import 'package:spotiflac_android/screens/settings/backup_restore_page.dart';
import 'package:spotiflac_android/screens/settings/donate_page.dart';
import 'package:spotiflac_android/screens/settings/log_screen.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
@@ -28,6 +29,7 @@ class SettingsTab extends ConsumerWidget {
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final bottomInset = context.navBarBottomInset;
final wideInset = wideListInset(context);
return CustomScrollView(
slivers: [
@@ -69,7 +71,12 @@ class SettingsTab extends ConsumerWidget {
builder: (context) {
final l10n = context.l10n;
return SettingsGroup(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 4),
margin: EdgeInsets.fromLTRB(
16 + wideInset,
16,
16 + wideInset,
4,
),
children: [
SettingsItem(
icon: Icons.palette_outlined,
@@ -103,7 +110,12 @@ class SettingsTab extends ConsumerWidget {
builder: (context) {
final l10n = context.l10n;
return SettingsGroup(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 4),
margin: EdgeInsets.fromLTRB(
16 + wideInset,
4,
16 + wideInset,
4,
),
children: [
SettingsItem(
icon: Icons.download_outlined,
@@ -145,7 +157,12 @@ class SettingsTab extends ConsumerWidget {
builder: (context) {
final l10n = context.l10n;
return SettingsGroup(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 4),
margin: EdgeInsets.fromLTRB(
16 + wideInset,
4,
16 + wideInset,
4,
),
children: [
SettingsItem(
icon: Icons.storage_outlined,
@@ -158,8 +175,7 @@ class SettingsTab extends ConsumerWidget {
icon: Icons.tune_outlined,
title: l10n.settingsApp,
subtitle: l10n.settingsAppSubtitle,
onTap: () =>
_navigateTo(context, const AppSettingsPage()),
onTap: () => _navigateTo(context, const AppSettingsPage()),
),
SettingsItem(
icon: Icons.settings_backup_restore,
+91 -66
View File
@@ -9,6 +9,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/l10n/supported_locales.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/logger.dart';
@@ -653,41 +654,46 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: minContentHeight),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo-transparent.png',
width: logoSize,
height: logoSize,
color: colorScheme.primary,
fit: BoxFit.contain,
),
SizedBox(height: titleGap),
Text(
context.l10n.appName,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
fontSize:
(Theme.of(context).textTheme.displaySmall?.fontSize ??
36) *
(1 + ((textScale - 1) * 0.18)),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: minContentHeight,
maxWidth: 560,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo-transparent.png',
width: logoSize,
height: logoSize,
color: colorScheme.primary,
fit: BoxFit.contain,
),
),
SizedBox(height: subtitleGap),
Text(
context.l10n.setupDownloadInFlac,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.5,
SizedBox(height: titleGap),
Text(
context.l10n.appName,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
fontSize:
(Theme.of(context).textTheme.displaySmall?.fontSize ??
36) *
(1 + ((textScale - 1) * 0.18)),
),
),
),
],
SizedBox(height: subtitleGap),
Text(
context.l10n.setupDownloadInFlac,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.5,
),
),
],
),
),
),
);
@@ -743,7 +749,12 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
padding: EdgeInsets.fromLTRB(
24 + wideListInset(context, contentMaxWidth: 560),
24,
24 + wideListInset(context, contentMaxWidth: 560),
0,
),
child: Column(
children: [
Container(
@@ -782,7 +793,12 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 80),
padding: EdgeInsets.fromLTRB(
24 + wideListInset(context, contentMaxWidth: 560),
0,
24 + wideListInset(context, contentMaxWidth: 560),
80,
),
itemCount: languages.length,
itemBuilder: (context, index) {
final lang = languages[index];
@@ -984,40 +1000,49 @@ class _StepLayout extends StatelessWidget {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: minContentHeight),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(iconPadding),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
shape: BoxShape.circle,
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: minContentHeight,
maxWidth: 560,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(iconPadding),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: iconSize,
color: colorScheme.primary,
),
),
child: Icon(icon, size: iconSize, color: colorScheme.primary),
),
SizedBox(height: titleGap),
Text(
title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
SizedBox(height: titleGap),
Text(
title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
SizedBox(height: descriptionGap),
Text(
description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.5,
SizedBox(height: descriptionGap),
Text(
description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.5,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
SizedBox(height: actionGap),
child,
],
SizedBox(height: actionGap),
child,
],
),
),
),
);
+6 -1
View File
@@ -1181,7 +1181,12 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen>
),
SliverToBoxAdapter(
child: _buildAnimatedTrackContent(context, ref, colorScheme),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: _buildAnimatedTrackContent(context, ref, colorScheme),
),
),
),
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
],
+76 -64
View File
@@ -244,21 +244,26 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
}),
),
SizedBox(height: bottomGap),
SizedBox(
width: double.infinity,
height: actionButtonHeight,
child: FilledButton(
onPressed: _nextPage,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
),
child: Text(
isLastPage ? l10n.setupGetStarted : l10n.setupNext,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: SizedBox(
width: double.infinity,
height: actionButtonHeight,
child: FilledButton(
onPressed: _nextPage,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
),
child: Text(
isLastPage ? l10n.setupGetStarted : l10n.setupNext,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
),
@@ -712,58 +717,65 @@ class _TutorialPage extends StatelessWidget {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
SizedBox(height: topGap),
AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutBack,
transform: Matrix4.translationValues(0, isActive ? 0 : -20, 0),
padding: EdgeInsets.all(iconPadding),
decoration: BoxDecoration(
color: (iconColor ?? colorScheme.primary).withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: iconSize,
color: iconColor ?? colorScheme.primary,
),
),
SizedBox(height: iconTextGap),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: isActive ? 1.0 : 0.0,
curve: Curves.easeOut,
child: Text(
title,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
letterSpacing: -0.5,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(
children: [
SizedBox(height: topGap),
AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutBack,
transform: Matrix4.translationValues(0, isActive ? 0 : -20, 0),
padding: EdgeInsets.all(iconPadding),
decoration: BoxDecoration(
color: (iconColor ?? colorScheme.primary).withValues(
alpha: 0.15,
),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: iconSize,
color: iconColor ?? colorScheme.primary,
),
),
textAlign: TextAlign.center,
),
),
SizedBox(height: descriptionGap),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: isActive ? 1.0 : 0.0,
curve: Curves.easeOut,
child: Text(
description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.5,
fontSize: 16 * (1 + ((textScale - 1) * 0.1)),
SizedBox(height: iconTextGap),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: isActive ? 1.0 : 0.0,
curve: Curves.easeOut,
child: Text(
title,
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
letterSpacing: -0.5,
),
textAlign: TextAlign.center,
),
),
textAlign: TextAlign.center,
),
SizedBox(height: descriptionGap),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: isActive ? 1.0 : 0.0,
curve: Curves.easeOut,
child: Text(
description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.5,
fontSize: 16 * (1 + ((textScale - 1) * 0.1)),
),
textAlign: TextAlign.center,
),
),
SizedBox(height: contentGap),
content, // The content itself now handles its own internal animations
SizedBox(height: bottomGap),
],
),
SizedBox(height: contentGap),
content, // The content itself now handles its own internal animations
SizedBox(height: bottomGap),
],
),
),
);
}
+12 -4
View File
@@ -1,9 +1,17 @@
import 'package:flutter/widgets.dart';
/// Horizontal inset that centers content of [maxWidth] at [contentMaxWidth];
/// zero once it fits, so rows stop stretching across a wide surface. Prefer
/// this constraint-based form when the widget may live inside an already
/// clamped box (e.g. a bottom sheet), where screen width would over-inset.
double wideInsetForWidth(double maxWidth, {double contentMaxWidth = 720}) =>
maxWidth > contentMaxWidth ? (maxWidth - contentMaxWidth) / 2 : 0;
/// Horizontal inset that centers full-width list content at [contentMaxWidth]
/// on tablets/landscape; zero on phones, so rows stop stretching across the
/// whole screen.
double wideListInset(BuildContext context, {double contentMaxWidth = 720}) {
final width = MediaQuery.sizeOf(context).width;
return width > contentMaxWidth ? (width - contentMaxWidth) / 2 : 0;
}
double wideListInset(BuildContext context, {double contentMaxWidth = 720}) =>
wideInsetForWidth(
MediaQuery.sizeOf(context).width,
contentMaxWidth: contentMaxWidth,
);
+1 -1
View File
@@ -101,7 +101,7 @@ class AppAnnouncementDialog extends StatelessWidget {
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 260),
constraints: const BoxConstraints(maxHeight: 260, maxWidth: 480),
child: SingleChildScrollView(
child: Text(
announcement.message,
+11 -2
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
class PrioritySettingsScaffold extends StatelessWidget {
@@ -40,6 +41,7 @@ class PrioritySettingsScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final wideInset = wideListInset(context);
return PopScope(
canPop: !hasChanges,
@@ -70,7 +72,9 @@ class PrioritySettingsScaffold extends StatelessWidget {
),
SliverToBoxAdapter(
child: Padding(
padding: descriptionPadding,
padding: descriptionPadding.add(
EdgeInsets.symmetric(horizontal: wideInset),
),
child: Text(
description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
@@ -82,7 +86,12 @@ class PrioritySettingsScaffold extends StatelessWidget {
...slivers,
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.fromLTRB(
16 + wideInset,
16,
16 + wideInset,
16,
),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
+36 -12
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
/// Background fill for grouped cards, matching the Settings group look. Blends a
/// translucent overlay over the surface so it stays visible on AMOLED (pure
@@ -28,19 +29,42 @@ class SettingsGroup extends StatelessWidget {
final cardColor = settingsGroupColor(context);
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: margin ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
final decoration = BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
clipBehavior: Clip.antiAlias,
child: Material(
color: Colors.transparent,
child: Column(mainAxisSize: MainAxisSize.min, children: children),
);
final child = Material(
color: Colors.transparent,
child: Column(mainAxisSize: MainAxisSize.min, children: children),
);
// Explicit caller margin wins as-is. Otherwise center on wide surfaces
// using the incoming constraint (not screen width) so groups nested in an
// already clamped box, e.g. a bottom sheet, are not over-inset.
if (margin != null) {
return Container(
margin: margin,
decoration: decoration,
clipBehavior: Clip.antiAlias,
child: child,
);
}
return LayoutBuilder(
builder: (context, constraints) => Container(
margin: EdgeInsets.symmetric(
horizontal:
16 +
(constraints.hasBoundedWidth
? wideInsetForWidth(constraints.maxWidth)
: 0),
vertical: 4,
),
decoration: decoration,
clipBehavior: Clip.antiAlias,
child: child,
),
);
}