[imp]: Added tabs for multi-repo versions & UI tweaks

This commit is contained in:
nahnah
2026-03-16 17:33:02 +00:00
parent 4d9bf760aa
commit e03f1fa23c
4 changed files with 502 additions and 314 deletions
+26
View File
@@ -443,6 +443,32 @@ class AppProvider extends ChangeNotifier {
}
}
/// Fetches a specific app from a specific repository URL.
/// Returns null if the app is not found in that repository.
Future<FDroidApp?> fetchAppFromRepository(
String packageName,
String repositoryUrl,
) async {
try {
final results = await _apiService.searchAppsFromRepositoryUrl(
packageName,
repositoryUrl,
);
for (final app in results) {
if (app.packageName == packageName) {
return app.copyWith(repositoryUrl: repositoryUrl);
}
}
return null;
} catch (e) {
debugPrint(
'Error fetching app $packageName from repository $repositoryUrl: $e',
);
return null;
}
}
/// Fetches latest apps from F-Droid and custom repositories
Future<void> fetchLatestApps({
RepositoriesProvider? repositoriesProvider,
+453 -307
View File
@@ -6,10 +6,12 @@ import 'package:florid/screens/app_details/developer_apps_screen.dart';
import 'package:florid/screens/app_details/permissions_screen.dart';
import 'package:florid/screens/home/category_apps_screen.dart';
import 'package:florid/widgets/changelog_preview.dart';
import 'package:florid/widgets/f_tabbar.dart';
import 'package:florid/widgets/list_icon.dart';
import 'package:florid/widgets/m_list.dart';
import 'package:florid/widgets/markup_content.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -18,7 +20,6 @@ import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../constants.dart';
import '../../models/fdroid_app.dart';
import '../../providers/app_provider.dart';
import '../../providers/download_provider.dart';
@@ -1197,6 +1198,10 @@ class _DetailsSheetsSection extends StatelessWidget {
children: [
MListHeader(
title: AppLocalizations.of(context)!.version_information,
trailing: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Symbols.keyboard_arrow_down),
),
),
MListView(
items: [
@@ -2191,50 +2196,15 @@ class _AppInfoSection extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
if (app.antiFeatures != null)
Column(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
MListHeader(title: 'Anti-features'),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: app.antiFeatures?.isNotEmpty == true
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: app.antiFeatures!
.map(
(antiFeature) => Text(
'- $antiFeature',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.error,
),
),
)
.toList(),
)
: Text(
AppLocalizations.of(
context,
)!.no_antifeature_listed,
),
),
),
],
),
Column(
spacing: 4,
children: [
MListHeader(
title: AppLocalizations.of(context)!.app_information,
trailing: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Symbols.keyboard_arrow_down),
),
),
MListView(
items: [
@@ -2286,6 +2256,45 @@ class _AppInfoSection extends StatelessWidget {
),
],
),
if (app.antiFeatures != null)
Column(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
MListHeader(title: 'Anti-features'),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: app.antiFeatures?.isNotEmpty == true
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: app.antiFeatures!
.map(
(antiFeature) => Text(
'- $antiFeature',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.error,
),
),
)
.toList(),
)
: Text(
AppLocalizations.of(
context,
)!.no_antifeature_listed,
),
),
),
],
),
],
);
},
@@ -2501,122 +2510,6 @@ class _IncludeUnstableSectionState extends State<IncludeUnstableSection> {
}
}
class _VersionInfoSection extends StatefulWidget {
final FDroidVersion version;
const _VersionInfoSection({required this.version});
@override
State<_VersionInfoSection> createState() => _VersionInfoSectionState();
}
class _VersionInfoSectionState extends State<_VersionInfoSection> {
bool _showMinAndroid = true;
bool _showTargetAndroid = true;
String _androidVersionName(int sdk) {
return kAndroidSdkVersions[sdk] ?? 'Android (SDK $sdk)';
}
String _androidVersionLabel(String sdkValue) {
final sdk = int.tryParse(sdkValue);
if (sdk == null) {
return 'Android (SDK $sdkValue)';
}
return _androidVersionName(sdk);
}
@override
Widget build(BuildContext context) {
final version = widget.version;
buildVersionInfoModel() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MListHeader(title: 'Version Information'),
MListView(
items: [
MListItemData(
title: 'Version Name',
subtitle: version.versionName,
onTap: () {},
),
MListItemData(
title: 'Version Code',
subtitle: version.versionCode.toString(),
onTap: () {},
),
MListItemData(
title: 'Size',
subtitle: version.sizeString,
onTap: () {},
),
if (version.minSdkVersion != null)
MListItemData(
title: _showMinAndroid
? 'Minimum Android Version'
: 'Min SDK',
subtitle: _showMinAndroid
? _androidVersionLabel(version.minSdkVersion!)
: version.minSdkVersion!,
onTap: () {
setState(() {
_showMinAndroid = !_showMinAndroid;
});
},
),
if (version.targetSdkVersion != null)
MListItemData(
title: _showTargetAndroid
? 'Target Android Version'
: 'Target SDK',
subtitle: _showTargetAndroid
? _androidVersionLabel(version.targetSdkVersion!)
: version.targetSdkVersion!,
onTap: () {
setState(() {
_showTargetAndroid = !_showTargetAndroid;
});
},
),
],
),
],
),
);
},
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MListView(
items: [
MListItemData(
title: AppLocalizations.of(context)!.version_information,
subtitle: '',
leading: Icon(Symbols.info),
onTap: () {
buildVersionInfoModel();
},
suffix: Icon(Symbols.arrow_forward),
),
],
),
],
);
}
}
class _NoVersionInfoSection extends StatelessWidget {
const _NoVersionInfoSection();
@@ -2842,11 +2735,25 @@ class _AppExtraInfoSectionState extends State<AppExtraInfoSection> {
return;
}
final opened = await launchUrl(
launchUri,
mode: LaunchMode.externalApplication,
);
if (!opened && context.mounted) {
try {
final opened = await launchUrl(
launchUri,
mode: LaunchMode.externalApplication,
);
if (!opened && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unable to open donation link: $link')),
);
}
} on PlatformException {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No app is available to handle this donation link.'),
),
);
} catch (_) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unable to open donation link: $link')),
);
@@ -3014,177 +2921,416 @@ class _AppDetailsIconState extends State<AppDetailsIcon> {
}
}
class _AllVersionsSection extends StatelessWidget {
class _AllVersionsSection extends StatefulWidget {
final FDroidApp app;
const _AllVersionsSection({required this.app});
@override
State<_AllVersionsSection> createState() => _AllVersionsSectionState();
}
class _AllVersionsSectionState extends State<_AllVersionsSection> {
late Future<List<_RepoVersionsTabData>> _repoTabsFuture;
late Future<_AllVersionsInitData> _initialDataFuture;
int _selectedRepoIndex = 0;
bool _userSelectedRepo = false;
@override
void initState() {
super.initState();
_repoTabsFuture = _loadRepoTabs();
_initialDataFuture = _loadInitialData();
}
@override
void didUpdateWidget(covariant _AllVersionsSection oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.app.packageName != widget.app.packageName) {
_userSelectedRepo = false;
_selectedRepoIndex = 0;
_repoTabsFuture = _loadRepoTabs();
_initialDataFuture = _loadInitialData();
}
}
Future<_AllVersionsInitData> _loadInitialData() async {
final appProvider = context.read<AppProvider>();
final includeUnstable = await appProvider.getIncludeUnstable(
widget.app.packageName,
);
final supportedAbis = await appProvider.getSupportedAbis();
final trackedRepoUrl = await context.read<DownloadProvider>().getAppSource(
widget.app.packageName,
);
final tabs = await _repoTabsFuture;
return _AllVersionsInitData(
includeUnstable: includeUnstable,
supportedAbis: supportedAbis,
trackedRepoUrl: trackedRepoUrl,
tabs: tabs,
);
}
Future<List<_RepoVersionsTabData>> _loadRepoTabs() async {
final appProvider = context.read<AppProvider>();
final tabs = <_RepoVersionsTabData>[];
final seenUrls = <String>{};
final enrichedApp = await appProvider.enrichAppWithRepositories(
widget.app,
context.read<RepositoriesProvider>(),
);
void addFallbackCurrentRepo() {
if (seenUrls.add(widget.app.repositoryUrl)) {
tabs.add(
_RepoVersionsTabData(
repo: RepositorySource(
name: 'Current',
url: widget.app.repositoryUrl,
),
app: widget.app.copyWith(repositoryUrl: widget.app.repositoryUrl),
),
);
}
}
final repos =
enrichedApp.availableRepositories ?? const <RepositorySource>[];
if (repos.isEmpty) {
addFallbackCurrentRepo();
return tabs;
}
for (final repo in repos) {
if (!seenUrls.add(repo.url)) continue;
if (repo.url == widget.app.repositoryUrl) {
tabs.add(
_RepoVersionsTabData(
repo: repo,
app: widget.app.copyWith(repositoryUrl: repo.url),
),
);
continue;
}
final repoApp = await appProvider.fetchAppFromRepository(
widget.app.packageName,
repo.url,
);
tabs.add(
_RepoVersionsTabData(
repo: repo,
app: repoApp?.copyWith(
repositoryUrl: repo.url,
availableRepositories: widget.app.availableRepositories,
),
),
);
}
addFallbackCurrentRepo();
return tabs;
}
List<FDroidVersion> _versionsForRepo(
FDroidApp repoApp,
bool includeUnstable,
List<String> supportedAbis,
) {
var versions = repoApp.packages?.values.toList() ?? [];
if (versions.isEmpty) return const <FDroidVersion>[];
if (!includeUnstable) {
versions = versions.where((v) => !v.isUnstable).toList();
if (versions.isEmpty) return const <FDroidVersion>[];
}
bool isUniversal(FDroidVersion v) =>
v.nativecode == null || v.nativecode!.isEmpty;
bool supportsDevice(FDroidVersion v) {
if (isUniversal(v)) return true;
if (supportedAbis.isEmpty) return true;
return v.nativecode!.any((abi) => supportedAbis.contains(abi));
}
final compatible = versions.where(supportsDevice).toList();
if (compatible.isNotEmpty) {
versions = compatible;
}
versions.sort((a, b) => b.versionCode.compareTo(a.versionCode));
return versions;
}
@override
Widget build(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, appProvider, _) {
return FutureBuilder<List<Object?>>(
future: Future.wait([
appProvider.getIncludeUnstable(app.packageName),
appProvider.getSupportedAbis(),
]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox.shrink();
}
final appProvider = context.watch<AppProvider>();
return FutureBuilder<_AllVersionsInitData>(
future: _initialDataFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0,
children: [
MListHeader(
title: 'All Versions',
trailing: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Symbols.keyboard_arrow_down),
),
),
SizedBox(
height: 260,
child: Center(child: CircularProgressIndicator()),
),
],
);
}
final includeUnstable = (snapshot.data?[0] as bool?) ?? false;
final supportedAbis =
(snapshot.data?[1] as List<String>?) ?? const <String>[];
final initialData = snapshot.data;
if (initialData == null) {
return const SizedBox.shrink();
}
var versions = app.packages?.values.toList() ?? [];
if (versions.isEmpty) return const SizedBox.shrink();
final includeUnstable = initialData.includeUnstable;
final supportedAbis = initialData.supportedAbis;
final trackedRepoUrl = initialData.trackedRepoUrl;
final tabs = initialData.tabs;
if (!includeUnstable) {
versions = versions.where((v) => !v.isUnstable).toList();
if (versions.isEmpty) return const SizedBox.shrink();
}
if (tabs.isEmpty) return const SizedBox.shrink();
bool isUniversal(FDroidVersion v) =>
v.nativecode == null || v.nativecode!.isEmpty;
final trackedIndex = trackedRepoUrl == null
? -1
: tabs.indexWhere((t) => t.repo.url == trackedRepoUrl);
final defaultIndex = tabs.indexWhere(
(t) => t.repo.url == widget.app.repositoryUrl,
);
bool supportsDevice(FDroidVersion v) {
if (isUniversal(v)) return true;
if (supportedAbis.isEmpty) return true;
return v.nativecode!.any((abi) => supportedAbis.contains(abi));
}
final preferredIndex = trackedIndex >= 0
? trackedIndex
: (defaultIndex >= 0 ? defaultIndex : 0);
// Filter out incompatible ABIs; if none remain, fall back to show all
final compatible = versions.where(supportsDevice).toList();
if (compatible.isNotEmpty) {
versions = compatible;
}
final selectedIndex = _userSelectedRepo
? _selectedRepoIndex.clamp(0, tabs.length - 1)
: preferredIndex;
versions.sort((a, b) => b.versionCode.compareTo(a.versionCode));
final selectedTab = tabs[selectedIndex];
final selectedRepoApp = selectedTab.app;
final versions = selectedRepoApp == null
? const <FDroidVersion>[]
: _versionsForRepo(selectedRepoApp, includeUnstable, supportedAbis);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0,
children: [
MListHeader(title: 'All Versions'),
MListViewBuilder(
itemCount: versions.length,
itemBuilder: (index) {
final version = versions[index];
final isLatest = version == versions.first;
final compatibleAbi = supportsDevice(version);
final installedApp = appProvider.getInstalledApp(
app.packageName,
);
final isInstalledVersion =
appProvider.isAppInstalled(app.packageName) &&
installedApp != null &&
(installedApp.versionCode != null
? installedApp.versionCode == version.versionCode
: installedApp.versionName == version.versionName);
return MListItemData(
title: version.versionName,
subtitle: version.sizeString,
suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isInstalledVersion)
IconButton.filledTonal(
style: FilledButton.styleFrom(
foregroundColor: Theme.of(
context,
).colorScheme.onErrorContainer,
backgroundColor: Theme.of(
context,
).colorScheme.errorContainer,
),
onPressed: () async {
try {
await context
.read<DownloadProvider>()
.uninstallApp(app.packageName);
await Future.delayed(
const Duration(milliseconds: 100),
);
await appProvider.fetchInstalledApps();
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Uninstall failed: $e'),
),
);
}
}
},
icon: const Icon(Symbols.delete_rounded, fill: 1),
)
else ...[
IconButton(
onPressed: compatibleAbi
? () async {
final url = version.downloadUrl(
app.repositoryUrl,
);
await launchUrl(Uri.parse(url));
}
: null,
icon: const Icon(Symbols.open_in_new_rounded),
bool isUniversal(FDroidVersion v) =>
v.nativecode == null || v.nativecode!.isEmpty;
bool supportsDevice(FDroidVersion v) {
if (isUniversal(v)) return true;
if (supportedAbis.isEmpty) return true;
return v.nativecode!.any((abi) => supportedAbis.contains(abi));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0,
children: [
MListHeader(
title: 'All Versions',
trailing: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Symbols.keyboard_arrow_down),
),
),
DefaultTabController(
key: ValueKey('repo-tabs-${tabs.length}-$selectedIndex'),
length: tabs.length,
initialIndex: selectedIndex,
child: Builder(
builder: (context) {
return FTabBar(
controller: DefaultTabController.of(context),
isScrollable: true,
onTabChanged: (index) {
setState(() {
_selectedRepoIndex = index;
_userSelectedRepo = true;
});
},
items: [
for (final tab in tabs)
FloridTabBarItem(
icon: Symbols.storage_rounded,
label: trackedRepoUrl == tab.repo.url
? '${tab.repo.name} (Installed)'
: tab.repo.name,
),
],
);
},
),
),
if (selectedRepoApp == null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Card.outlined(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
'No version metadata available from ${selectedTab.repo.name}.',
),
),
),
)
else if (versions.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Card.outlined(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
'No compatible versions available in ${selectedTab.repo.name}.',
),
),
),
)
else
MListViewBuilder(
itemCount: versions.length,
itemBuilder: (index) {
final version = versions[index];
final isLatest = version == versions.first;
final compatibleAbi = supportsDevice(version);
final installedApp = appProvider.getInstalledApp(
widget.app.packageName,
);
final isInstalledVersion =
appProvider.isAppInstalled(widget.app.packageName) &&
installedApp != null &&
(installedApp.versionCode != null
? installedApp.versionCode == version.versionCode
: installedApp.versionName == version.versionName);
return MListItemData(
title: version.versionName,
subtitle: version.sizeString,
suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isInstalledVersion)
IconButton.filledTonal(
style: FilledButton.styleFrom(
foregroundColor: Theme.of(
context,
).colorScheme.onErrorContainer,
backgroundColor: Theme.of(
context,
).colorScheme.errorContainer,
),
IconButton.filledTonal(
onPressed: compatibleAbi
? () async {
try {
final appWithVersion = app
.copyWithVersion(version);
await context
.read<DownloadProvider>()
.downloadApk(appWithVersion);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'Download failed: $e',
),
),
onPressed: () async {
try {
await context
.read<DownloadProvider>()
.uninstallApp(widget.app.packageName);
await Future.delayed(
const Duration(milliseconds: 100),
);
await appProvider.fetchInstalledApps();
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Uninstall failed: $e'),
),
);
}
}
},
icon: const Icon(Symbols.delete_rounded, fill: 1),
)
else ...[
IconButton(
onPressed: compatibleAbi
? () async {
final url = version.downloadUrl(
selectedTab.repo.url,
);
await launchUrl(Uri.parse(url));
}
: null,
icon: const Icon(Symbols.open_in_new_rounded),
),
IconButton.filledTonal(
onPressed: compatibleAbi
? () async {
try {
final appWithVersion = selectedRepoApp
.copyWithVersion(version)
.copyWith(
repositoryUrl: selectedTab.repo.url,
);
}
await context
.read<DownloadProvider>()
.downloadApk(appWithVersion);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'Download failed: $e',
),
),
);
}
}
: null,
icon: const Icon(Symbols.download),
),
],
],
),
selected: isLatest,
onTap: () {
// Scroll to the tapped version's section
// This requires a more complex setup with GlobalKeys or a scroll controller
// For simplicity, we'll just show a snackbar here
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Tapped on version ${version.versionName}',
),
}
: null,
icon: const Icon(Symbols.download),
),
);
},
);
},
),
],
);
},
],
],
),
selected: isLatest,
onTap: () {},
);
},
).animate().fadeIn(
delay: Duration(milliseconds: 300),
duration: Duration(milliseconds: 300),
),
],
);
},
);
}
}
class _RepoVersionsTabData {
final RepositorySource repo;
final FDroidApp? app;
const _RepoVersionsTabData({required this.repo, required this.app});
}
class _AllVersionsInitData {
final bool includeUnstable;
final List<String> supportedAbis;
final String? trackedRepoUrl;
final List<_RepoVersionsTabData> tabs;
const _AllVersionsInitData({
required this.includeUnstable,
required this.supportedAbis,
required this.trackedRepoUrl,
required this.tabs,
});
}
enum _ShizukuAction { switchToSystem, cancel }
Future<void> _handleShizukuUnavailable(
+12
View File
@@ -806,6 +806,12 @@ class FDroidApiService {
'Repository $repositoryUrl not in database, fetching from network...',
);
final repo = await fetchRepositoryFromUrl(repositoryUrl);
final repositoryId = await getRepositoryIdByUrl(repositoryUrl);
if (repositoryId != null) {
await importRepositoryToDatabase(repo, repositoryId: repositoryId);
}
return repo.searchApps(query);
} catch (e) {
throw Exception('Error searching apps from repository: $e');
@@ -833,6 +839,12 @@ class FDroidApiService {
}
final repo = await fetchRepositoryFromUrl(repositoryUrl);
final repositoryId = await getRepositoryIdByUrl(repositoryUrl);
if (repositoryId != null) {
await importRepositoryToDatabase(repo, repositoryId: repositoryId);
}
return repo.apps.containsKey(packageName);
} catch (e) {
debugPrint('Error checking repository package existence: $e');
+11 -7
View File
@@ -209,19 +209,23 @@ class MListViewBuilder extends StatelessWidget {
return ClipRRect(
borderRadius: BorderRadius.only(
topLeft: index == 0 ? Radius.circular(16.0) : Radius.circular(4.0),
topRight: index == 0 ? Radius.circular(16.0) : Radius.circular(4.0),
topLeft: index == 0
? Radius.circular(isFlorid ? 16.0 : 0)
: Radius.circular(isFlorid ? 4.0 : 0),
topRight: index == 0
? Radius.circular(isFlorid ? 16.0 : 0)
: Radius.circular(isFlorid ? 4.0 : 0),
bottomLeft: isLastItem
? const Radius.circular(16.0)
: const Radius.circular(4.0),
? Radius.circular(isFlorid ? 16.0 : 0)
: Radius.circular(isFlorid ? 4.0 : 0),
bottomRight: isLastItem
? const Radius.circular(16.0)
: const Radius.circular(4.0),
? Radius.circular(isFlorid ? 16.0 : 0)
: Radius.circular(isFlorid ? 4.0 : 0),
),
child: Material(
color: isFlorid
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.surface,
: Colors.transparent,
clipBehavior: Clip.antiAlias,
child: ListTile(
contentPadding: EdgeInsets.only(left: 16.0, right: 16.0),