From 736cfcd08156fcd669e5f6027eda69d5c6f083a0 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:03:27 +0700 Subject: [PATCH] feat(settings): connect custom Material settings rows --- lib/screens/repo_tab.dart | 1 + lib/screens/settings/about_page.dart | 9 ++- lib/screens/settings/app_settings_page.dart | 16 +++--- .../settings/appearance_settings_page.dart | 4 ++ .../settings/download_settings_page.dart | 3 + .../settings/extension_detail_page.dart | 26 ++++++--- lib/screens/settings/extensions_page.dart | 2 + .../settings/library_settings_page.dart | 2 + .../settings/lyrics_settings_page.dart | 1 + .../settings/playback_settings_page.dart | 1 + lib/widgets/audio_analysis_widget.dart | 1 + lib/widgets/automix_settings.dart | 1 + lib/widgets/extension_row.dart | 3 +- lib/widgets/settings_group.dart | 55 +++++++++++++------ 14 files changed, 90 insertions(+), 35 deletions(-) diff --git a/lib/screens/repo_tab.dart b/lib/screens/repo_tab.dart index 16f8da32..088af085 100644 --- a/lib/screens/repo_tab.dart +++ b/lib/screens/repo_tab.dart @@ -267,6 +267,7 @@ class _RepoTabState extends ConsumerState { else SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: filteredExtensions.asMap().entries.map((entry) { final index = entry.key; final ext = entry.value; diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 69448dfb..a8830848 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -63,6 +63,7 @@ class AboutPage extends StatelessWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _ContributorItem( name: AppInfo.mobileAuthor, @@ -107,6 +108,7 @@ class AboutPage extends StatelessWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _ContributorItem( name: 'binimum', @@ -167,6 +169,7 @@ class AboutPage extends StatelessWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _AboutSettingsItem( icon: Icons.phone_android, @@ -212,6 +215,7 @@ class AboutPage extends StatelessWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _AboutSettingsItem( icon: Icons.telegram, @@ -236,6 +240,7 @@ class AboutPage extends StatelessWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _AboutSettingsItem( icon: Icons.info_outline, @@ -458,7 +463,7 @@ class _ContributorItem extends StatelessWidget { ), ), ), - if (showDivider) + if (showDivider && !settingsRowsConnected(context)) Divider( height: 1, thickness: 1, @@ -734,7 +739,7 @@ class _AboutSettingsItem extends StatelessWidget { ), ), ), - if (showDivider) + if (showDivider && !settingsRowsConnected(context)) Divider( height: 1, thickness: 1, diff --git a/lib/screens/settings/app_settings_page.dart b/lib/screens/settings/app_settings_page.dart index b0783afe..48601b03 100644 --- a/lib/screens/settings/app_settings_page.dart +++ b/lib/screens/settings/app_settings_page.dart @@ -32,6 +32,7 @@ class AppSettingsPage extends ConsumerWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ SettingsSwitchItem( icon: Icons.extension, @@ -442,13 +443,14 @@ class _VerificationBrowserModeSelector extends StatelessWidget { ], ), ), - Divider( - height: 1, - thickness: 1, - indent: 56, - endIndent: 20, - color: colorScheme.outlineVariant.withValues(alpha: 0.3), - ), + if (!settingsRowsConnected(context)) + Divider( + height: 1, + thickness: 1, + indent: 56, + endIndent: 20, + color: colorScheme.outlineVariant.withValues(alpha: 0.3), + ), ], ); return SettingsSearchTarget( diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 1c4acf59..e758a523 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -168,6 +168,7 @@ class AppearanceSettingsPage extends ConsumerWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _ThemeModeSelector( currentMode: themeSettings.themeMode, @@ -191,6 +192,7 @@ class AppearanceSettingsPage extends ConsumerWidget { SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ SettingsSwitchItem( icon: Icons.font_download_outlined, @@ -230,6 +232,7 @@ class AppearanceSettingsPage extends ConsumerWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _LanguageSelector( currentLocale: settings.locale, @@ -245,6 +248,7 @@ class AppearanceSettingsPage extends ConsumerWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _HistoryViewSelector( currentMode: settings.historyViewMode, diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index a9a233ef..b712aa74 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -58,6 +58,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _ServiceSelector( currentService: settings.defaultService, @@ -76,6 +77,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ SettingsSwitchItem( icon: Icons.tune, @@ -241,6 +243,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ const _MetadataSourceSelector(), const _DefaultSearchTabSelector(), diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index da585041..749f9547 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -261,6 +261,7 @@ class _ExtensionDetailPageState extends ConsumerState { SliverToBoxAdapter( child: SettingsGroup( glass: true, + connected: true, children: extension.settings.asMap().entries.map((entry) { final index = entry.key; final setting = entry.value; @@ -288,6 +289,7 @@ class _ExtensionDetailPageState extends ConsumerState { SliverToBoxAdapter( child: SettingsGroup( glass: true, + connected: true, children: [ _HealthSummaryItem( status: healthStatus, @@ -335,6 +337,7 @@ class _ExtensionDetailPageState extends ConsumerState { SliverToBoxAdapter( child: SettingsGroup( glass: true, + connected: true, children: extension.qualityOptions.asMap().entries.map(( entry, ) { @@ -359,6 +362,7 @@ class _ExtensionDetailPageState extends ConsumerState { SliverToBoxAdapter( child: SettingsGroup( glass: true, + connected: true, children: extension.postProcessing!.hooks.asMap().entries.map( (entry) { final index = entry.key; @@ -383,6 +387,7 @@ class _ExtensionDetailPageState extends ConsumerState { SliverToBoxAdapter( child: SettingsGroup( glass: true, + connected: true, children: extension.permissions.asMap().entries.map((entry) { final index = entry.key; final permission = entry.value; @@ -403,6 +408,7 @@ class _ExtensionDetailPageState extends ConsumerState { SliverToBoxAdapter( child: SettingsGroup( glass: true, + connected: true, children: [ _CapabilityItem( icon: Icons.search, @@ -601,14 +607,18 @@ class _ExtensionDivider extends StatelessWidget { final double indent; @override - Widget build(BuildContext context) => Divider( - height: 1, - indent: indent, - endIndent: 16, - color: context.isMornye - ? MornyeTheme.metadataDividerColor(context) - : Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.3), - ); + Widget build(BuildContext context) => settingsRowsConnected(context) + ? const SizedBox.shrink() + : Divider( + height: 1, + indent: indent, + endIndent: 16, + color: context.isMornye + ? MornyeTheme.metadataDividerColor(context) + : Theme.of( + context, + ).colorScheme.outlineVariant.withValues(alpha: 0.3), + ); } /// Long OAuth URLs: selectable text so users can copy without relying on snackbars. diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index f129bd8b..c0d2846f 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -117,6 +117,7 @@ class _ExtensionsPageState extends ConsumerState { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ _DownloadPriorityItem(), _DownloadFallbackItem(), @@ -174,6 +175,7 @@ class _ExtensionsPageState extends ConsumerState { if (extState.extensions.isNotEmpty) SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: extState.extensions.asMap().entries.map((entry) { final index = entry.key; final ext = entry.value; diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index 44ce7055..97690a12 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -676,6 +676,7 @@ class _LibrarySettingsPageState extends ConsumerState { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ SettingsSwitchItem( icon: Icons.library_music_outlined, @@ -854,6 +855,7 @@ class _LibrarySettingsPageState extends ConsumerState { hasDownloads || librarySources.isNotEmpty); return SettingsGroup( + connected: true, children: [ if (libraryState.isScanning) _ScanProgressTile( diff --git a/lib/screens/settings/lyrics_settings_page.dart b/lib/screens/settings/lyrics_settings_page.dart index f8527a02..b923beb0 100644 --- a/lib/screens/settings/lyrics_settings_page.dart +++ b/lib/screens/settings/lyrics_settings_page.dart @@ -35,6 +35,7 @@ class LyricsSettingsPage extends ConsumerWidget { ), SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ const LyricsSizeSetting(), SettingsSwitchItem( diff --git a/lib/screens/settings/playback_settings_page.dart b/lib/screens/settings/playback_settings_page.dart index 4db55933..872242c6 100644 --- a/lib/screens/settings/playback_settings_page.dart +++ b/lib/screens/settings/playback_settings_page.dart @@ -232,6 +232,7 @@ class _PlaybackSettingsPageState extends ConsumerState { if (Platform.isAndroid) SliverToBoxAdapter( child: SettingsGroup( + connected: true, children: [ ValueListenableBuilder( valueListenable: usbAudioStatus, diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index 33242c29..15834125 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -17,6 +17,7 @@ import 'package:spotiflac_android/services/audio_analysis_cache.dart'; import 'package:spotiflac_android/services/network_audio_analysis_input.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/app_content_card.dart'; +import 'package:spotiflac_android/widgets/app_progress_indicator.dart'; import 'package:spotiflac_android/widgets/app_choice_chip.dart'; import 'package:spotiflac_android/widgets/app_icon_button.dart'; import 'package:spotiflac_android/widgets/app_loading_indicator.dart'; diff --git a/lib/widgets/automix_settings.dart b/lib/widgets/automix_settings.dart index 9554370a..b759f4f5 100644 --- a/lib/widgets/automix_settings.dart +++ b/lib/widgets/automix_settings.dart @@ -110,6 +110,7 @@ class _AutoMixSettingsState extends ConsumerState { } return SettingsGroup( + connected: true, children: [ SettingsSwitchItem( icon: Icons.timer_outlined, diff --git a/lib/widgets/extension_row.dart b/lib/widgets/extension_row.dart index c21685b4..6e5f97b2 100644 --- a/lib/widgets/extension_row.dart +++ b/lib/widgets/extension_row.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:spotiflac_android/theme/app_tokens.dart'; import 'package:spotiflac_android/widgets/app_loading_indicator.dart'; +import 'package:spotiflac_android/widgets/settings_group.dart'; /// Square extension icon with a tinted fallback. class ExtensionAvatar extends StatelessWidget { @@ -145,7 +146,7 @@ class ExtensionRow extends StatelessWidget { ), ), ), - if (showDivider) + if (showDivider && !settingsRowsConnected(context)) Divider( height: 1, thickness: 1, diff --git a/lib/widgets/settings_group.dart b/lib/widgets/settings_group.dart index 55daf646..b5cf6a24 100644 --- a/lib/widgets/settings_group.dart +++ b/lib/widgets/settings_group.dart @@ -194,14 +194,25 @@ class _SettingsSearchTargetState extends State { } /// Whether rows sit in an expressive connected [SettingsGroup], where the gaps -/// between row surfaces replace dividers. -bool _rowsConnected(BuildContext context) => +/// between row surfaces replace dividers. Custom rows check this to hide +/// their own dividers. +bool settingsRowsConnected(BuildContext context) => context.dependOnInheritedWidgetOfExactType<_ConnectedRowsScope>() != null; -BorderRadius _connectedRowRadius({ - required Radius top, - required Radius bottom, -}) => BorderRadius.vertical(top: top, bottom: bottom); +/// Corners of one row in an expressive connected list: the card radius at the +/// [first] and [last] ends of the run, tight corners between rows. +BorderRadius connectedRowRadius( + BuildContext context, { + required bool first, + required bool last, +}) { + final outer = Radius.circular(context.tokens.radiusCard); + const inner = Radius.circular(4); + return BorderRadius.vertical( + top: first ? outer : inner, + bottom: last ? outer : inner, + ); +} class _ConnectedRowsScope extends InheritedWidget { const _ConnectedRowsScope({required super.child}); @@ -215,11 +226,17 @@ class SettingsGroup extends StatelessWidget { final EdgeInsetsGeometry? margin; final bool glass; + /// Connects custom [children] that are each one complete row, such as + /// extension rows. Groups of [SettingsItem], [SettingsSwitchItem] and [ListTile] + /// connect without it. + final bool connected; + const SettingsGroup({ super.key, required this.children, this.margin, this.glass = false, + this.connected = false, }); @override @@ -266,15 +283,18 @@ class SettingsGroup extends StatelessWidget { // custom content keep one card, so spacers never become empty tiles. final standardRows = children.isNotEmpty && - children.every( - (child) => child is SettingsItem || child is SettingsSwitchItem, - ); - if (expressive && standardRows) { + (connected || + children.every( + (child) => + child is SettingsItem || + child is SettingsSwitchItem || + child is ListTile, + )); + // Connected surfaces are static layout, so reduced motion keeps the gaps. + if (!context.isMornye && standardRows) { // Expressive connected list: every row is its own surface separated by // a 2dp gap. The group's ends keep the card radius, inner corners stay // tight, and the gaps replace row dividers. - final outer = Radius.circular(context.tokens.radiusCard); - const inner = Radius.circular(4); final last = children.length - 1; return LayoutBuilder( builder: (context, constraints) => Padding( @@ -298,9 +318,10 @@ class SettingsGroup extends StatelessWidget { color: cardColor, clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( - borderRadius: _connectedRowRadius( - top: index == 0 ? outer : inner, - bottom: index == last ? outer : inner, + borderRadius: connectedRowRadius( + context, + first: index == 0, + last: index == last, ), ), child: row, @@ -486,7 +507,7 @@ class SettingsItem extends StatelessWidget { ), ), ), - if (showDivider && !_rowsConnected(context)) + if (showDivider && !settingsRowsConnected(context)) Divider( height: 1, indent: showIcon @@ -607,7 +628,7 @@ class SettingsSwitchItem extends StatelessWidget { ), ), ), - if (showDivider && !_rowsConnected(context)) + if (showDivider && !settingsRowsConnected(context)) Divider( height: 1, indent: showIcon