feat(settings): connect custom Material settings rows

This commit is contained in:
zarzet committed 2026-10-14 23:03:27 +07:00
1 parent ef600973e8
commit 736cfcd081
14 files changed
+90 -35

No files matched your search

+1
View File
@@ -267,6 +267,7 @@ class _RepoTabState extends ConsumerState<RepoTab> {
else
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: filteredExtensions.asMap().entries.map((entry) {
final index = entry.key;
final ext = entry.value;
+7 -2
View File
@@ -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,
+9 -7
View File
@@ -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(
@@ -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,
@@ -58,6 +58,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
_ServiceSelector(
currentService: settings.defaultService,
@@ -76,6 +77,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
SettingsSwitchItem(
icon: Icons.tune,
@@ -241,6 +243,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
const _MetadataSourceSelector(),
const _DefaultSearchTabSelector(),
@@ -261,6 +261,7 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
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<ExtensionDetailPage> {
SliverToBoxAdapter(
child: SettingsGroup(
glass: true,
connected: true,
children: [
_HealthSummaryItem(
status: healthStatus,
@@ -335,6 +337,7 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
SliverToBoxAdapter(
child: SettingsGroup(
glass: true,
connected: true,
children: extension.qualityOptions.asMap().entries.map((
entry,
) {
@@ -359,6 +362,7 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
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<ExtensionDetailPage> {
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<ExtensionDetailPage> {
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.
@@ -117,6 +117,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
),
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
_DownloadPriorityItem(),
_DownloadFallbackItem(),
@@ -174,6 +175,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
if (extState.extensions.isNotEmpty)
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: extState.extensions.asMap().entries.map((entry) {
final index = entry.key;
final ext = entry.value;
@@ -676,6 +676,7 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
),
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
SettingsSwitchItem(
icon: Icons.library_music_outlined,
@@ -854,6 +855,7 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
hasDownloads ||
librarySources.isNotEmpty);
return SettingsGroup(
connected: true,
children: [
if (libraryState.isScanning)
_ScanProgressTile(
@@ -35,6 +35,7 @@ class LyricsSettingsPage extends ConsumerWidget {
),
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
const LyricsSizeSetting(),
SettingsSwitchItem(
@@ -232,6 +232,7 @@ class _PlaybackSettingsPageState extends ConsumerState<PlaybackSettingsPage> {
if (Platform.isAndroid)
SliverToBoxAdapter(
child: SettingsGroup(
connected: true,
children: [
ValueListenableBuilder<UsbAudioStatus>(
valueListenable: usbAudioStatus,
+1
View File
@@ -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';
+1
View File
@@ -110,6 +110,7 @@ class _AutoMixSettingsState extends ConsumerState<AutoMixSettings> {
}
return SettingsGroup(
connected: true,
children: [
SettingsSwitchItem(
icon: Icons.timer_outlined,
+2 -1
View File
@@ -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,
+38 -17
View File
@@ -194,14 +194,25 @@ class _SettingsSearchTargetState extends State<SettingsSearchTarget> {
}
/// 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