mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
feat(settings): connect custom Material settings rows
This commit is contained in:
1 parent
ef600973e8
commit
736cfcd081
14 files changed
+90
-35
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -110,6 +110,7 @@ class _AutoMixSettingsState extends ConsumerState<AutoMixSettings> {
|
||||
}
|
||||
|
||||
return SettingsGroup(
|
||||
connected: true,
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.timer_outlined,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user