From 1f6b9773ddb5e674da4adc164aeb3f5092d9c8bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:34:18 +0000 Subject: [PATCH 01/12] Initial plan From ae94a3b85d655be5d2b9256ae0adfd51ee32e3dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:43:24 +0000 Subject: [PATCH 02/12] Add unstable version opt-in and repository tracking - Added isUnstable property to FDroidVersion to detect beta/alpha/RC versions - Added getLatestVersion method to FDroidApp to filter unstable versions - Added includeUnstableVersions setting in SettingsProvider - Updated AppProvider to use settings when determining latest version - Updated main.dart to inject SettingsProvider into AppProvider - Updated app_details_screen to use AppProvider.getLatestVersion - Added settings UI toggle for unstable versions - Created InstallationTrackingService to track app download sources - Updated DownloadProvider to track repository URLs and clean up on uninstall Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/main.dart | 12 +- lib/models/fdroid_app.dart | 33 ++- lib/providers/app_provider.dart | 24 ++- lib/providers/download_provider.dart | 15 ++ lib/providers/settings_provider.dart | 11 + lib/screens/app_details_screen.dart | 191 ++++++++++-------- lib/screens/settings_screen.dart | 16 ++ .../installation_tracking_service.dart | 41 ++++ 8 files changed, 257 insertions(+), 86 deletions(-) create mode 100644 lib/services/installation_tracking_service.dart diff --git a/lib/main.dart b/lib/main.dart index 5afe8b4..5d32ed5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -55,12 +55,18 @@ class MainApp extends StatelessWidget { return service; }, ), - ChangeNotifierProxyProvider( + ChangeNotifierProxyProvider2( create: (context) => AppProvider( Provider.of(context, listen: false), + Provider.of(context, listen: false), ), - update: (context, apiService, previous) => - previous ?? AppProvider(apiService), + update: (context, apiService, settings, previous) { + if (previous == null) { + return AppProvider(apiService, settings); + } + previous.updateSettings(settings); + return previous; + }, ), ChangeNotifierProxyProvider2< FDroidApiService, diff --git a/lib/models/fdroid_app.dart b/lib/models/fdroid_app.dart index 1db40bb..8acee18 100644 --- a/lib/models/fdroid_app.dart +++ b/lib/models/fdroid_app.dart @@ -131,8 +131,23 @@ class FDroidApp { String get categoryString => categories?.join(', ') ?? 'Unknown'; FDroidVersion? get latestVersion { + return getLatestVersion(); + } + + /// Gets the latest version, optionally filtering out unstable versions + FDroidVersion? getLatestVersion({bool includeUnstable = true}) { if (packages == null || packages!.isEmpty) return null; - final versions = packages!.values.toList(); + + var versions = packages!.values.toList(); + + // Filter out unstable versions if requested + if (!includeUnstable) { + versions = versions.where((v) => !v.isUnstable).toList(); + + // If no stable versions exist, return null + if (versions.isEmpty) return null; + } + versions.sort((a, b) => b.versionCode.compareTo(a.versionCode)); return versions.first; } @@ -271,6 +286,22 @@ class FDroidVersion { if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)}KB'; return '${(size / (1024 * 1024)).toStringAsFixed(1)}MB'; } + + /// Checks if this version is considered unstable (beta, alpha, RC, etc.) + bool get isUnstable { + final lowerVersionName = versionName.toLowerCase(); + // Check for common unstable version indicators + return lowerVersionName.contains('alpha') || + lowerVersionName.contains('beta') || + lowerVersionName.contains('rc') || + lowerVersionName.contains('dev') || + lowerVersionName.contains('pre') || + lowerVersionName.contains('snapshot') || + lowerVersionName.contains('nightly') || + lowerVersionName.contains('canary') || + lowerVersionName.contains('preview') || + lowerVersionName.contains('test'); + } } @JsonSerializable() diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index eabcc69..8fff8fa 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -5,6 +5,7 @@ import 'package:installed_apps/installed_apps.dart'; import '../models/fdroid_app.dart'; import '../services/fdroid_api_service.dart'; import 'repositories_provider.dart'; +import 'settings_provider.dart'; enum LoadingState { idle, loading, success, error } @@ -25,8 +26,14 @@ class AppInfo { class AppProvider extends ChangeNotifier { final FDroidApiService _apiService; + SettingsProvider? _settingsProvider; - AppProvider(this._apiService); + AppProvider(this._apiService, [this._settingsProvider]); + + void updateSettings(SettingsProvider settings) { + _settingsProvider = settings; + notifyListeners(); + } // Latest apps state List _latestApps = []; @@ -555,22 +562,27 @@ class AppProvider extends ChangeNotifier { } final updatableApps = []; + final includeUnstable = _settingsProvider?.includeUnstableVersions ?? false; for (final installedApp in _installedApps) { // Check if the app exists in F-Droid repository final fdroidApp = _repository!.apps[installedApp.packageName]; if (fdroidApp == null) continue; + // Get the latest version based on user's unstable preference + final latestVersion = fdroidApp.getLatestVersion(includeUnstable: includeUnstable); + // Check if F-Droid app has a latest version - if (fdroidApp.latestVersion == null) continue; + if (latestVersion == null) continue; // Check if installed app has version info if (installedApp.versionCode == null) continue; // Compare version codes - if F-Droid has a newer version, it's updatable - if (fdroidApp.latestVersion!.versionCode > installedApp.versionCode!) { + if (latestVersion.versionCode > installedApp.versionCode!) { updatableApps.add(fdroidApp); } + } } // Sort by app name for consistent ordering @@ -593,6 +605,12 @@ class AppProvider extends ChangeNotifier { } } + /// Gets the latest version for an app based on user's unstable preference + FDroidVersion? getLatestVersion(FDroidApp app) { + final includeUnstable = _settingsProvider?.includeUnstableVersions ?? false; + return app.getLatestVersion(includeUnstable: includeUnstable); + } + /// Attempts to launch an installed app by package name Future openInstalledApp(String packageName) async { try { diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index dcbf322..0d94c01 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -8,6 +8,7 @@ import 'package:permission_handler/permission_handler.dart'; import '../models/fdroid_app.dart'; import '../providers/settings_provider.dart'; import '../services/fdroid_api_service.dart'; +import '../services/installation_tracking_service.dart'; import '../services/notification_service.dart'; enum DownloadStatus { idle, downloading, completed, error, cancelled } @@ -77,6 +78,7 @@ class DownloadProvider extends ChangeNotifier { SettingsProvider _settingsProvider; final Map _downloads = {}; final NotificationService _notificationService = NotificationService(); + final InstallationTrackingService _trackingService = InstallationTrackingService(); DownloadProvider(this._apiService, this._settingsProvider) { _initNotifications(); @@ -273,6 +275,9 @@ class DownloadProvider extends ChangeNotifier { ); notifyListeners(); + // Track which repository this app was downloaded from + await _trackingService.setAppSource(app.packageName, app.repositoryUrl); + // Show completion notification await _notificationService.showDownloadComplete( title: app.name, @@ -424,8 +429,18 @@ class DownloadProvider extends ChangeNotifier { data: 'package:$packageName', ); await intent.launch(); + + // Remove tracking data when app is uninstalled + // Note: This is best-effort. The actual uninstall is handled by Android + // and we can't know for sure if it succeeded immediately + await _trackingService.removeAppSource(packageName); } catch (e) { throw Exception('Failed to uninstall app: $e'); } } + + /// Gets the repository URL that an app was downloaded from + Future getAppSource(String packageName) async { + return await _trackingService.getAppSource(packageName); + } } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 67518a0..2cd0b5a 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -12,6 +12,7 @@ class SettingsProvider extends ChangeNotifier { static const _localeKey = 'locale'; static const _onboardingCompleteKey = 'onboarding_complete'; static const _sniBypassKey = 'sni_bypass_enabled'; + static const _includeUnstableKey = 'include_unstable_versions'; ThemeMode _themeMode = ThemeMode.system; ThemeStyle _themeStyle = ThemeStyle.florid; @@ -20,6 +21,7 @@ class SettingsProvider extends ChangeNotifier { String _locale = 'en-US'; bool _onboardingComplete = false; bool _sniBypassEnabled = true; + bool _includeUnstableVersions = false; bool _loaded = false; SettingsProvider() { @@ -34,6 +36,7 @@ class SettingsProvider extends ChangeNotifier { String get locale => _locale; bool get onboardingComplete => _onboardingComplete; bool get sniBypassEnabled => _sniBypassEnabled; + bool get includeUnstableVersions => _includeUnstableVersions; /// Available locales for F-Droid repository data static const List availableLocales = [ @@ -98,6 +101,7 @@ class SettingsProvider extends ChangeNotifier { _locale = prefs.getString(_localeKey) ?? 'en-US'; _onboardingComplete = prefs.getBool(_onboardingCompleteKey) ?? false; _sniBypassEnabled = prefs.getBool(_sniBypassKey) ?? true; + _includeUnstableVersions = prefs.getBool(_includeUnstableKey) ?? false; _loaded = true; notifyListeners(); } @@ -166,4 +170,11 @@ class SettingsProvider extends ChangeNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_sniBypassKey, value); } + + Future setIncludeUnstableVersions(bool value) async { + _includeUnstableVersions = value; + notifyListeners(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_includeUnstableKey, value); + } } diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 68b0b65..2dc9a56 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -16,6 +16,7 @@ import '../models/fdroid_app.dart'; import '../providers/app_provider.dart'; import '../providers/download_provider.dart'; import '../providers/repositories_provider.dart'; +import '../providers/settings_provider.dart'; import '../services/izzy_stats_service.dart'; class AppDetailsScreen extends StatefulWidget { @@ -230,14 +231,17 @@ class _AppDetailsScreenState extends State { await Future.delayed(const Duration(milliseconds: 800)); await appProvider.fetchInstalledApps(); if (appProvider.isAppInstalled(widget.app.packageName)) { - final downloadInfo = downloadProvider.getDownloadInfo( - widget.app.packageName, - widget.app.latestVersion!.versionName, - ); - if (downloadInfo?.filePath != null) { - await downloadProvider.deleteDownloadedFile( - downloadInfo!.filePath!, + final latestVersion = appProvider.getLatestVersion(widget.app); + if (latestVersion != null) { + final downloadInfo = downloadProvider.getDownloadInfo( + widget.app.packageName, + latestVersion.versionName, ); + if (downloadInfo?.filePath != null) { + await downloadProvider.deleteDownloadedFile( + downloadInfo!.filePath!, + ); + } } break; } @@ -506,7 +510,11 @@ class _AppDetailsScreenState extends State { children: [ Consumer2( builder: (context, downloadProvider, appProvider, child) { - final version = widget.app.latestVersion!; + final version = appProvider.getLatestVersion(widget.app); + if (version == null) { + return const SizedBox.shrink(); + } + final isInstalled = appProvider.isAppInstalled( widget.app.packageName, ); @@ -591,7 +599,7 @@ class _AppDetailsScreenState extends State { // Check if update is available final hasUpdate = installedApp.versionCode != null && - widget.app.latestVersion!.versionCode > + version.versionCode > installedApp.versionCode!; if (hasUpdate) { @@ -923,14 +931,21 @@ class _AppDetailsScreenState extends State { delay: Duration(milliseconds: 300), duration: Duration(milliseconds: 300), ), - if (widget.app.latestVersion?.whatsNew != null && - widget.app.latestVersion!.whatsNew!.isNotEmpty) - ChangelogPreview( - text: widget.app.latestVersion!.whatsNew, - ).animate().fadeIn( - delay: Duration(milliseconds: 300), - duration: Duration(milliseconds: 300), - ), + Builder( + builder: (context) { + final latestVersion = appProvider.getLatestVersion(widget.app); + if (latestVersion?.whatsNew != null && + latestVersion!.whatsNew!.isNotEmpty) { + return ChangelogPreview( + text: latestVersion.whatsNew, + ).animate().fadeIn( + delay: Duration(milliseconds: 300), + duration: Duration(milliseconds: 300), + ); + } + return const SizedBox.shrink(); + }, + ), if (isInstalled) Chip( visualDensity: VisualDensity.compact, @@ -1045,18 +1060,24 @@ class _AppDetailsScreenState extends State { ), // Version info - if (widget.app.latestVersion != null) - _VersionInfoSection( - version: widget.app.latestVersion!, - ).animate().fadeIn( - delay: Duration(milliseconds: 300), - duration: Duration(milliseconds: 300), - ) - else - const _NoVersionInfoSection().animate().fadeIn( - delay: Duration(milliseconds: 300), - duration: Duration(milliseconds: 300), - ), + Builder( + builder: (context) { + final latestVersion = appProvider.getLatestVersion(widget.app); + if (latestVersion != null) { + return _VersionInfoSection( + version: latestVersion, + ).animate().fadeIn( + delay: Duration(milliseconds: 300), + duration: Duration(milliseconds: 300), + ); + } else { + return const _NoVersionInfoSection().animate().fadeIn( + delay: Duration(milliseconds: 300), + duration: Duration(milliseconds: 300), + ); + } + }, + ), // All versions history if (widget.app.packages != null && widget.app.packages!.isNotEmpty) @@ -1090,49 +1111,53 @@ class _DownloadSection extends StatefulWidget { class _DownloadSectionState extends State<_DownloadSection> { @override Widget build(BuildContext context) { - if (widget.app.latestVersion == null) { - return Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.errorContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + return Consumer( + builder: (context, appProvider, _) { + final latestVersion = appProvider.getLatestVersion(widget.app); + + if (latestVersion == null) { + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Symbols.warning, - color: Theme.of(context).colorScheme.onErrorContainer, + Row( + children: [ + Icon( + Symbols.warning, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 8), + Text( + 'No Version Available', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + fontWeight: FontWeight.w600, + ), + ), + ], ), - const SizedBox(width: 8), + const SizedBox(height: 8), Text( - 'No Version Available', - style: Theme.of(context).textTheme.titleMedium?.copyWith( + 'This app doesn\'t have any downloadable versions available.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onErrorContainer, - fontWeight: FontWeight.w600, ), ), ], ), - const SizedBox(height: 8), - Text( - 'This app doesn\'t have any downloadable versions available.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ], - ), - ); - } + ); + } return Consumer2( builder: (context, downloadProvider, appProvider, child) { - final version = widget.app.latestVersion!; + final version = latestVersion; // Check if ANY version of this app is downloading DownloadInfo? activeDownloadInfo; @@ -1373,6 +1398,8 @@ class _DownloadSectionState extends State<_DownloadSection> { ); }, ); + }, + ); } } @@ -1562,21 +1589,25 @@ class _AppInfoSection extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MListHeader(title: 'App Information'), - MListView( - items: [ - MListItemData( - leading: Icon( - Symbols.package_rounded, - color: Theme.of(context).colorScheme.primary, - ), - title: 'Package Name', - subtitle: app.packageName, - onTap: () {}, - ), + return Consumer( + builder: (context, appProvider, _) { + final latestVersion = appProvider.getLatestVersion(app); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MListHeader(title: 'App Information'), + MListView( + items: [ + MListItemData( + leading: Icon( + Symbols.package_rounded, + color: Theme.of(context).colorScheme.primary, + ), + title: 'Package Name', + subtitle: app.packageName, + onTap: () {}, + ), MListItemData( leading: Icon( Symbols.license_rounded, @@ -1606,21 +1637,21 @@ class _AppInfoSection extends StatelessWidget { subtitle: _formatDate(app.lastUpdated!), onTap: () {}, ), - if (app.latestVersion?.permissions?.isNotEmpty == true) + if (latestVersion?.permissions?.isNotEmpty == true) MListItemData( leading: Icon( Symbols.security, color: Theme.of(context).colorScheme.primary, ), title: 'Permissions ', - subtitle: '(${app.latestVersion!.permissions!.length})', + subtitle: '(${latestVersion!.permissions!.length})', suffix: Icon(Symbols.arrow_forward), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => PermissionsScreen( - permissions: app.latestVersion!.permissions!, + permissions: latestVersion.permissions!, appName: app.name, ), ), @@ -1631,6 +1662,8 @@ class _AppInfoSection extends StatelessWidget { ), ], ); + }, + ); } String _formatDate(DateTime date) { diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 99953a1..e6cb0dd 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -200,6 +200,22 @@ class _SettingsScreenState extends State { subtitle: 'Add or remove F-Droid repositories', suffix: Icon(Symbols.chevron_right), ), + MListItemData( + title: 'Include unstable versions', + onTap: () { + settings.setIncludeUnstableVersions( + !settings.includeUnstableVersions, + ); + }, + subtitle: + 'Show beta, alpha, and other pre-release versions', + suffix: Switch( + value: settings.includeUnstableVersions, + onChanged: (value) { + settings.setIncludeUnstableVersions(value); + }, + ), + ), ], ), ], diff --git a/lib/services/installation_tracking_service.dart b/lib/services/installation_tracking_service.dart new file mode 100644 index 0000000..e973b30 --- /dev/null +++ b/lib/services/installation_tracking_service.dart @@ -0,0 +1,41 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Service for tracking which repository each app was downloaded from +class InstallationTrackingService { + static const String _keyPrefix = 'app_source_'; + + /// Saves the repository URL that an app was downloaded from + Future setAppSource(String packageName, String repositoryUrl) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('$_keyPrefix$packageName', repositoryUrl); + } + + /// Gets the repository URL that an app was downloaded from + Future getAppSource(String packageName) async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('$_keyPrefix$packageName'); + } + + /// Removes the tracking data for an app (call when app is uninstalled) + Future removeAppSource(String packageName) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('$_keyPrefix$packageName'); + } + + /// Gets all tracked app sources + Future> getAllAppSources() async { + final prefs = await SharedPreferences.getInstance(); + final keys = prefs.getKeys().where((key) => key.startsWith(_keyPrefix)); + final Map sources = {}; + + for (final key in keys) { + final packageName = key.substring(_keyPrefix.length); + final source = prefs.getString(key); + if (source != null) { + sources[packageName] = source; + } + } + + return sources; + } +} From b86fbf6a4b290ea7ceebbfac45e0979c7aa4d35a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:44:43 +0000 Subject: [PATCH 03/12] Use tracked repository for updates and highlight in UI - Updated _buildInstallButton to use tracked repository URL when available - Updated _showRepositorySelection to show which repo app was installed from - Highlighted tracked repository in repository selection dialog Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/screens/app_details_screen.dart | 153 ++++++++++++++++------------ 1 file changed, 88 insertions(+), 65 deletions(-) diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 2dc9a56..62143f7 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -63,76 +63,84 @@ class _AppDetailsScreenState extends State { final hasMultipleRepos = availableRepos != null && availableRepos.length > 1; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return ScaleTransition( - scale: animation, - child: FadeTransition(opacity: animation, child: child), - ); - }, - child: hasMultipleRepos - ? Row( - key: const ValueKey('split-button'), - spacing: 2, - children: [ - Expanded( - child: SizedBox( - height: 48, - child: FilledButton.icon( - onPressed: () => _handleInstall( - context, - downloadProvider, - appProvider, - isDownloaded, - version, - app.repositoryUrl, + return FutureBuilder( + future: downloadProvider.getAppSource(app.packageName), + builder: (context, snapshot) { + // Use tracked repository if available, otherwise use app's default repository + final defaultRepoUrl = snapshot.data ?? app.repositoryUrl; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: hasMultipleRepos + ? Row( + key: const ValueKey('split-button'), + spacing: 2, + children: [ + Expanded( + child: SizedBox( + height: 48, + child: FilledButton.icon( + onPressed: () => _handleInstall( + context, + downloadProvider, + appProvider, + isDownloaded, + version, + defaultRepoUrl, + ), + icon: Icon( + isDownloaded + ? Symbols.install_mobile + : Symbols.download, + ), + label: Text(isDownloaded ? 'Install' : 'Download'), + style: FilledButton.styleFrom(), + ), ), - icon: Icon( - isDownloaded - ? Symbols.install_mobile - : Symbols.download, - ), - label: Text(isDownloaded ? 'Install' : 'Download'), - style: FilledButton.styleFrom(), ), - ), - ), - SizedBox( + SizedBox( + height: 48, + child: IconButton.filledTonal( + onPressed: () => _showRepositorySelection( + context, + downloadProvider, + appProvider, + isDownloaded, + version, + app, + ), + icon: Icon(Symbols.keyboard_arrow_down), + ), + ), + ], + ) + : SizedBox( + key: const ValueKey('simple-button'), height: 48, - child: IconButton.filledTonal( - onPressed: () => _showRepositorySelection( + width: double.infinity, + child: FilledButton.icon( + onPressed: () => _handleInstall( context, downloadProvider, appProvider, isDownloaded, version, - app, + defaultRepoUrl, ), - icon: Icon(Symbols.keyboard_arrow_down), + icon: Icon( + isDownloaded ? Symbols.install_mobile : Symbols.download, + ), + label: Text(isDownloaded ? 'Install' : 'Download'), ), ), - ], - ) - : SizedBox( - key: const ValueKey('simple-button'), - height: 48, - width: double.infinity, - child: FilledButton.icon( - onPressed: () => _handleInstall( - context, - downloadProvider, - appProvider, - isDownloaded, - version, - app.repositoryUrl, - ), - icon: Icon( - isDownloaded ? Symbols.install_mobile : Symbols.download, - ), - label: Text(isDownloaded ? 'Install' : 'Download'), - ), - ), + ); + }, ); } @@ -272,6 +280,9 @@ class _AppDetailsScreenState extends State { final availableRepos = app.availableRepositories; if (availableRepos == null || availableRepos.isEmpty) return; + // Get the tracked repository for this app (if any) + final trackedRepo = await downloadProvider.getAppSource(app.packageName); + // Capture the mounted context before showing dialog final scaffoldMessenger = ScaffoldMessenger.of(context); @@ -303,6 +314,16 @@ class _AppDetailsScreenState extends State { '${isDownloaded ? 'install' : 'download'} this app.', style: Theme.of(dialogContext).textTheme.labelMedium, ), + if (trackedRepo != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + 'Previously installed from: ${availableRepos.firstWhere((r) => r.url == trackedRepo, orElse: () => RepositorySource(name: 'Unknown', url: trackedRepo)).name}', + style: Theme.of(dialogContext).textTheme.bodySmall?.copyWith( + color: Theme.of(dialogContext).colorScheme.primary, + ), + ), + ), ], ), ], @@ -311,12 +332,14 @@ class _AppDetailsScreenState extends State { MListViewBuilder( itemCount: availableRepos.length, itemBuilder: (index) { - final isPrimary = - availableRepos[index].url == app.repositoryUrl; + final repo = availableRepos[index]; + final isPrimary = repo.url == app.repositoryUrl; + final isTracked = trackedRepo != null && repo.url == trackedRepo; return MListItemData( - selected: isPrimary, - leading: isPrimary ? Icon(Symbols.check) : null, - title: availableRepos[index].name, + selected: isPrimary || isTracked, + leading: (isPrimary || isTracked) ? Icon(Symbols.check) : null, + title: repo.name, + subtitle: isTracked ? 'Previously installed from here' : null, onTap: () { Navigator.of(dialogContext).pop(); _handleInstall( @@ -325,7 +348,7 @@ class _AppDetailsScreenState extends State { appProvider, isDownloaded, version, - availableRepos[index].url, + repo.url, ); }, suffix: Visibility( From 84309040bc20cc9934790ff97cb2553b1ab59180 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:46:16 +0000 Subject: [PATCH 04/12] Improve code readability in repository selection dialog - Extract repository lookup into separate variable for better readability Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/screens/app_details_screen.dart | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 62143f7..249f7f8 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -317,11 +317,22 @@ class _AppDetailsScreenState extends State { if (trackedRepo != null) Padding( padding: const EdgeInsets.only(top: 4.0), - child: Text( - 'Previously installed from: ${availableRepos.firstWhere((r) => r.url == trackedRepo, orElse: () => RepositorySource(name: 'Unknown', url: trackedRepo)).name}', - style: Theme.of(dialogContext).textTheme.bodySmall?.copyWith( - color: Theme.of(dialogContext).colorScheme.primary, - ), + child: Builder( + builder: (context) { + final trackedRepoSource = availableRepos.firstWhere( + (r) => r.url == trackedRepo, + orElse: () => RepositorySource( + name: 'Unknown', + url: trackedRepo, + ), + ); + return Text( + 'Previously installed from: ${trackedRepoSource.name}', + style: Theme.of(dialogContext).textTheme.bodySmall?.copyWith( + color: Theme.of(dialogContext).colorScheme.primary, + ), + ); + }, ), ), ], From da8e2acde135463079ee0e1cdb7f9bcbd63095cd Mon Sep 17 00:00:00 2001 From: nahnah Date: Wed, 4 Feb 2026 23:00:05 +0000 Subject: [PATCH 05/12] [fix]: run issues fixed --- lib/providers/app_provider.dart | 37 +- lib/providers/settings_provider.dart | 2 +- lib/screens/app_details_screen.dart | 593 ++++++++++++++------------- 3 files changed, 326 insertions(+), 306 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 8fff8fa..70be6c6 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -179,7 +179,7 @@ class AppProvider extends ChangeNotifier { for (final entry in repo.apps.entries) { final packageName = entry.key; final app = entry.value; - + if (mergedApps.containsKey(packageName)) { // App already exists, add this repository to the available sources final existing = mergedApps[packageName]!; @@ -187,13 +187,13 @@ class AppProvider extends ChangeNotifier { name: repo.name, url: app.repositoryUrl, ); - + // Add the new repository if it's not already in the list final availableRepos = existing.availableRepositories ?? []; if (!availableRepos.contains(repoSource)) { // Create new list with the additional repository final updatedRepos = [...availableRepos, repoSource]; - + // Keep the existing app but update available repositories mergedApps[packageName] = existing.copyWith( availableRepositories: updatedRepos, @@ -203,10 +203,7 @@ class AppProvider extends ChangeNotifier { // First time seeing this app, add it with its repository as a source mergedApps[packageName] = app.copyWith( availableRepositories: [ - RepositorySource( - name: repo.name, - url: app.repositoryUrl, - ), + RepositorySource(name: repo.name, url: app.repositoryUrl), ], ); } @@ -255,12 +252,13 @@ class AppProvider extends ChangeNotifier { final availableReposList = []; if (app.repositoryUrl.isNotEmpty) { // Find the repo name for the original URL - final originalRepo = enabledRepos.where((r) => r.url == app.repositoryUrl).firstOrNull; + final originalRepo = enabledRepos + .where((r) => r.url == app.repositoryUrl) + .firstOrNull; if (originalRepo != null) { - availableReposList.add(RepositorySource( - name: originalRepo.name, - url: app.repositoryUrl, - )); + availableReposList.add( + RepositorySource(name: originalRepo.name, url: app.repositoryUrl), + ); } } @@ -272,19 +270,21 @@ class AppProvider extends ChangeNotifier { if (repo.url == app.repositoryUrl) { return null; } - + // Try to find the app in this repository via database final results = await _apiService.searchAppsFromRepositoryUrl( app.packageName, // Use exact package name for lookup repo.url, ); - + // If found in this repository, return the source if (results.any((a) => a.packageName == app.packageName)) { return RepositorySource(name: repo.name, url: repo.url); } } catch (e) { - debugPrint('Error checking repo ${repo.name} for ${app.packageName}: $e'); + debugPrint( + 'Error checking repo ${repo.name} for ${app.packageName}: $e', + ); } return null; }), @@ -570,8 +570,10 @@ class AppProvider extends ChangeNotifier { if (fdroidApp == null) continue; // Get the latest version based on user's unstable preference - final latestVersion = fdroidApp.getLatestVersion(includeUnstable: includeUnstable); - + final latestVersion = fdroidApp.getLatestVersion( + includeUnstable: includeUnstable, + ); + // Check if F-Droid app has a latest version if (latestVersion == null) continue; @@ -582,7 +584,6 @@ class AppProvider extends ChangeNotifier { if (latestVersion.versionCode > installedApp.versionCode!) { updatableApps.add(fdroidApp); } - } } // Sort by app name for consistent ordering diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 2cd0b5a..c865eda 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -92,7 +92,7 @@ class SettingsProvider extends ChangeNotifier { _themeMode = ThemeMode.values[themeIndex]; } final themeStyleIndex = - prefs.getInt(_themeStyleKey) ?? 0; // Default to Florid + prefs.getInt(_themeStyleKey) ?? 1; // Default to Florid if (themeStyleIndex >= 0 && themeStyleIndex < ThemeStyle.values.length) { _themeStyle = ThemeStyle.values[themeStyleIndex]; } diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 249f7f8..f68ecec 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -16,7 +16,6 @@ import '../models/fdroid_app.dart'; import '../providers/app_provider.dart'; import '../providers/download_provider.dart'; import '../providers/repositories_provider.dart'; -import '../providers/settings_provider.dart'; import '../services/izzy_stats_service.dart'; class AppDetailsScreen extends StatefulWidget { @@ -68,7 +67,7 @@ class _AppDetailsScreenState extends State { builder: (context, snapshot) { // Use tracked repository if available, otherwise use app's default repository final defaultRepoUrl = snapshot.data ?? app.repositoryUrl; - + return AnimatedSwitcher( duration: const Duration(milliseconds: 300), transitionBuilder: (child, animation) { @@ -319,18 +318,24 @@ class _AppDetailsScreenState extends State { padding: const EdgeInsets.only(top: 4.0), child: Builder( builder: (context) { - final trackedRepoSource = availableRepos.firstWhere( - (r) => r.url == trackedRepo, - orElse: () => RepositorySource( - name: 'Unknown', - url: trackedRepo, - ), - ); + final trackedRepoSource = availableRepos + .firstWhere( + (r) => r.url == trackedRepo, + orElse: () => RepositorySource( + name: 'Unknown', + url: trackedRepo, + ), + ); return Text( 'Previously installed from: ${trackedRepoSource.name}', - style: Theme.of(dialogContext).textTheme.bodySmall?.copyWith( - color: Theme.of(dialogContext).colorScheme.primary, - ), + style: Theme.of(dialogContext) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of( + dialogContext, + ).colorScheme.primary, + ), ); }, ), @@ -345,10 +350,13 @@ class _AppDetailsScreenState extends State { itemBuilder: (index) { final repo = availableRepos[index]; final isPrimary = repo.url == app.repositoryUrl; - final isTracked = trackedRepo != null && repo.url == trackedRepo; + final isTracked = + trackedRepo != null && repo.url == trackedRepo; return MListItemData( selected: isPrimary || isTracked, - leading: (isPrimary || isTracked) ? Icon(Symbols.check) : null, + leading: (isPrimary || isTracked) + ? Icon(Symbols.check) + : null, title: repo.name, subtitle: isTracked ? 'Previously installed from here' : null, onTap: () { @@ -544,11 +552,13 @@ class _AppDetailsScreenState extends State { children: [ Consumer2( builder: (context, downloadProvider, appProvider, child) { - final version = appProvider.getLatestVersion(widget.app); + final version = appProvider.getLatestVersion( + widget.app, + ); if (version == null) { return const SizedBox.shrink(); } - + final isInstalled = appProvider.isAppInstalled( widget.app.packageName, ); @@ -967,7 +977,8 @@ class _AppDetailsScreenState extends State { ), Builder( builder: (context) { - final latestVersion = appProvider.getLatestVersion(widget.app); + final latestVersion = appProvider + .getLatestVersion(widget.app); if (latestVersion?.whatsNew != null && latestVersion!.whatsNew!.isNotEmpty) { return ChangelogPreview( @@ -1096,7 +1107,9 @@ class _AppDetailsScreenState extends State { // Version info Builder( builder: (context) { - final latestVersion = appProvider.getLatestVersion(widget.app); + final latestVersion = context + .read() + .getLatestVersion(widget.app); if (latestVersion != null) { return _VersionInfoSection( version: latestVersion, @@ -1148,7 +1161,7 @@ class _DownloadSectionState extends State<_DownloadSection> { return Consumer( builder: (context, appProvider, _) { final latestVersion = appProvider.getLatestVersion(widget.app); - + if (latestVersion == null) { return Container( width: double.infinity, @@ -1189,251 +1202,257 @@ class _DownloadSectionState extends State<_DownloadSection> { ); } - return Consumer2( - builder: (context, downloadProvider, appProvider, child) { - final version = latestVersion; + return Consumer2( + builder: (context, downloadProvider, appProvider, child) { + final version = latestVersion; - // Check if ANY version of this app is downloading - DownloadInfo? activeDownloadInfo; - bool isDownloading = false; - String downloadingVersionName = version.versionName; + // Check if ANY version of this app is downloading + DownloadInfo? activeDownloadInfo; + bool isDownloading = false; + String downloadingVersionName = version.versionName; - if (widget.app.packages != null) { - for (var pkg in widget.app.packages!.values) { - final info = downloadProvider.getDownloadInfo( - widget.app.packageName, - pkg.versionName, - ); - if (info?.status == DownloadStatus.downloading) { - activeDownloadInfo = info; - isDownloading = true; - downloadingVersionName = pkg.versionName; - break; + if (widget.app.packages != null) { + for (var pkg in widget.app.packages!.values) { + final info = downloadProvider.getDownloadInfo( + widget.app.packageName, + pkg.versionName, + ); + if (info?.status == DownloadStatus.downloading) { + activeDownloadInfo = info; + isDownloading = true; + downloadingVersionName = pkg.versionName; + break; + } + } } - } - } - final progress = isDownloading - ? downloadProvider.getProgress( - widget.app.packageName, - downloadingVersionName, - ) - : 0.0; + final progress = isDownloading + ? downloadProvider.getProgress( + widget.app.packageName, + downloadingVersionName, + ) + : 0.0; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 16.0, - children: [ - if (isDownloading && activeDownloadInfo != null) ...[ - Column( - spacing: 4.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.0, + children: [ + if (isDownloading && activeDownloadInfo != null) ...[ + Column( + spacing: 4.0, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Downloading... ${(progress * 100).toInt()}%', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ).animate().fadeIn( + duration: Duration(milliseconds: 300), + ), + if (activeDownloadInfo.totalBytes > 0) + Text( + '${activeDownloadInfo.formattedBytesDownloaded} / ${activeDownloadInfo.formattedTotalBytes}', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ) + .animate() + .fadeIn(duration: Duration(milliseconds: 300)) + .slideY( + begin: 0.5, + end: 0, + duration: Duration(milliseconds: 300), + ), + ], + ), + const SizedBox(height: 4), Text( - 'Downloading... ${(progress * 100).toInt()}%', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ).animate().fadeIn(duration: Duration(milliseconds: 300)), - if (activeDownloadInfo.totalBytes > 0) - Text( - '${activeDownloadInfo.formattedBytesDownloaded} / ${activeDownloadInfo.formattedTotalBytes}', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), + activeDownloadInfo.formattedSpeed, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ) + .animate() + .fadeIn(duration: Duration(milliseconds: 300)) + .slideY( + begin: 0.5, + end: 0, + duration: Duration(milliseconds: 300), + ), + const SizedBox(height: 8), + LinearProgressIndicator(value: progress, year2023: false) + .animate() + .fadeIn(duration: Duration(milliseconds: 300)) + .slideY( + begin: 0.5, + end: 0, + duration: Duration(milliseconds: 300), + ), ], ), - const SizedBox(height: 4), - Text( - activeDownloadInfo.formattedSpeed, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w600, - ), - ) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), - const SizedBox(height: 8), - LinearProgressIndicator(value: progress, year2023: false) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), ], - ), - ], - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.download, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - version.sizeString, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.download, + size: 32, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), + ), + Text( + version.sizeString, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], ), - ], + ), ), ), - ), - ), - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.code_rounded, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - version.versionName, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.code_rounded, + size: 32, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), + ), + Text( + version.versionName, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], ), - ], + ), ), ), - ), + if (widget.stats?.hasAny != true) + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.license_rounded, + size: 32, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + Text( + widget.app.license, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + if (widget.stats?.hasAny == true) + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.chart_data, + size: 32, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + Text( + widget.stats!.last365Days != null + ? _formatCount(widget.stats!.last365Days!) + : 'N/A', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + ], ), - if (widget.stats?.hasAny != true) - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.license_rounded, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - widget.app.license, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), - if (widget.stats?.hasAny == true) - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.chart_data, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - widget.stats!.last365Days != null - ? _formatCount(widget.stats!.last365Days!) - : 'N/A', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), ], - ), - ], + ); + }, ); }, ); - }, - ); } } @@ -1626,7 +1645,7 @@ class _AppInfoSection extends StatelessWidget { return Consumer( builder: (context, appProvider, _) { final latestVersion = appProvider.getLatestVersion(app); - + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1642,60 +1661,60 @@ class _AppInfoSection extends StatelessWidget { subtitle: app.packageName, onTap: () {}, ), - MListItemData( - leading: Icon( - Symbols.license_rounded, - color: Theme.of(context).colorScheme.primary, - ), - title: 'License', - subtitle: app.license, - onTap: () {}, - ), - if (app.added != null) - MListItemData( - leading: Icon( - Symbols.add, - color: Theme.of(context).colorScheme.primary, + MListItemData( + leading: Icon( + Symbols.license_rounded, + color: Theme.of(context).colorScheme.primary, + ), + title: 'License', + subtitle: app.license, + onTap: () {}, ), - title: 'Added', - subtitle: _formatDate(app.added!), - onTap: () {}, - ), - if (app.added != null) - MListItemData( - leading: Icon( - Symbols.update, - color: Theme.of(context).colorScheme.primary, - ), - title: 'Last Updated', - subtitle: _formatDate(app.lastUpdated!), - onTap: () {}, - ), - if (latestVersion?.permissions?.isNotEmpty == true) - MListItemData( - leading: Icon( - Symbols.security, - color: Theme.of(context).colorScheme.primary, - ), - title: 'Permissions ', - subtitle: '(${latestVersion!.permissions!.length})', - suffix: Icon(Symbols.arrow_forward), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PermissionsScreen( - permissions: latestVersion.permissions!, - appName: app.name, - ), + if (app.added != null) + MListItemData( + leading: Icon( + Symbols.add, + color: Theme.of(context).colorScheme.primary, ), - ); - }, - ), + title: 'Added', + subtitle: _formatDate(app.added!), + onTap: () {}, + ), + if (app.added != null) + MListItemData( + leading: Icon( + Symbols.update, + color: Theme.of(context).colorScheme.primary, + ), + title: 'Last Updated', + subtitle: _formatDate(app.lastUpdated!), + onTap: () {}, + ), + if (latestVersion?.permissions?.isNotEmpty == true) + MListItemData( + leading: Icon( + Symbols.security, + color: Theme.of(context).colorScheme.primary, + ), + title: 'Permissions ', + subtitle: '(${latestVersion!.permissions!.length})', + suffix: Icon(Symbols.arrow_forward), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PermissionsScreen( + permissions: latestVersion.permissions!, + appName: app.name, + ), + ), + ); + }, + ), + ], + ), ], - ), - ], - ); + ); }, ); } From 0aa15be323f15ad8bf350ab880fe4c8352a775cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:15:45 +0000 Subject: [PATCH 06/12] Convert to per-app unstable version preferences - Created AppPreferencesService to manage per-app unstable version settings - Updated AppProvider to use per-app preferences instead of global setting - Made getLatestVersion async to load preferences - Updated all callers to use FutureBuilder or await - Clean up preferences when apps are uninstalled - Updated UpdatesScreen and app_details_screen to handle async version lookups Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/providers/app_provider.dart | 32 +++- lib/providers/download_provider.dart | 3 + lib/screens/app_details_screen.dart | 116 +++++++------ lib/screens/updates_screen.dart | 197 +++++++++++----------- lib/services/app_preferences_service.dart | 44 +++++ 5 files changed, 238 insertions(+), 154 deletions(-) create mode 100644 lib/services/app_preferences_service.dart diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 70be6c6..d6e5936 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -3,6 +3,7 @@ import 'package:installed_apps/app_info.dart' as installed; import 'package:installed_apps/installed_apps.dart'; import '../models/fdroid_app.dart'; +import '../services/app_preferences_service.dart'; import '../services/fdroid_api_service.dart'; import 'repositories_provider.dart'; import 'settings_provider.dart'; @@ -27,6 +28,7 @@ class AppInfo { class AppProvider extends ChangeNotifier { final FDroidApiService _apiService; SettingsProvider? _settingsProvider; + final AppPreferencesService _preferencesService = AppPreferencesService(); AppProvider(this._apiService, [this._settingsProvider]); @@ -547,6 +549,10 @@ class AppProvider extends ChangeNotifier { ) .toList(); + // Clean up preferences for uninstalled apps + final installedPackages = _installedApps.map((app) => app.packageName).toSet(); + await _preferencesService.cleanupUninstalledApps(installedPackages); + _installedAppsState = LoadingState.success; } catch (e) { debugPrint('Error fetching installed apps: $e'); @@ -556,20 +562,22 @@ class AppProvider extends ChangeNotifier { } /// Gets apps that have updates available - List getUpdatableApps() { + Future> getUpdatableApps() async { if (_repository == null || _installedApps.isEmpty) { return []; } final updatableApps = []; - final includeUnstable = _settingsProvider?.includeUnstableVersions ?? false; for (final installedApp in _installedApps) { // Check if the app exists in F-Droid repository final fdroidApp = _repository!.apps[installedApp.packageName]; if (fdroidApp == null) continue; - // Get the latest version based on user's unstable preference + // Get the latest version based on per-app unstable preference + final includeUnstable = await _preferencesService.getIncludeUnstable( + installedApp.packageName, + ); final latestVersion = fdroidApp.getLatestVersion( includeUnstable: includeUnstable, ); @@ -606,12 +614,24 @@ class AppProvider extends ChangeNotifier { } } - /// Gets the latest version for an app based on user's unstable preference - FDroidVersion? getLatestVersion(FDroidApp app) { - final includeUnstable = _settingsProvider?.includeUnstableVersions ?? false; + /// Gets the latest version for an app based on per-app unstable preference + Future getLatestVersion(FDroidApp app) async { + final includeUnstable = await _preferencesService.getIncludeUnstable(app.packageName); return app.getLatestVersion(includeUnstable: includeUnstable); } + /// Gets whether unstable versions should be included for a specific app + Future getIncludeUnstable(String packageName) async { + return await _preferencesService.getIncludeUnstable(packageName); + } + + /// Sets whether unstable versions should be included for a specific app + /// This should only be called for installed apps + Future setIncludeUnstable(String packageName, bool include) async { + await _preferencesService.setIncludeUnstable(packageName, include); + notifyListeners(); + } + /// Attempts to launch an installed app by package name Future openInstalledApp(String packageName) async { try { diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index 0d94c01..0396b56 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -7,6 +7,7 @@ import 'package:permission_handler/permission_handler.dart'; import '../models/fdroid_app.dart'; import '../providers/settings_provider.dart'; +import '../services/app_preferences_service.dart'; import '../services/fdroid_api_service.dart'; import '../services/installation_tracking_service.dart'; import '../services/notification_service.dart'; @@ -79,6 +80,7 @@ class DownloadProvider extends ChangeNotifier { final Map _downloads = {}; final NotificationService _notificationService = NotificationService(); final InstallationTrackingService _trackingService = InstallationTrackingService(); + final AppPreferencesService _preferencesService = AppPreferencesService(); DownloadProvider(this._apiService, this._settingsProvider) { _initNotifications(); @@ -434,6 +436,7 @@ class DownloadProvider extends ChangeNotifier { // Note: This is best-effort. The actual uninstall is handled by Android // and we can't know for sure if it succeeded immediately await _trackingService.removeAppSource(packageName); + await _preferencesService.removeIncludeUnstable(packageName); } catch (e) { throw Exception('Failed to uninstall app: $e'); } diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index f68ecec..cdc836c 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -238,7 +238,7 @@ class _AppDetailsScreenState extends State { await Future.delayed(const Duration(milliseconds: 800)); await appProvider.fetchInstalledApps(); if (appProvider.isAppInstalled(widget.app.packageName)) { - final latestVersion = appProvider.getLatestVersion(widget.app); + final latestVersion = await appProvider.getLatestVersion(widget.app); if (latestVersion != null) { final downloadInfo = downloadProvider.getDownloadInfo( widget.app.packageName, @@ -552,19 +552,20 @@ class _AppDetailsScreenState extends State { children: [ Consumer2( builder: (context, downloadProvider, appProvider, child) { - final version = appProvider.getLatestVersion( - widget.app, - ); - if (version == null) { - return const SizedBox.shrink(); - } + return FutureBuilder( + future: appProvider.getLatestVersion(widget.app), + builder: (context, snapshot) { + final version = snapshot.data; + if (version == null) { + return const SizedBox.shrink(); + } - final isInstalled = appProvider.isAppInstalled( - widget.app.packageName, - ); - final installedApp = appProvider.getInstalledApp( - widget.app.packageName, - ); + final isInstalled = appProvider.isAppInstalled( + widget.app.packageName, + ); + final installedApp = appProvider.getInstalledApp( + widget.app.packageName, + ); // Check if ANY version of this app is downloading DownloadInfo? activeDownloadInfo; @@ -970,15 +971,17 @@ class _AppDetailsScreenState extends State { ); }, ); + }, + ); }, ).animate().fadeIn( delay: Duration(milliseconds: 300), duration: Duration(milliseconds: 300), ), - Builder( - builder: (context) { - final latestVersion = appProvider - .getLatestVersion(widget.app); + FutureBuilder( + future: appProvider.getLatestVersion(widget.app), + builder: (context, snapshot) { + final latestVersion = snapshot.data; if (latestVersion?.whatsNew != null && latestVersion!.whatsNew!.isNotEmpty) { return ChangelogPreview( @@ -1105,11 +1108,10 @@ class _AppDetailsScreenState extends State { ), // Version info - Builder( - builder: (context) { - final latestVersion = context - .read() - .getLatestVersion(widget.app); + FutureBuilder( + future: context.read().getLatestVersion(widget.app), + builder: (context, snapshot) { + final latestVersion = snapshot.data; if (latestVersion != null) { return _VersionInfoSection( version: latestVersion, @@ -1160,16 +1162,19 @@ class _DownloadSectionState extends State<_DownloadSection> { Widget build(BuildContext context) { return Consumer( builder: (context, appProvider, _) { - final latestVersion = appProvider.getLatestVersion(widget.app); + return FutureBuilder( + future: appProvider.getLatestVersion(widget.app), + builder: (context, snapshot) { + final latestVersion = snapshot.data; - if (latestVersion == null) { - return Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.errorContainer, - borderRadius: BorderRadius.circular(16), + if (latestVersion == null) { + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1451,6 +1456,8 @@ class _DownloadSectionState extends State<_DownloadSection> { ); }, ); + }, + ); }, ); } @@ -1644,28 +1651,31 @@ class _AppInfoSection extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, appProvider, _) { - final latestVersion = appProvider.getLatestVersion(app); + return FutureBuilder( + future: appProvider.getLatestVersion(app), + builder: (context, snapshot) { + final latestVersion = snapshot.data; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MListHeader(title: 'App Information'), - MListView( - items: [ - MListItemData( - leading: Icon( - Symbols.package_rounded, - color: Theme.of(context).colorScheme.primary, - ), - title: 'Package Name', - subtitle: app.packageName, - onTap: () {}, - ), - MListItemData( - leading: Icon( - Symbols.license_rounded, - color: Theme.of(context).colorScheme.primary, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MListHeader(title: 'App Information'), + MListView( + items: [ + MListItemData( + leading: Icon( + Symbols.package_rounded, + color: Theme.of(context).colorScheme.primary, + ), + title: 'Package Name', + subtitle: app.packageName, + onTap: () {}, + ), + MListItemData( + leading: Icon( + Symbols.license_rounded, + color: Theme.of(context).colorScheme.primary, + ), title: 'License', subtitle: app.license, onTap: () {}, @@ -1715,6 +1725,8 @@ class _AppInfoSection extends StatelessWidget { ), ], ); + }, + ); }, ); } diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index 9d574d7..76255f6 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -98,108 +98,113 @@ class _UpdatesScreenState extends State ); } - final updatableApps = repositoryLoaded - ? appProvider.getUpdatableApps() - : []; + return FutureBuilder>( + future: repositoryLoaded + ? appProvider.getUpdatableApps() + : Future.value([]), + builder: (context, snapshot) { + final updatableApps = snapshot.data ?? []; - // Get all F-Droid apps installed on device - final allFDroidApps = installedApps - .where( - (installedApp) => - appProvider.repository?.apps[installedApp.packageName] != - null, - ) - .map( - (installedApp) => - appProvider.repository!.apps[installedApp.packageName]!, - ) - .toList(); + // Get all F-Droid apps installed on device + final allFDroidApps = installedApps + .where( + (installedApp) => + appProvider.repository?.apps[installedApp.packageName] != + null, + ) + .map( + (installedApp) => + appProvider.repository!.apps[installedApp.packageName]!, + ) + .toList(); - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, - surfaceTintColor: Theme.of(context).colorScheme.surfaceContainerLow, - title: const Text('Apps'), - actions: [ - IconButton( - onPressed: () { - _onRefresh(); - }, - icon: Icon(Symbols.refresh), - ), - PopupMenuButton( - onSelected: (value) { - switch (value) { - case 'settings': - MenuActions.showSettings(context); - break; - case 'about': - MenuActions.showAbout(context); - break; - } - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'settings', - child: ListTile( - leading: Icon(Symbols.settings), - title: Text('Settings'), - contentPadding: EdgeInsets.zero, - ), + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, + surfaceTintColor: Theme.of(context).colorScheme.surfaceContainerLow, + title: const Text('Apps'), + actions: [ + IconButton( + onPressed: () { + _onRefresh(); + }, + icon: Icon(Symbols.refresh), ), - const PopupMenuItem( - value: 'about', - child: ListTile( - leading: Icon(Symbols.info), - title: Text('About'), - contentPadding: EdgeInsets.zero, - ), + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'settings': + MenuActions.showSettings(context); + break; + case 'about': + MenuActions.showAbout(context); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'settings', + child: ListTile( + leading: Icon(Symbols.settings), + title: Text('Settings'), + contentPadding: EdgeInsets.zero, + ), + ), + const PopupMenuItem( + value: 'about', + child: ListTile( + leading: Icon(Symbols.info), + title: Text('About'), + contentPadding: EdgeInsets.zero, + ), + ), + ], ), ], + bottom: FTabBar( + controller: _tabController, + items: [ + FloridTabBarItem( + icon: Symbols.system_update, + label: repositoryLoaded && updatableApps.isNotEmpty + ? 'Updates (${updatableApps.length})' + : 'Updates', + ), + FloridTabBarItem(icon: Symbols.devices, label: 'On Device'), + // Tab( + // text: repositoryLoaded && updatableApps.isNotEmpty + // ? 'Updates (${updatableApps.length})' + // : 'Updates', + // ), + // Tab(text: 'On Device'), + ], + onTabChanged: (index) { + _tabController.animateTo(index); + }, + ), ), - ], - bottom: FTabBar( - controller: _tabController, - items: [ - FloridTabBarItem( - icon: Symbols.system_update, - label: repositoryLoaded && updatableApps.isNotEmpty - ? 'Updates (${updatableApps.length})' - : 'Updates', - ), - FloridTabBarItem(icon: Symbols.devices, label: 'On Device'), - // Tab( - // text: repositoryLoaded && updatableApps.isNotEmpty - // ? 'Updates (${updatableApps.length})' - // : 'Updates', - // ), - // Tab(text: 'On Device'), - ], - onTabChanged: (index) { - _tabController.animateTo(index); - }, - ), - ), - body: RefreshIndicator( - onRefresh: _onRefresh, - child: TabBarView( - controller: _tabController, - children: [ - // Tab 1: Updates Only - _buildUpdatesTab( - context, - appProvider, - updatableApps, - repositoryLoaded, - repositoryState, - repositoryError, - ), + body: RefreshIndicator( + onRefresh: _onRefresh, + child: TabBarView( + controller: _tabController, + children: [ + // Tab 1: Updates Only + _buildUpdatesTab( + context, + appProvider, + updatableApps, + repositoryLoaded, + repositoryState, + repositoryError, + ), - // Tab 2: All Installed F-Droid Apps - _buildInstalledAppsTab(context, appProvider, allFDroidApps), - ], - ), - ), + // Tab 2: All Installed F-Droid Apps + _buildInstalledAppsTab(context, appProvider, allFDroidApps, updatableApps), + ], + ), + ), + ); + }, ); }, ); @@ -430,6 +435,7 @@ class _UpdatesScreenState extends State BuildContext context, AppProvider appProvider, List allFDroidApps, + List updatableApps, ) { if (allFDroidApps.isEmpty) { return SingleChildScrollView( @@ -466,7 +472,6 @@ class _UpdatesScreenState extends State itemBuilder: (context, index) { final app = allFDroidApps[index]; final installedApp = appProvider.getInstalledApp(app.packageName); - final updatableApps = appProvider.getUpdatableApps(); final hasUpdate = updatableApps.any( (updateApp) => updateApp.packageName == app.packageName, ); diff --git a/lib/services/app_preferences_service.dart b/lib/services/app_preferences_service.dart new file mode 100644 index 0000000..d090276 --- /dev/null +++ b/lib/services/app_preferences_service.dart @@ -0,0 +1,44 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Service for managing per-app preferences like unstable version opt-in +/// These preferences are only kept for installed apps +class AppPreferencesService { + static const String _unstableKeyPrefix = 'app_unstable_'; + + /// Gets whether unstable versions should be included for a specific app + /// Returns false by default (only stable versions) + Future getIncludeUnstable(String packageName) async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool('$_unstableKeyPrefix$packageName') ?? false; + } + + /// Sets whether unstable versions should be included for a specific app + /// This should only be called for installed apps + Future setIncludeUnstable(String packageName, bool include) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('$_unstableKeyPrefix$packageName', include); + } + + /// Removes the unstable preference for an app (call when app is uninstalled) + Future removeIncludeUnstable(String packageName) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('$_unstableKeyPrefix$packageName'); + } + + /// Gets all apps that have unstable version preferences set + Future> getAllAppsWithUnstablePreference() async { + final prefs = await SharedPreferences.getInstance(); + final keys = prefs.getKeys().where((key) => key.startsWith(_unstableKeyPrefix)); + return keys.map((key) => key.substring(_unstableKeyPrefix.length)).toSet(); + } + + /// Cleans up preferences for apps that are no longer installed + Future cleanupUninstalledApps(Set installedPackages) async { + final appsWithPrefs = await getAllAppsWithUnstablePreference(); + for (final packageName in appsWithPrefs) { + if (!installedPackages.contains(packageName)) { + await removeIncludeUnstable(packageName); + } + } + } +} From 5eba97083ac7a35c39aa1922c518a3822c15edfb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:17:59 +0000 Subject: [PATCH 07/12] Remove global unstable setting and add per-app toggle - Removed includeUnstableVersions from SettingsProvider - Removed global unstable toggle from settings screen - Added per-app unstable version toggle in app details (only for installed apps) - Filter "All Versions" section based on per-app preference - Show/hide unstable versions based on user's per-app choice Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/providers/settings_provider.dart | 11 -- lib/screens/app_details_screen.dart | 172 +++++++++++++++++++-------- lib/screens/settings_screen.dart | 16 --- 3 files changed, 125 insertions(+), 74 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index c865eda..e3428f0 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -12,7 +12,6 @@ class SettingsProvider extends ChangeNotifier { static const _localeKey = 'locale'; static const _onboardingCompleteKey = 'onboarding_complete'; static const _sniBypassKey = 'sni_bypass_enabled'; - static const _includeUnstableKey = 'include_unstable_versions'; ThemeMode _themeMode = ThemeMode.system; ThemeStyle _themeStyle = ThemeStyle.florid; @@ -21,7 +20,6 @@ class SettingsProvider extends ChangeNotifier { String _locale = 'en-US'; bool _onboardingComplete = false; bool _sniBypassEnabled = true; - bool _includeUnstableVersions = false; bool _loaded = false; SettingsProvider() { @@ -36,7 +34,6 @@ class SettingsProvider extends ChangeNotifier { String get locale => _locale; bool get onboardingComplete => _onboardingComplete; bool get sniBypassEnabled => _sniBypassEnabled; - bool get includeUnstableVersions => _includeUnstableVersions; /// Available locales for F-Droid repository data static const List availableLocales = [ @@ -101,7 +98,6 @@ class SettingsProvider extends ChangeNotifier { _locale = prefs.getString(_localeKey) ?? 'en-US'; _onboardingComplete = prefs.getBool(_onboardingCompleteKey) ?? false; _sniBypassEnabled = prefs.getBool(_sniBypassKey) ?? true; - _includeUnstableVersions = prefs.getBool(_includeUnstableKey) ?? false; _loaded = true; notifyListeners(); } @@ -170,11 +166,4 @@ class SettingsProvider extends ChangeNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_sniBypassKey, value); } - - Future setIncludeUnstableVersions(bool value) async { - _includeUnstableVersions = value; - notifyListeners(); - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_includeUnstableKey, value); - } } diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index cdc836c..3495dbd 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -1005,6 +1005,67 @@ class _AppDetailsScreenState extends State { delay: Duration(milliseconds: 300), duration: Duration(milliseconds: 300), ), + if (isInstalled) + FutureBuilder( + future: appProvider.getIncludeUnstable(widget.app.packageName), + builder: (context, snapshot) { + final includeUnstable = snapshot.data ?? false; + return Card.outlined( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + child: Row( + children: [ + Icon( + Symbols.science, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Include unstable versions', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Text( + 'Show beta, alpha, and pre-release versions for this app', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Switch( + value: includeUnstable, + onChanged: (value) async { + await appProvider.setIncludeUnstable( + widget.app.packageName, + value, + ); + // Rebuild the widget to reflect the change + if (mounted) { + setState(() {}); + } + }, + ), + ], + ), + ), + ).animate().fadeIn( + delay: Duration(milliseconds: 300), + duration: Duration(milliseconds: 300), + ); + }, + ), FutureBuilder( future: _statsFuture, @@ -2030,58 +2091,71 @@ class _AllVersionsSection extends StatelessWidget { @override Widget build(BuildContext context) { - final versions = app.packages?.values.toList() ?? []; - if (versions.isEmpty) return const SizedBox.shrink(); + return Consumer( + builder: (context, appProvider, _) { + return FutureBuilder( + future: appProvider.getIncludeUnstable(app.packageName), + builder: (context, snapshot) { + final includeUnstable = snapshot.data ?? false; + + var versions = app.packages?.values.toList() ?? []; + if (versions.isEmpty) return const SizedBox.shrink(); - // Sort versions by version code descending - versions.sort((a, b) => b.versionCode.compareTo(a.versionCode)); + // Filter out unstable versions if not enabled + if (!includeUnstable) { + versions = versions.where((v) => !v.isUnstable).toList(); + if (versions.isEmpty) return const SizedBox.shrink(); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8.0, - children: [ - MListHeader(title: 'All Versions'), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - ...versions.map((version) { - final isLatest = version == versions.first; + // Sort versions by version code descending + versions.sort((a, b) => b.versionCode.compareTo(a.versionCode)); - return Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(bottom: 4), - decoration: BoxDecoration( - color: isLatest - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(16), - border: isLatest - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 1, - ) - : null, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.0, + children: [ + MListHeader(title: 'All Versions'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - version.versionName, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - Text( - 'Code: ${version.versionCode}', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( + ...versions.map((version) { + final isLatest = version == versions.first; + + return Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 4), + decoration: BoxDecoration( + color: isLatest + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + border: isLatest + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 1, + ) + : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + version.versionName, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + Text( + 'Code: ${version.versionCode}', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, @@ -2136,6 +2210,10 @@ class _AllVersionsSection extends StatelessWidget { ), ), ], + ); + }, + ); + }, ); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e6cb0dd..99953a1 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -200,22 +200,6 @@ class _SettingsScreenState extends State { subtitle: 'Add or remove F-Droid repositories', suffix: Icon(Symbols.chevron_right), ), - MListItemData( - title: 'Include unstable versions', - onTap: () { - settings.setIncludeUnstableVersions( - !settings.includeUnstableVersions, - ); - }, - subtitle: - 'Show beta, alpha, and other pre-release versions', - suffix: Switch( - value: settings.includeUnstableVersions, - onChanged: (value) { - settings.setIncludeUnstableVersions(value); - }, - ), - ), ], ), ], From bfb686d89d04495aeadd71cb1ed278de0a8a1a12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:18:42 +0000 Subject: [PATCH 08/12] Fix download bug - use filtered version for downloads - Fixed _handleInstall to use the specific filtered version when downloading - Ensures downloads respect per-app unstable version preference - Changed from copyWith to copyWithVersion to pass only the selected version Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/screens/app_details_screen.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 3495dbd..2a4a8f3 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -229,9 +229,11 @@ class _AppDetailsScreenState extends State { } try { - // Create a copy of the app with the selected repository URL - final appWithRepo = widget.app.copyWith(repositoryUrl: repositoryUrl); - await downloadProvider.downloadApk(appWithRepo); + // Create a copy of the app with the selected repository URL and the specific version + final appWithVersion = widget.app.copyWithVersion(version).copyWith( + repositoryUrl: repositoryUrl, + ); + await downloadProvider.downloadApk(appWithVersion); if (context.mounted) { for (int i = 0; i < 15; i++) { From a0931c9031d7bbf6e610b242a53f47fd22d0d961 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:19:40 +0000 Subject: [PATCH 09/12] Address code review feedback - Improved comment to explain the bug fix more clearly - Fixed spelling: 'pre-release' to 'prerelease' for consistency Co-authored-by: Nandanrmenon <16499541+Nandanrmenon@users.noreply.github.com> --- lib/screens/app_details_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 2a4a8f3..8247b0c 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -229,7 +229,8 @@ class _AppDetailsScreenState extends State { } try { - // Create a copy of the app with the selected repository URL and the specific version + // Create a copy of the app with the selected repository URL and only the filtered version + // This ensures we download the correct version respecting the per-app unstable preference final appWithVersion = widget.app.copyWithVersion(version).copyWith( repositoryUrl: repositoryUrl, ); @@ -1038,7 +1039,7 @@ class _AppDetailsScreenState extends State { ), ), Text( - 'Show beta, alpha, and pre-release versions for this app', + 'Show beta, alpha, and prerelease versions for this app', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), From d49e2681bf898bd4f4c2a46844e9371470a735e8 Mon Sep 17 00:00:00 2001 From: nahnah Date: Wed, 4 Feb 2026 23:40:05 +0000 Subject: [PATCH 10/12] [fix]: Use FutureBuilder for updatable apps count --- lib/screens/florid_app.dart | 105 ++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/lib/screens/florid_app.dart b/lib/screens/florid_app.dart index b6f41f1..28353ee 100644 --- a/lib/screens/florid_app.dart +++ b/lib/screens/florid_app.dart @@ -1,4 +1,5 @@ import 'package:florid/l10n/app_localizations.dart'; +import 'package:florid/models/fdroid_app.dart'; import 'package:florid/screens/library_screen.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -51,54 +52,66 @@ class _FloridAppState extends State { body: IndexedStack(index: _currentIndex, children: _screens), bottomNavigationBar: Consumer( builder: (context, appProvider, child) { - final updatableAppsCount = appProvider.getUpdatableApps().length; + return FutureBuilder>( + future: appProvider.getUpdatableApps(), + builder: (context, snapshot) { + final updatableAppsCount = snapshot.data?.length ?? 0; + // Build destinations with translations + final destinations = [ + NavigationDestination( + icon: const Icon(Symbols.newsstand_rounded), + selectedIcon: const Icon( + Symbols.newsstand_rounded, + fill: 1, + weight: 600, + ), + label: AppLocalizations.of(context)!.home, + ), + NavigationDestination( + icon: const Icon(Symbols.search), + selectedIcon: const Icon( + Symbols.search, + fill: 1, + weight: 600, + ), + label: AppLocalizations.of(context)!.search, + ), + NavigationDestination( + icon: updatableAppsCount > 0 + ? Badge.count( + count: updatableAppsCount, + child: const Icon(Symbols.mobile_3_rounded), + ) + : const Icon(Symbols.mobile_3_rounded), + selectedIcon: updatableAppsCount > 0 + ? Badge.count( + count: updatableAppsCount, + child: const Icon( + Symbols.mobile_3_rounded, + fill: 1, + weight: 600, + ), + ) + : const Icon( + Symbols.mobile_3_rounded, + fill: 1, + weight: 600, + ), + label: AppLocalizations.of(context)!.device, + ), + ]; - // Build destinations with translations - final destinations = [ - NavigationDestination( - icon: const Icon(Symbols.newsstand_rounded), - selectedIcon: const Icon( - Symbols.newsstand_rounded, - fill: 1, - weight: 600, - ), - label: AppLocalizations.of(context)!.home, - ), - NavigationDestination( - icon: const Icon(Symbols.search), - selectedIcon: const Icon(Symbols.search, fill: 1, weight: 600), - label: AppLocalizations.of(context)!.search, - ), - NavigationDestination( - icon: updatableAppsCount > 0 - ? Badge.count( - count: updatableAppsCount, - child: const Icon(Symbols.mobile_3_rounded), - ) - : const Icon(Symbols.mobile_3_rounded), - selectedIcon: updatableAppsCount > 0 - ? Badge.count( - count: updatableAppsCount, - child: const Icon( - Symbols.mobile_3_rounded, - fill: 1, - weight: 600, - ), - ) - : const Icon(Symbols.mobile_3_rounded, fill: 1, weight: 600), - label: AppLocalizations.of(context)!.device, - ), - ]; - - return NavigationBar( - selectedIndex: _currentIndex, - onDestinationSelected: (index) { - setState(() { - _currentIndex = index; - }); - _tabNotifier.value = index; + return NavigationBar( + selectedIndex: _currentIndex, + onDestinationSelected: (index) { + setState(() { + _currentIndex = index; + }); + _tabNotifier.value = index; + }, + destinations: destinations, + ); }, - destinations: destinations, ); }, ), From 79eda2798640ef87f360e899afe6055f3bc85f9e Mon Sep 17 00:00:00 2001 From: nahnah Date: Wed, 4 Feb 2026 23:40:29 +0000 Subject: [PATCH 11/12] [imp]: Move the toggle for unstable version to a suitable location --- lib/screens/app_details_screen.dart | 1621 ++++++++++++++------------- 1 file changed, 861 insertions(+), 760 deletions(-) diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 8247b0c..83f2472 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -231,9 +231,9 @@ class _AppDetailsScreenState extends State { try { // Create a copy of the app with the selected repository URL and only the filtered version // This ensures we download the correct version respecting the per-app unstable preference - final appWithVersion = widget.app.copyWithVersion(version).copyWith( - repositoryUrl: repositoryUrl, - ); + final appWithVersion = widget.app + .copyWithVersion(version) + .copyWith(repositoryUrl: repositoryUrl); await downloadProvider.downloadApk(appWithVersion); if (context.mounted) { @@ -241,7 +241,9 @@ class _AppDetailsScreenState extends State { await Future.delayed(const Duration(milliseconds: 800)); await appProvider.fetchInstalledApps(); if (appProvider.isAppInstalled(widget.app.packageName)) { - final latestVersion = await appProvider.getLatestVersion(widget.app); + final latestVersion = await appProvider.getLatestVersion( + widget.app, + ); if (latestVersion != null) { final downloadInfo = downloadProvider.getDownloadInfo( widget.app.packageName, @@ -556,206 +558,364 @@ class _AppDetailsScreenState extends State { Consumer2( builder: (context, downloadProvider, appProvider, child) { return FutureBuilder( - future: appProvider.getLatestVersion(widget.app), + future: appProvider.getLatestVersion( + widget.app, + ), builder: (context, snapshot) { final version = snapshot.data; if (version == null) { return const SizedBox.shrink(); } - final isInstalled = appProvider.isAppInstalled( - widget.app.packageName, - ); - final installedApp = appProvider.getInstalledApp( - widget.app.packageName, - ); + final isInstalled = appProvider + .isAppInstalled(widget.app.packageName); + final installedApp = appProvider + .getInstalledApp(widget.app.packageName); - // Check if ANY version of this app is downloading - DownloadInfo? activeDownloadInfo; - bool isDownloading = false; + // Check if ANY version of this app is downloading + DownloadInfo? activeDownloadInfo; + bool isDownloading = false; - if (widget.app.packages != null) { - for (var pkg in widget.app.packages!.values) { - final info = downloadProvider.getDownloadInfo( - widget.app.packageName, - pkg.versionName, - ); - if (info?.status == - DownloadStatus.downloading) { - activeDownloadInfo = info; - isDownloading = true; - break; - } - } - } - - // If no version is downloading, check the latest version for install/download buttons - final downloadInfo = - activeDownloadInfo ?? - downloadProvider.getDownloadInfo( - widget.app.packageName, - version.versionName, - ); - final isCancelled = - downloadInfo?.status == - DownloadStatus.cancelled; - final fileExists = downloadInfo?.filePath != null - ? File(downloadInfo!.filePath!).existsSync() - : false; - final isDownloaded = - downloadInfo?.status == - DownloadStatus.completed && - downloadInfo?.filePath != null && - !isCancelled && - fileExists; - - if (isDownloading && activeDownloadInfo != null) { - // Find the version name that's downloading - String downloadingVersionName = - version.versionName; - if (widget.app.packages != null) { - for (var pkg in widget.app.packages!.values) { - final info = downloadProvider - .getDownloadInfo( - widget.app.packageName, - pkg.versionName, - ); - if (info?.status == - DownloadStatus.downloading) { - downloadingVersionName = pkg.versionName; - break; + if (widget.app.packages != null) { + for (var pkg + in widget.app.packages!.values) { + final info = downloadProvider + .getDownloadInfo( + widget.app.packageName, + pkg.versionName, + ); + if (info?.status == + DownloadStatus.downloading) { + activeDownloadInfo = info; + isDownloading = true; + break; + } } } - } - return SizedBox( - width: double.infinity, - height: 48, - child: FilledButton.tonal( - onPressed: () { - downloadProvider.cancelDownload( + // If no version is downloading, check the latest version for install/download buttons + final downloadInfo = + activeDownloadInfo ?? + downloadProvider.getDownloadInfo( widget.app.packageName, - downloadingVersionName, + version.versionName, ); - }, - child: const Text('Cancel Download'), - ), - ); - } + final isCancelled = + downloadInfo?.status == + DownloadStatus.cancelled; + final fileExists = + downloadInfo?.filePath != null + ? File( + downloadInfo!.filePath!, + ).existsSync() + : false; + final isDownloaded = + downloadInfo?.status == + DownloadStatus.completed && + downloadInfo?.filePath != null && + !isCancelled && + fileExists; - if (isInstalled && installedApp != null) { - // Check if update is available - final hasUpdate = - installedApp.versionCode != null && - version.versionCode > - installedApp.versionCode!; + if (isDownloading && + activeDownloadInfo != null) { + // Find the version name that's downloading + String downloadingVersionName = + version.versionName; + if (widget.app.packages != null) { + for (var pkg + in widget.app.packages!.values) { + final info = downloadProvider + .getDownloadInfo( + widget.app.packageName, + pkg.versionName, + ); + if (info?.status == + DownloadStatus.downloading) { + downloadingVersionName = + pkg.versionName; + break; + } + } + } - if (hasUpdate) { - // Show Update button - return Column( - spacing: 8, - children: [ - Row( + return SizedBox( + width: double.infinity, + height: 48, + child: FilledButton.tonal( + onPressed: () { + downloadProvider.cancelDownload( + widget.app.packageName, + downloadingVersionName, + ); + }, + child: const Text('Cancel Download'), + ), + ); + } + + if (isInstalled && installedApp != null) { + // Check if update is available + final hasUpdate = + installedApp.versionCode != null && + version.versionCode > + installedApp.versionCode!; + + if (hasUpdate) { + // Show Update button + return Column( spacing: 8, children: [ - Expanded( - child: SizedBox( - height: 48, - child: FilledButton.icon( - onPressed: () async { - final hasPermission = - await downloadProvider - .requestPermissions(); + Row( + spacing: 8, + children: [ + Expanded( + child: SizedBox( + height: 48, + child: FilledButton.icon( + onPressed: () async { + final hasPermission = + await downloadProvider + .requestPermissions(); - if (!hasPermission) { - if (context.mounted) { - await showDialog( - context: context, - builder: (context) => AlertDialog( - icon: const Icon( - Symbols.warning, - size: 48, - ), - title: const Text( - 'Storage Permission Required', - ), - content: const Text( - 'Florid needs storage permission to download APK files.\n\n' - 'To enable:\n' - '1. Go to Settings (button below)\n' - '2. Find "Permissions"\n' - '3. Enable "Files and media" or "Storage"\n\n' - 'Then try downloading again.', - ), - actions: [ - TextButton( - onPressed: () => - Navigator.of( - context, - ).pop(), - child: const Text( - 'Cancel', + if (!hasPermission) { + if (context.mounted) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon( + Symbols.warning, + size: 48, + ), + title: const Text( + 'Storage Permission Required', + ), + content: const Text( + 'Florid needs storage permission to download APK files.\n\n' + 'To enable:\n' + '1. Go to Settings (button below)\n' + '2. Find "Permissions"\n' + '3. Enable "Files and media" or "Storage"\n\n' + 'Then try downloading again.', + ), + actions: [ + TextButton( + onPressed: () => + Navigator.of( + context, + ).pop(), + child: + const Text( + 'Cancel', + ), + ), + FilledButton( + onPressed: () async { + Navigator.of( + context, + ).pop(); + await openAppSettings(); + }, + child: const Text( + 'Open Settings', + ), + ), + ], + ), + ); + } + return; + } + + try { + await downloadProvider + .downloadApk( + widget.app, + ); + + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Downloading ${widget.app.name} update...', ), ), - FilledButton( - onPressed: () async { - Navigator.of( - context, - ).pop(); - await openAppSettings(); - }, - child: const Text( - 'Open Settings', + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Update failed: $e', ), ), - ], - ), - ); - } - return; - } - - try { - await downloadProvider - .downloadApk( - widget.app, - ); - - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Downloading ${widget.app.name} update...', - ), - ), - ); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Update failed: $e', - ), - ), - ); - } - } - }, - icon: const Icon( - Symbols.upgrade, + ); + } + } + }, + icon: const Icon( + Symbols.upgrade, + ), + label: const Text('Update'), + ), ), - label: const Text('Update'), + ), + SizedBox( + height: 48, + child: FilledButton.tonalIcon( + onPressed: () async { + try { + final opened = + await appProvider + .openInstalledApp( + widget + .app + .packageName, + ); + if (!opened && + context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Unable to open ${widget.app.name}.', + ), + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Open failed: ${e.toString()}', + ), + ), + ); + } + } + }, + icon: const Icon( + Symbols.open_in_new_rounded, + ), + label: const Text('Open'), + ), + ), + SizedBox( + height: 48, + child: FilledButton.tonal( + onPressed: () async { + try { + await downloadProvider + .uninstallApp( + widget + .app + .packageName, + ); + await Future.delayed( + const Duration( + seconds: 1, + ), + ); + await appProvider + .fetchInstalledApps(); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Uninstall failed: ${e.toString()}', + ), + ), + ); + } + } + }, + // label: const Text('Uninstall'), + style: FilledButton.styleFrom( + foregroundColor: + Theme.of(context) + .colorScheme + .onErrorContainer, + backgroundColor: + Theme.of(context) + .colorScheme + .errorContainer, + ), + child: const Icon( + Symbols.delete_rounded, + fill: 1, + ), + ), + ), + ], + ), + ], + ); + } + + // No update available, show normal buttons + return Row( + spacing: 8, + children: [ + Expanded( + child: SizedBox( + height: 48, + child: FilledButton.tonalIcon( + onPressed: () async { + try { + await downloadProvider + .uninstallApp( + widget.app.packageName, + ); + await Future.delayed( + const Duration(seconds: 1), + ); + await appProvider + .fetchInstalledApps(); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Uninstall failed: ${e.toString()}', + ), + ), + ); + } + } + }, + icon: const Icon( + Symbols.delete_rounded, + fill: 1, + ), + label: const Text('Uninstall'), + style: FilledButton.styleFrom( + foregroundColor: Theme.of( + context, + ).colorScheme.onErrorContainer, + backgroundColor: Theme.of( + context, + ).colorScheme.errorContainer, ), ), ), - SizedBox( + ), + Expanded( + child: SizedBox( height: 48, - child: FilledButton.tonalIcon( + child: FilledButton.icon( onPressed: () async { try { final opened = @@ -797,185 +957,46 @@ class _AppDetailsScreenState extends State { label: const Text('Open'), ), ), - SizedBox( - height: 48, - child: FilledButton.tonal( - onPressed: () async { - try { - await downloadProvider - .uninstallApp( - widget.app.packageName, - ); - await Future.delayed( - const Duration(seconds: 1), - ); - await appProvider - .fetchInstalledApps(); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Uninstall failed: ${e.toString()}', - ), - ), - ); - } - } - }, - // label: const Text('Uninstall'), - style: FilledButton.styleFrom( - foregroundColor: Theme.of( - context, - ).colorScheme.onErrorContainer, - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - ), - child: const Icon( - Symbols.delete_rounded, - fill: 1, - ), - ), - ), - ], - ), - ], - ); - } - - // No update available, show normal buttons - return Row( - spacing: 8, - children: [ - Expanded( - child: SizedBox( - height: 48, - child: FilledButton.tonalIcon( - onPressed: () async { - try { - await downloadProvider - .uninstallApp( - widget.app.packageName, - ); - await Future.delayed( - const Duration(seconds: 1), - ); - await appProvider - .fetchInstalledApps(); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Uninstall failed: ${e.toString()}', - ), - ), - ); - } - } - }, - icon: const Icon( - Symbols.delete_rounded, - fill: 1, - ), - label: const Text('Uninstall'), - style: FilledButton.styleFrom( - foregroundColor: Theme.of( - context, - ).colorScheme.onErrorContainer, - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - ), ), - ), - ), - Expanded( - child: SizedBox( - height: 48, - child: FilledButton.icon( - onPressed: () async { - try { - final opened = await appProvider - .openInstalledApp( - widget.app.packageName, - ); - if (!opened && context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Unable to open ${widget.app.name}.', - ), - ), - ); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Open failed: ${e.toString()}', - ), - ), - ); - } - } - }, - icon: const Icon( - Symbols.open_in_new_rounded, - ), - label: const Text('Open'), - ), - ), - ), - ], - ); - } - - // return SizedBox( - // width: double.infinity, - return FutureBuilder( - future: _enrichedAppFuture, - builder: (context, snapshot) { - // Use enriched app if available and loaded, otherwise fall back to widget.app - final enrichedApp = - snapshot.connectionState == - ConnectionState.done && - snapshot.hasData - ? snapshot.data! - : widget.app; - - // Log error if enrichment failed - if (snapshot.hasError) { - debugPrint( - 'Error enriching app: ${snapshot.error}', + ], ); } - return SizedBox( - width: double.infinity, - child: _buildInstallButton( - context, - downloadProvider, - appProvider, - isDownloaded, - version, - enrichedApp, - ), + // return SizedBox( + // width: double.infinity, + return FutureBuilder( + future: _enrichedAppFuture, + builder: (context, snapshot) { + // Use enriched app if available and loaded, otherwise fall back to widget.app + final enrichedApp = + snapshot.connectionState == + ConnectionState.done && + snapshot.hasData + ? snapshot.data! + : widget.app; + + // Log error if enrichment failed + if (snapshot.hasError) { + debugPrint( + 'Error enriching app: ${snapshot.error}', + ); + } + + return SizedBox( + width: double.infinity, + child: _buildInstallButton( + context, + downloadProvider, + appProvider, + isDownloaded, + version, + enrichedApp, + ), + ); + }, ); }, ); - }, - ); }, ).animate().fadeIn( delay: Duration(milliseconds: 300), @@ -1008,67 +1029,6 @@ class _AppDetailsScreenState extends State { delay: Duration(milliseconds: 300), duration: Duration(milliseconds: 300), ), - if (isInstalled) - FutureBuilder( - future: appProvider.getIncludeUnstable(widget.app.packageName), - builder: (context, snapshot) { - final includeUnstable = snapshot.data ?? false; - return Card.outlined( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - child: Row( - children: [ - Icon( - Symbols.science, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Include unstable versions', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - Text( - 'Show beta, alpha, and prerelease versions for this app', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - Switch( - value: includeUnstable, - onChanged: (value) async { - await appProvider.setIncludeUnstable( - widget.app.packageName, - value, - ); - // Rebuild the widget to reflect the change - if (mounted) { - setState(() {}); - } - }, - ), - ], - ), - ), - ).animate().fadeIn( - delay: Duration(milliseconds: 300), - duration: Duration(milliseconds: 300), - ); - }, - ), FutureBuilder( future: _statsFuture, @@ -1137,6 +1097,12 @@ class _AppDetailsScreenState extends State { duration: Duration(milliseconds: 300), ), + // Include unstable versions toggle (only show if unstable versions exist) + IncludeUnstableSection(app: widget.app).animate().fadeIn( + delay: Duration(milliseconds: 300), + duration: Duration(milliseconds: 300), + ), + // App details _AppInfoSection(app: widget.app).animate().fadeIn( delay: Duration(milliseconds: 300), @@ -1173,7 +1139,9 @@ class _AppDetailsScreenState extends State { // Version info FutureBuilder( - future: context.read().getLatestVersion(widget.app), + future: context.read().getLatestVersion( + widget.app, + ), builder: (context, snapshot) { final latestVersion = snapshot.data; if (latestVersion != null) { @@ -1239,289 +1207,311 @@ class _DownloadSectionState extends State<_DownloadSection> { decoration: BoxDecoration( color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Symbols.warning, - color: Theme.of(context).colorScheme.onErrorContainer, + Row( + children: [ + Icon( + Symbols.warning, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 8), + Text( + 'No Version Available', + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onErrorContainer, + fontWeight: FontWeight.w600, + ), + ), + ], ), - const SizedBox(width: 8), + const SizedBox(height: 8), Text( - 'No Version Available', - style: Theme.of(context).textTheme.titleMedium?.copyWith( + 'This app doesn\'t have any downloadable versions available.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onErrorContainer, - fontWeight: FontWeight.w600, ), ), ], ), - const SizedBox(height: 8), - Text( - 'This app doesn\'t have any downloadable versions available.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ], - ), - ); - } - - return Consumer2( - builder: (context, downloadProvider, appProvider, child) { - final version = latestVersion; - - // Check if ANY version of this app is downloading - DownloadInfo? activeDownloadInfo; - bool isDownloading = false; - String downloadingVersionName = version.versionName; - - if (widget.app.packages != null) { - for (var pkg in widget.app.packages!.values) { - final info = downloadProvider.getDownloadInfo( - widget.app.packageName, - pkg.versionName, - ); - if (info?.status == DownloadStatus.downloading) { - activeDownloadInfo = info; - isDownloading = true; - downloadingVersionName = pkg.versionName; - break; - } - } + ); } - final progress = isDownloading - ? downloadProvider.getProgress( - widget.app.packageName, - downloadingVersionName, - ) - : 0.0; + return Consumer2( + builder: (context, downloadProvider, appProvider, child) { + final version = latestVersion; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 16.0, - children: [ - if (isDownloading && activeDownloadInfo != null) ...[ - Column( - spacing: 4.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // Check if ANY version of this app is downloading + DownloadInfo? activeDownloadInfo; + bool isDownloading = false; + String downloadingVersionName = version.versionName; + + if (widget.app.packages != null) { + for (var pkg in widget.app.packages!.values) { + final info = downloadProvider.getDownloadInfo( + widget.app.packageName, + pkg.versionName, + ); + if (info?.status == DownloadStatus.downloading) { + activeDownloadInfo = info; + isDownloading = true; + downloadingVersionName = pkg.versionName; + break; + } + } + } + + final progress = isDownloading + ? downloadProvider.getProgress( + widget.app.packageName, + downloadingVersionName, + ) + : 0.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16.0, + children: [ + if (isDownloading && activeDownloadInfo != null) ...[ + Column( + spacing: 4.0, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Downloading... ${(progress * 100).toInt()}%', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ).animate().fadeIn( - duration: Duration(milliseconds: 300), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Downloading... ${(progress * 100).toInt()}%', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ).animate().fadeIn( + duration: Duration(milliseconds: 300), + ), + if (activeDownloadInfo.totalBytes > 0) + Text( + '${activeDownloadInfo.formattedBytesDownloaded} / ${activeDownloadInfo.formattedTotalBytes}', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ) + .animate() + .fadeIn( + duration: Duration(milliseconds: 300), + ) + .slideY( + begin: 0.5, + end: 0, + duration: Duration(milliseconds: 300), + ), + ], ), - if (activeDownloadInfo.totalBytes > 0) - Text( - '${activeDownloadInfo.formattedBytesDownloaded} / ${activeDownloadInfo.formattedTotalBytes}', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), + const SizedBox(height: 4), + Text( + activeDownloadInfo.formattedSpeed, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ) + .animate() + .fadeIn(duration: Duration(milliseconds: 300)) + .slideY( + begin: 0.5, + end: 0, + duration: Duration(milliseconds: 300), + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: progress, + year2023: false, + ) + .animate() + .fadeIn(duration: Duration(milliseconds: 300)) + .slideY( + begin: 0.5, + end: 0, + duration: Duration(milliseconds: 300), + ), ], ), - const SizedBox(height: 4), - Text( - activeDownloadInfo.formattedSpeed, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w600, - ), - ) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), - const SizedBox(height: 8), - LinearProgressIndicator(value: progress, year2023: false) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), ], - ), - ], - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.download, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - version.sizeString, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.download, + size: 32, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), + ), + Text( + version.sizeString, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], ), - ], + ), ), ), - ), - ), - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.code_rounded, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - version.versionName, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.code_rounded, + size: 32, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), + ), + Text( + version.versionName, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], ), - ], + ), ), ), - ), + if (widget.stats?.hasAny != true) + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.license_rounded, + size: 32, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + Text( + widget.app.license, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + if (widget.stats?.hasAny == true) + Expanded( + child: Card.outlined( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 8, + children: [ + SizedBox( + height: 32, + child: Icon( + Symbols.chart_data, + size: 32, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + Text( + widget.stats!.last365Days != null + ? _formatCount( + widget.stats!.last365Days!, + ) + : 'N/A', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + ], ), - if (widget.stats?.hasAny != true) - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.license_rounded, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - widget.app.license, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), - if (widget.stats?.hasAny == true) - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.chart_data, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - widget.stats!.last365Days != null - ? _formatCount(widget.stats!.last365Days!) - : 'N/A', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), ], - ), - ], + ); + }, ); }, ); - }, - ); }, ); } @@ -1740,55 +1730,55 @@ class _AppInfoSection extends StatelessWidget { Symbols.license_rounded, color: Theme.of(context).colorScheme.primary, ), - title: 'License', - subtitle: app.license, - onTap: () {}, - ), - if (app.added != null) - MListItemData( - leading: Icon( - Symbols.add, - color: Theme.of(context).colorScheme.primary, + title: 'License', + subtitle: app.license, + onTap: () {}, ), - title: 'Added', - subtitle: _formatDate(app.added!), - onTap: () {}, - ), - if (app.added != null) - MListItemData( - leading: Icon( - Symbols.update, - color: Theme.of(context).colorScheme.primary, - ), - title: 'Last Updated', - subtitle: _formatDate(app.lastUpdated!), - onTap: () {}, - ), - if (latestVersion?.permissions?.isNotEmpty == true) - MListItemData( - leading: Icon( - Symbols.security, - color: Theme.of(context).colorScheme.primary, - ), - title: 'Permissions ', - subtitle: '(${latestVersion!.permissions!.length})', - suffix: Icon(Symbols.arrow_forward), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PermissionsScreen( - permissions: latestVersion.permissions!, - appName: app.name, - ), + if (app.added != null) + MListItemData( + leading: Icon( + Symbols.add, + color: Theme.of(context).colorScheme.primary, ), - ); - }, - ), + title: 'Added', + subtitle: _formatDate(app.added!), + onTap: () {}, + ), + if (app.added != null) + MListItemData( + leading: Icon( + Symbols.update, + color: Theme.of(context).colorScheme.primary, + ), + title: 'Last Updated', + subtitle: _formatDate(app.lastUpdated!), + onTap: () {}, + ), + if (latestVersion?.permissions?.isNotEmpty == true) + MListItemData( + leading: Icon( + Symbols.security, + color: Theme.of(context).colorScheme.primary, + ), + title: 'Permissions ', + subtitle: '(${latestVersion!.permissions!.length})', + suffix: Icon(Symbols.arrow_forward), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PermissionsScreen( + permissions: latestVersion.permissions!, + appName: app.name, + ), + ), + ); + }, + ), + ], + ), ], - ), - ], - ); + ); }, ); }, @@ -1894,6 +1884,94 @@ class _DescriptionSectionState extends State<_DescriptionSection> } } +class IncludeUnstableSection extends StatefulWidget { + final FDroidApp app; + const IncludeUnstableSection({super.key, required this.app}); + + @override + State createState() => _IncludeUnstableSectionState(); +} + +class _IncludeUnstableSectionState extends State { + @override + Widget build(BuildContext context) { + if (widget.app.packages != null && + widget.app.packages!.values.any((v) => v.isUnstable)) { + return FutureBuilder( + future: context.read().getIncludeUnstable( + widget.app.packageName, + ), + builder: (context, snapshot) { + final includeUnstable = snapshot.data ?? false; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: + Card.outlined( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + child: Row( + children: [ + Icon( + Symbols.science, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Include unstable versions', + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w500), + ), + Text( + 'Show beta, alpha, and prerelease versions for this app', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Switch( + value: includeUnstable, + onChanged: (value) async { + await context + .read() + .setIncludeUnstable( + widget.app.packageName, + value, + ); + // Rebuild the widget to reflect the change + if (mounted) { + setState(() {}); + } + }, + ), + ], + ), + ), + ).animate().fadeIn( + delay: Duration(milliseconds: 300), + duration: Duration(milliseconds: 300), + ), + ); + }, + ); + } + return const SizedBox.shrink(); + } +} + class _VersionInfoSection extends StatelessWidget { final FDroidVersion version; @@ -2100,7 +2178,7 @@ class _AllVersionsSection extends StatelessWidget { future: appProvider.getIncludeUnstable(app.packageName), builder: (context, snapshot) { final includeUnstable = snapshot.data ?? false; - + var versions = app.packages?.values.toList() ?? []; if (versions.isEmpty) return const SizedBox.shrink(); @@ -2131,11 +2209,15 @@ class _AllVersionsSection extends StatelessWidget { decoration: BoxDecoration( color: isLatest ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainer, + : Theme.of( + context, + ).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(16), border: isLatest ? Border.all( - color: Theme.of(context).colorScheme.primary, + color: Theme.of( + context, + ).colorScheme.primary, width: 1, ) : null, @@ -2144,76 +2226,95 @@ class _AllVersionsSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Text( version.versionName, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), Text( 'Code: ${version.versionCode}', - style: Theme.of(context).textTheme.bodySmall + style: Theme.of(context) + .textTheme + .bodySmall ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + if (isLatest) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( color: Theme.of( context, - ).colorScheme.onSurfaceVariant, + ).colorScheme.primary, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Latest', + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onPrimary, + ), ), - ), - ], - ), - ), - if (isLatest) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'Latest', - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onPrimary, ), + ], ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Text( - 'Size: ${version.sizeString}', - style: Theme.of(context).textTheme.bodySmall, + const SizedBox(height: 8), + Row( + children: [ + Text( + 'Size: ${version.sizeString}', + style: Theme.of( + context, + ).textTheme.bodySmall, + ), + const SizedBox(width: 16), + Text( + 'Released: ${_formatDate(version.added)}', + style: Theme.of( + context, + ).textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 12), + _VersionDownloadButton( + app: app, + version: version, + ), + ], ), - const SizedBox(width: 16), - Text( - 'Released: ${_formatDate(version.added)}', - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - const SizedBox(height: 12), - _VersionDownloadButton(app: app, version: version), + ); + }), ], ), - ); - }), - ], - ), - ), - ], - ); + ), + ], + ); }, ); }, From df5abe3b53aa4f2eee82f5db1f783df1a7162178 Mon Sep 17 00:00:00 2001 From: nahnah Date: Wed, 4 Feb 2026 23:42:37 +0000 Subject: [PATCH 12/12] Revert "Merge branch 'main' into copilot/allow-unstable-version-opt-in" This reverts commit dfa07614158a3f7a06a662300efd48387997c238, reversing changes made to 79eda2798640ef87f360e899afe6055f3bc85f9e. --- lib/l10n/crowdin_localizations.dart | 515 ++++++++------------ lib/screens/app_details_screen.dart | 85 +--- lib/screens/categories_screen.dart | 3 +- lib/screens/category_apps_screen.dart | 2 +- lib/screens/home_screen.dart | 17 +- lib/screens/latest_screen.dart | 3 +- lib/screens/library_screen.dart | 125 +++-- lib/screens/onboarding_screen.dart | 11 +- lib/screens/recently_updated_screen.dart | 2 +- lib/screens/repositories_screen.dart | 572 ++++++++++++----------- lib/screens/search_screen.dart | 2 +- lib/screens/settings_screen.dart | 520 +++++++++++---------- lib/screens/updates_screen.dart | 4 +- lib/themes/app_themes.dart | 93 +--- 14 files changed, 861 insertions(+), 1093 deletions(-) diff --git a/lib/l10n/crowdin_localizations.dart b/lib/l10n/crowdin_localizations.dart index e753a7a..b85c958 100644 --- a/lib/l10n/crowdin_localizations.dart +++ b/lib/l10n/crowdin_localizations.dart @@ -1,433 +1,306 @@ -import 'package:crowdin_sdk/crowdin_sdk.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; +import 'dart:convert'; import 'app_localizations.dart'; +import 'package:crowdin_sdk/crowdin_sdk.dart'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + class CrowdinLocalization extends AppLocalizations { final AppLocalizations _fallbackTexts; + + CrowdinLocalization(String locale, AppLocalizations fallbackTexts) : _fallbackTexts = fallbackTexts, super(locale); - CrowdinLocalization(super.locale, AppLocalizations fallbackTexts) - : _fallbackTexts = fallbackTexts; + static const LocalizationsDelegate delegate = _CrowdinLocalizationsDelegate(); - static const LocalizationsDelegate delegate = - _CrowdinLocalizationsDelegate(); + static const List> localizationsDelegates = < + LocalizationsDelegate>[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; - static const List> localizationsDelegates = - >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + static const List supportedLocales = AppLocalizations.supportedLocales; + + @override + String get app_name => Crowdin.getText(localeName, 'app_name') ?? _fallbackTexts.app_name; - static const List supportedLocales = - AppLocalizations.supportedLocales; + @override + String get welcome => Crowdin.getText(localeName, 'welcome') ?? _fallbackTexts.welcome; - @override - String get app_name => - Crowdin.getText(localeName, 'app_name') ?? _fallbackTexts.app_name; + @override + String get search => Crowdin.getText(localeName, 'search') ?? _fallbackTexts.search; - @override - String get welcome => - Crowdin.getText(localeName, 'welcome') ?? _fallbackTexts.welcome; + @override + String get settings => Crowdin.getText(localeName, 'settings') ?? _fallbackTexts.settings; - @override - String get search => - Crowdin.getText(localeName, 'search') ?? _fallbackTexts.search; - - @override - String get settings => - Crowdin.getText(localeName, 'settings') ?? _fallbackTexts.settings; - - @override + @override String get home => Crowdin.getText(localeName, 'home') ?? _fallbackTexts.home; - @override - String get categories => - Crowdin.getText(localeName, 'categories') ?? _fallbackTexts.categories; + @override + String get categories => Crowdin.getText(localeName, 'categories') ?? _fallbackTexts.categories; - @override - String get updates => - Crowdin.getText(localeName, 'updates') ?? _fallbackTexts.updates; + @override + String get updates => Crowdin.getText(localeName, 'updates') ?? _fallbackTexts.updates; - @override - String get installed => - Crowdin.getText(localeName, 'installed') ?? _fallbackTexts.installed; + @override + String get installed => Crowdin.getText(localeName, 'installed') ?? _fallbackTexts.installed; - @override - String get download => - Crowdin.getText(localeName, 'download') ?? _fallbackTexts.download; + @override + String get download => Crowdin.getText(localeName, 'download') ?? _fallbackTexts.download; - @override - String get install => - Crowdin.getText(localeName, 'install') ?? _fallbackTexts.install; + @override + String get install => Crowdin.getText(localeName, 'install') ?? _fallbackTexts.install; - @override - String get uninstall => - Crowdin.getText(localeName, 'uninstall') ?? _fallbackTexts.uninstall; + @override + String get uninstall => Crowdin.getText(localeName, 'uninstall') ?? _fallbackTexts.uninstall; - @override + @override String get open => Crowdin.getText(localeName, 'open') ?? _fallbackTexts.open; - @override - String get cancel => - Crowdin.getText(localeName, 'cancel') ?? _fallbackTexts.cancel; + @override + String get cancel => Crowdin.getText(localeName, 'cancel') ?? _fallbackTexts.cancel; - @override - String get update_available => - Crowdin.getText(localeName, 'update_available') ?? - _fallbackTexts.update_available; + @override + String get update_available => Crowdin.getText(localeName, 'update_available') ?? _fallbackTexts.update_available; - @override - String get downloading => - Crowdin.getText(localeName, 'downloading') ?? _fallbackTexts.downloading; + @override + String get downloading => Crowdin.getText(localeName, 'downloading') ?? _fallbackTexts.downloading; - @override - String get install_permission_required => - Crowdin.getText(localeName, 'install_permission_required') ?? - _fallbackTexts.install_permission_required; + @override + String get install_permission_required => Crowdin.getText(localeName, 'install_permission_required') ?? _fallbackTexts.install_permission_required; - @override - String get storage_permission_required => - Crowdin.getText(localeName, 'storage_permission_required') ?? - _fallbackTexts.storage_permission_required; + @override + String get storage_permission_required => Crowdin.getText(localeName, 'storage_permission_required') ?? _fallbackTexts.storage_permission_required; - @override - String get cancel_download => - Crowdin.getText(localeName, 'cancel_download') ?? - _fallbackTexts.cancel_download; + @override + String get cancel_download => Crowdin.getText(localeName, 'cancel_download') ?? _fallbackTexts.cancel_download; - @override - String get version => - Crowdin.getText(localeName, 'version') ?? _fallbackTexts.version; + @override + String get version => Crowdin.getText(localeName, 'version') ?? _fallbackTexts.version; - @override + @override String get size => Crowdin.getText(localeName, 'size') ?? _fallbackTexts.size; - @override - String get description => - Crowdin.getText(localeName, 'description') ?? _fallbackTexts.description; + @override + String get description => Crowdin.getText(localeName, 'description') ?? _fallbackTexts.description; - @override - String get permissions => - Crowdin.getText(localeName, 'permissions') ?? _fallbackTexts.permissions; + @override + String get permissions => Crowdin.getText(localeName, 'permissions') ?? _fallbackTexts.permissions; - @override - String get screenshots => - Crowdin.getText(localeName, 'screenshots') ?? _fallbackTexts.screenshots; + @override + String get screenshots => Crowdin.getText(localeName, 'screenshots') ?? _fallbackTexts.screenshots; - @override - String get no_version_available => - Crowdin.getText(localeName, 'no_version_available') ?? - _fallbackTexts.no_version_available; + @override + String get no_version_available => Crowdin.getText(localeName, 'no_version_available') ?? _fallbackTexts.no_version_available; - @override - String get app_information => - Crowdin.getText(localeName, 'app_information') ?? - _fallbackTexts.app_information; + @override + String get app_information => Crowdin.getText(localeName, 'app_information') ?? _fallbackTexts.app_information; - @override - String get package_name => - Crowdin.getText(localeName, 'package_name') ?? - _fallbackTexts.package_name; + @override + String get package_name => Crowdin.getText(localeName, 'package_name') ?? _fallbackTexts.package_name; - @override - String get license => - Crowdin.getText(localeName, 'license') ?? _fallbackTexts.license; + @override + String get license => Crowdin.getText(localeName, 'license') ?? _fallbackTexts.license; - @override - String get added => - Crowdin.getText(localeName, 'added') ?? _fallbackTexts.added; + @override + String get added => Crowdin.getText(localeName, 'added') ?? _fallbackTexts.added; - @override - String get last_updated => - Crowdin.getText(localeName, 'last_updated') ?? - _fallbackTexts.last_updated; + @override + String get last_updated => Crowdin.getText(localeName, 'last_updated') ?? _fallbackTexts.last_updated; - @override - String get version_information => - Crowdin.getText(localeName, 'version_information') ?? - _fallbackTexts.version_information; + @override + String get version_information => Crowdin.getText(localeName, 'version_information') ?? _fallbackTexts.version_information; - @override - String get version_name => - Crowdin.getText(localeName, 'version_name') ?? - _fallbackTexts.version_name; + @override + String get version_name => Crowdin.getText(localeName, 'version_name') ?? _fallbackTexts.version_name; - @override - String get version_code => - Crowdin.getText(localeName, 'version_code') ?? - _fallbackTexts.version_code; + @override + String get version_code => Crowdin.getText(localeName, 'version_code') ?? _fallbackTexts.version_code; - @override - String get min_sdk => - Crowdin.getText(localeName, 'min_sdk') ?? _fallbackTexts.min_sdk; + @override + String get min_sdk => Crowdin.getText(localeName, 'min_sdk') ?? _fallbackTexts.min_sdk; - @override - String get target_sdk => - Crowdin.getText(localeName, 'target_sdk') ?? _fallbackTexts.target_sdk; + @override + String get target_sdk => Crowdin.getText(localeName, 'target_sdk') ?? _fallbackTexts.target_sdk; - @override - String get all_versions => - Crowdin.getText(localeName, 'all_versions') ?? - _fallbackTexts.all_versions; + @override + String get all_versions => Crowdin.getText(localeName, 'all_versions') ?? _fallbackTexts.all_versions; - @override - String get latest => - Crowdin.getText(localeName, 'latest') ?? _fallbackTexts.latest; + @override + String get latest => Crowdin.getText(localeName, 'latest') ?? _fallbackTexts.latest; - @override - String get released => - Crowdin.getText(localeName, 'released') ?? _fallbackTexts.released; + @override + String get released => Crowdin.getText(localeName, 'released') ?? _fallbackTexts.released; - @override - String get loading => - Crowdin.getText(localeName, 'loading') ?? _fallbackTexts.loading; + @override + String get loading => Crowdin.getText(localeName, 'loading') ?? _fallbackTexts.loading; - @override - String get error => - Crowdin.getText(localeName, 'error') ?? _fallbackTexts.error; + @override + String get error => Crowdin.getText(localeName, 'error') ?? _fallbackTexts.error; - @override - String get retry => - Crowdin.getText(localeName, 'retry') ?? _fallbackTexts.retry; + @override + String get retry => Crowdin.getText(localeName, 'retry') ?? _fallbackTexts.retry; - @override - String get share => - Crowdin.getText(localeName, 'share') ?? _fallbackTexts.share; + @override + String get share => Crowdin.getText(localeName, 'share') ?? _fallbackTexts.share; - @override - String get website => - Crowdin.getText(localeName, 'website') ?? _fallbackTexts.website; + @override + String get website => Crowdin.getText(localeName, 'website') ?? _fallbackTexts.website; - @override - String get source_code => - Crowdin.getText(localeName, 'source_code') ?? _fallbackTexts.source_code; + @override + String get source_code => Crowdin.getText(localeName, 'source_code') ?? _fallbackTexts.source_code; - @override - String get issue_tracker => - Crowdin.getText(localeName, 'issue_tracker') ?? - _fallbackTexts.issue_tracker; + @override + String get issue_tracker => Crowdin.getText(localeName, 'issue_tracker') ?? _fallbackTexts.issue_tracker; - @override - String get whats_new => - Crowdin.getText(localeName, 'whats_new') ?? _fallbackTexts.whats_new; + @override + String get whats_new => Crowdin.getText(localeName, 'whats_new') ?? _fallbackTexts.whats_new; - @override - String get show_more => - Crowdin.getText(localeName, 'show_more') ?? _fallbackTexts.show_more; + @override + String get show_more => Crowdin.getText(localeName, 'show_more') ?? _fallbackTexts.show_more; - @override - String get show_less => - Crowdin.getText(localeName, 'show_less') ?? _fallbackTexts.show_less; + @override + String get show_less => Crowdin.getText(localeName, 'show_less') ?? _fallbackTexts.show_less; - @override - String get downloads_stats => - Crowdin.getText(localeName, 'downloads_stats') ?? - _fallbackTexts.downloads_stats; + @override + String get downloads_stats => Crowdin.getText(localeName, 'downloads_stats') ?? _fallbackTexts.downloads_stats; - @override - String get last_day => - Crowdin.getText(localeName, 'last_day') ?? _fallbackTexts.last_day; + @override + String get last_day => Crowdin.getText(localeName, 'last_day') ?? _fallbackTexts.last_day; - @override - String get last_30_days => - Crowdin.getText(localeName, 'last_30_days') ?? - _fallbackTexts.last_30_days; + @override + String get last_30_days => Crowdin.getText(localeName, 'last_30_days') ?? _fallbackTexts.last_30_days; - @override - String get last_365_days => - Crowdin.getText(localeName, 'last_365_days') ?? - _fallbackTexts.last_365_days; + @override + String get last_365_days => Crowdin.getText(localeName, 'last_365_days') ?? _fallbackTexts.last_365_days; - @override - String get not_available => - Crowdin.getText(localeName, 'not_available') ?? - _fallbackTexts.not_available; + @override + String get not_available => Crowdin.getText(localeName, 'not_available') ?? _fallbackTexts.not_available; - @override - String get download_failed => - Crowdin.getText(localeName, 'download_failed') ?? - _fallbackTexts.download_failed; + @override + String get download_failed => Crowdin.getText(localeName, 'download_failed') ?? _fallbackTexts.download_failed; - @override - String get installation_failed => - Crowdin.getText(localeName, 'installation_failed') ?? - _fallbackTexts.installation_failed; + @override + String get installation_failed => Crowdin.getText(localeName, 'installation_failed') ?? _fallbackTexts.installation_failed; - @override - String get uninstall_failed => - Crowdin.getText(localeName, 'uninstall_failed') ?? - _fallbackTexts.uninstall_failed; + @override + String get uninstall_failed => Crowdin.getText(localeName, 'uninstall_failed') ?? _fallbackTexts.uninstall_failed; - @override - String get open_failed => - Crowdin.getText(localeName, 'open_failed') ?? _fallbackTexts.open_failed; + @override + String get open_failed => Crowdin.getText(localeName, 'open_failed') ?? _fallbackTexts.open_failed; - @override - String get device => - Crowdin.getText(localeName, 'device') ?? _fallbackTexts.device; + @override + String get device => Crowdin.getText(localeName, 'device') ?? _fallbackTexts.device; - @override - String get recently_updated => - Crowdin.getText(localeName, 'recently_updated') ?? - _fallbackTexts.recently_updated; + @override + String get recently_updated => Crowdin.getText(localeName, 'recently_updated') ?? _fallbackTexts.recently_updated; - @override - String get refresh => - Crowdin.getText(localeName, 'refresh') ?? _fallbackTexts.refresh; + @override + String get refresh => Crowdin.getText(localeName, 'refresh') ?? _fallbackTexts.refresh; - @override - String get about => - Crowdin.getText(localeName, 'about') ?? _fallbackTexts.about; + @override + String get about => Crowdin.getText(localeName, 'about') ?? _fallbackTexts.about; - @override - String get refreshing_data => - Crowdin.getText(localeName, 'refreshing_data') ?? - _fallbackTexts.refreshing_data; + @override + String get refreshing_data => Crowdin.getText(localeName, 'refreshing_data') ?? _fallbackTexts.refreshing_data; - @override - String get data_refreshed => - Crowdin.getText(localeName, 'data_refreshed') ?? - _fallbackTexts.data_refreshed; + @override + String get data_refreshed => Crowdin.getText(localeName, 'data_refreshed') ?? _fallbackTexts.data_refreshed; - @override - String get refresh_failed => - Crowdin.getText(localeName, 'refresh_failed') ?? - _fallbackTexts.refresh_failed; + @override + String get refresh_failed => Crowdin.getText(localeName, 'refresh_failed') ?? _fallbackTexts.refresh_failed; - @override - String get loading_latest_apps => - Crowdin.getText(localeName, 'loading_latest_apps') ?? - _fallbackTexts.loading_latest_apps; + @override + String get loading_latest_apps => Crowdin.getText(localeName, 'loading_latest_apps') ?? _fallbackTexts.loading_latest_apps; - @override - String get latest_apps => - Crowdin.getText(localeName, 'latest_apps') ?? _fallbackTexts.latest_apps; + @override + String get latest_apps => Crowdin.getText(localeName, 'latest_apps') ?? _fallbackTexts.latest_apps; - @override - String get no_apps_found => - Crowdin.getText(localeName, 'no_apps_found') ?? - _fallbackTexts.no_apps_found; + @override + String get no_apps_found => Crowdin.getText(localeName, 'no_apps_found') ?? _fallbackTexts.no_apps_found; - @override - String get searching => - Crowdin.getText(localeName, 'searching') ?? _fallbackTexts.searching; + @override + String get searching => Crowdin.getText(localeName, 'searching') ?? _fallbackTexts.searching; - @override - String get setup_failed => - Crowdin.getText(localeName, 'setup_failed') ?? - _fallbackTexts.setup_failed; + @override + String get setup_failed => Crowdin.getText(localeName, 'setup_failed') ?? _fallbackTexts.setup_failed; - @override + @override String get back => Crowdin.getText(localeName, 'back') ?? _fallbackTexts.back; - @override - String get allow => - Crowdin.getText(localeName, 'allow') ?? _fallbackTexts.allow; + @override + String get allow => Crowdin.getText(localeName, 'allow') ?? _fallbackTexts.allow; - @override - String get manage_repositories => - Crowdin.getText(localeName, 'manage_repositories') ?? - _fallbackTexts.manage_repositories; + @override + String get manage_repositories => Crowdin.getText(localeName, 'manage_repositories') ?? _fallbackTexts.manage_repositories; - @override - String get enable_disable => - Crowdin.getText(localeName, 'enable_disable') ?? - _fallbackTexts.enable_disable; + @override + String get enable_disable => Crowdin.getText(localeName, 'enable_disable') ?? _fallbackTexts.enable_disable; - @override + @override String get edit => Crowdin.getText(localeName, 'edit') ?? _fallbackTexts.edit; - @override - String get delete => - Crowdin.getText(localeName, 'delete') ?? _fallbackTexts.delete; + @override + String get delete => Crowdin.getText(localeName, 'delete') ?? _fallbackTexts.delete; - @override - String get delete_repository => - Crowdin.getText(localeName, 'delete_repository') ?? - _fallbackTexts.delete_repository; + @override + String get delete_repository => Crowdin.getText(localeName, 'delete_repository') ?? _fallbackTexts.delete_repository; - @override - String delete_repository_confirm(Object name, Object repo) => - Crowdin.getText(localeName, 'delete_repository_confirm', { - 'name': name, - 'repo': repo, - }) ?? - _fallbackTexts.delete_repository_confirm(name, repo); + @override + String delete_repository_confirm(Object name, Object repo) => Crowdin.getText(localeName, 'delete_repository_confirm', {'name': name, 'repo': repo}) ?? _fallbackTexts.delete_repository_confirm(name, repo); - @override - String get updating_repository => - Crowdin.getText(localeName, 'updating_repository') ?? - _fallbackTexts.updating_repository; + @override + String get updating_repository => Crowdin.getText(localeName, 'updating_repository') ?? _fallbackTexts.updating_repository; - @override - String get touch_grass_message => - Crowdin.getText(localeName, 'touch_grass_message') ?? - _fallbackTexts.touch_grass_message; + @override + String get touch_grass_message => Crowdin.getText(localeName, 'touch_grass_message') ?? _fallbackTexts.touch_grass_message; - @override - String get add_repository => - Crowdin.getText(localeName, 'add_repository') ?? - _fallbackTexts.add_repository; + @override + String get add_repository => Crowdin.getText(localeName, 'add_repository') ?? _fallbackTexts.add_repository; - @override + @override String get add => Crowdin.getText(localeName, 'add') ?? _fallbackTexts.add; - @override + @override String get save => Crowdin.getText(localeName, 'save') ?? _fallbackTexts.save; - @override - String get enter_repository_name => - Crowdin.getText(localeName, 'enter_repository_name') ?? - _fallbackTexts.enter_repository_name; + @override + String get enter_repository_name => Crowdin.getText(localeName, 'enter_repository_name') ?? _fallbackTexts.enter_repository_name; - @override - String get enter_repository_url => - Crowdin.getText(localeName, 'enter_repository_url') ?? - _fallbackTexts.enter_repository_url; + @override + String get enter_repository_url => Crowdin.getText(localeName, 'enter_repository_url') ?? _fallbackTexts.enter_repository_url; - @override - String get edit_repository => - Crowdin.getText(localeName, 'edit_repository') ?? - _fallbackTexts.edit_repository; + @override + String get edit_repository => Crowdin.getText(localeName, 'edit_repository') ?? _fallbackTexts.edit_repository; - @override - String get loading_apps => - Crowdin.getText(localeName, 'loading_apps') ?? - _fallbackTexts.loading_apps; + @override + String get loading_apps => Crowdin.getText(localeName, 'loading_apps') ?? _fallbackTexts.loading_apps; - @override - String no_apps_in_category(Object category) => - Crowdin.getText(localeName, 'no_apps_in_category', { - 'category': category, - }) ?? - _fallbackTexts.no_apps_in_category(category); + @override + String no_apps_in_category(Object category) => Crowdin.getText(localeName, 'no_apps_in_category', {'category':category}) ?? _fallbackTexts.no_apps_in_category(category); - @override - String get loading_categories => - Crowdin.getText(localeName, 'loading_categories') ?? - _fallbackTexts.loading_categories; + @override + String get loading_categories => Crowdin.getText(localeName, 'loading_categories') ?? _fallbackTexts.loading_categories; + + @override + String get no_categories_found => Crowdin.getText(localeName, 'no_categories_found') ?? _fallbackTexts.no_categories_found; - @override - String get no_categories_found => - Crowdin.getText(localeName, 'no_categories_found') ?? - _fallbackTexts.no_categories_found; } -class _CrowdinLocalizationsDelegate - extends LocalizationsDelegate { +class _CrowdinLocalizationsDelegate extends LocalizationsDelegate { const _CrowdinLocalizationsDelegate(); @override - Future load(Locale locale) => AppLocalizations.delegate - .load(locale) - .then((fallback) => CrowdinLocalization(locale.toString(), fallback)); + Future load(Locale locale) => + AppLocalizations.delegate.load(locale) + .then((fallback) => CrowdinLocalization(locale.toString(), fallback)); @override - bool isSupported(Locale locale) => - AppLocalizations.supportedLocales.contains(locale); + bool isSupported(Locale locale) => AppLocalizations.supportedLocales.contains(locale); @override bool shouldReload(_CrowdinLocalizationsDelegate old) => false; diff --git a/lib/screens/app_details_screen.dart b/lib/screens/app_details_screen.dart index 72bb6ad..83f2472 100644 --- a/lib/screens/app_details_screen.dart +++ b/lib/screens/app_details_screen.dart @@ -284,6 +284,12 @@ class _AppDetailsScreenState extends State { final availableRepos = app.availableRepositories; if (availableRepos == null || availableRepos.isEmpty) return; + // Get the tracked repository for this app (if any) + final trackedRepo = await downloadProvider.getAppSource(app.packageName); + + // Capture the mounted context before showing dialog + final scaffoldMessenger = ScaffoldMessenger.of(context); + await showModalBottomSheet( context: context, builder: (dialogContext) => Padding( @@ -415,7 +421,7 @@ class _AppDetailsScreenState extends State { return Scaffold( body: CustomScrollView( slivers: [ - SliverAppBar( + SliverAppBar.large( pinned: true, centerTitle: false, title: Row( @@ -473,7 +479,6 @@ class _AppDetailsScreenState extends State { }, ), PopupMenuButton( - icon: const Icon(Symbols.more_vert), onSelected: (value) async { switch (value) { case 'website': @@ -1223,59 +1228,13 @@ class _DownloadSectionState extends State<_DownloadSection> { fontWeight: FontWeight.w600, ), ), - ) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), - const SizedBox(height: 8), - LinearProgressIndicator(value: progress) - .animate() - .fadeIn(duration: Duration(milliseconds: 300)) - .slideY( - begin: 0.5, - end: 0, - duration: Duration(milliseconds: 300), - ), - ], - ), - ], - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - Expanded( - child: Card.outlined( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 8, - children: [ - SizedBox( - height: 32, - child: Icon( - Symbols.download, - size: 32, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - Text( - version.sizeString, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], + ], + ), + const SizedBox(height: 8), + Text( + 'This app doesn\'t have any downloadable versions available.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, ), ), ], @@ -1689,7 +1648,7 @@ class _IzzyStatsLoadingCard extends StatelessWidget { child: Row( spacing: 12, children: [ - CircularProgressIndicator(), + CircularProgressIndicator(year2023: false), Expanded( child: Text( 'Loading IzzyOnDroid download stats...', @@ -2158,14 +2117,14 @@ class _AppDetailsIconState extends State<_AppDetailsIcon> { Widget build(BuildContext context) { if (_showFallback) { return Container( - color: Colors.white.withValues(alpha: 0.2), + color: Colors.white.withOpacity(0.2), child: const Icon(Symbols.android, color: Colors.white, size: 40), ); } if (_index >= _candidates.length) { return Container( - color: Colors.white.withValues(alpha: 0.2), + color: Colors.white.withOpacity(0.2), child: const Icon(Symbols.apps, color: Colors.white, size: 40), ); } @@ -2178,7 +2137,7 @@ class _AppDetailsIconState extends State<_AppDetailsIcon> { // Move to next candidate or fallback _next(); return Container( - color: Colors.white.withValues(alpha: 0.2), + color: Colors.white.withOpacity(0.2), child: const Icon( Symbols.broken_image, color: Colors.white, @@ -2189,13 +2148,14 @@ class _AppDetailsIconState extends State<_AppDetailsIcon> { loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( - color: Colors.white.withValues(alpha: 0.2), + color: Colors.white.withOpacity(0.2), alignment: Alignment.center, child: const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, + year2023: false, valueColor: AlwaysStoppedAnimation(Colors.white), ), ), @@ -2479,7 +2439,7 @@ class _VersionDownloadButton extends StatelessWidget { ], ), const SizedBox(height: 8), - LinearProgressIndicator(value: progress), + LinearProgressIndicator(value: progress, year2023: false), ], ); } @@ -2669,7 +2629,7 @@ class _ScreenshotsSectionState extends State<_ScreenshotsSection> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const CircularProgressIndicator(), + const CircularProgressIndicator(year2023: false), const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric( @@ -2794,6 +2754,7 @@ class _FullScreenScreenshotsState extends State<_FullScreenScreenshots> { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( + year2023: false, value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! diff --git a/lib/screens/categories_screen.dart b/lib/screens/categories_screen.dart index 9a58f18..f8beee4 100644 --- a/lib/screens/categories_screen.dart +++ b/lib/screens/categories_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:florid/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -61,7 +62,7 @@ class _CategoriesScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const CircularProgressIndicator(), + const CircularProgressIndicator(year2023: false), const SizedBox(height: 16), Text(AppLocalizations.of(context)!.loading_categories), ], diff --git a/lib/screens/category_apps_screen.dart b/lib/screens/category_apps_screen.dart index b795042..e2771fc 100644 --- a/lib/screens/category_apps_screen.dart +++ b/lib/screens/category_apps_screen.dart @@ -80,7 +80,7 @@ class _CategoryAppsScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + CircularProgressIndicator(year2023: false), SizedBox(height: 16), Text(AppLocalizations.of(context)!.loading_apps), ], diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 0136e05..357dd4c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:florid/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -109,9 +110,7 @@ class _HomeScreenState extends State onPressed: _openRecentlyUpdatedScreen, iconAlignment: IconAlignment.end, icon: Icon(Symbols.arrow_forward), - label: Text( - AppLocalizations.of(context)!.show_more, - ), + label: Text(AppLocalizations.of(context)!.show_more), ), ], ), @@ -119,7 +118,9 @@ class _HomeScreenState extends State if (isLoading && recentlyUpdatedApps.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center(child: CircularProgressIndicator()), + child: Center( + child: CircularProgressIndicator(year2023: false), + ), ) else if (recentlyUpdatedApps.isEmpty) Padding( @@ -187,9 +188,7 @@ class _HomeScreenState extends State onPressed: _openLatestScreen, iconAlignment: IconAlignment.end, icon: Icon(Symbols.arrow_forward), - label: Text( - AppLocalizations.of(context)!.show_more, - ), + label: Text(AppLocalizations.of(context)!.show_more), ), ], ), @@ -197,7 +196,9 @@ class _HomeScreenState extends State if (isLoading && latestApps.isEmpty) const Padding( padding: EdgeInsets.symmetric(horizontal: 32.0), - child: Center(child: CircularProgressIndicator()), + child: Center( + child: CircularProgressIndicator(year2023: false), + ), ) else if (latestApps.isEmpty) Padding( diff --git a/lib/screens/latest_screen.dart b/lib/screens/latest_screen.dart index 9580ca0..fe8e0aa 100644 --- a/lib/screens/latest_screen.dart +++ b/lib/screens/latest_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:florid/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -64,7 +65,7 @@ class _LatestScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + CircularProgressIndicator(year2023: false), SizedBox(height: 16), Text(AppLocalizations.of(context)!.loading_latest_apps), ], diff --git a/lib/screens/library_screen.dart b/lib/screens/library_screen.dart index 2511e5d..23793aa 100644 --- a/lib/screens/library_screen.dart +++ b/lib/screens/library_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:florid/l10n/app_localizations.dart'; import 'package:florid/providers/app_provider.dart'; import 'package:florid/providers/repositories_provider.dart'; @@ -6,7 +7,7 @@ import 'package:florid/screens/home_screen.dart'; import 'package:florid/utils/menu_actions.dart'; import 'package:florid/widgets/f_tabbar.dart'; import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:provider/provider.dart'; class LibraryScreen extends StatefulWidget { @@ -36,73 +37,67 @@ class _LibraryScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: Text(AppLocalizations.of(context)!.app_name), - backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, - surfaceTintColor: Theme.of(context).colorScheme.surfaceContainerLow, - actions: [ - PopupMenuButton( - onSelected: (value) { - switch (value) { - case 'refresh': - _refreshData(); - break; - case 'settings': - MenuActions.showSettings(context); - break; - case 'about': - MenuActions.showAbout(context); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'refresh', - child: ListTile( - leading: const Icon(Symbols.refresh), - title: Text(AppLocalizations.of(context)!.refresh), - contentPadding: EdgeInsets.zero, - ), - ), - PopupMenuItem( - value: 'settings', - child: ListTile( - leading: const Icon(Symbols.settings), - title: Text(AppLocalizations.of(context)!.settings), - contentPadding: EdgeInsets.zero, - ), - ), - PopupMenuItem( - value: 'about', - child: ListTile( - leading: Icon(Symbols.info), - title: Text(AppLocalizations.of(context)!.about), - contentPadding: EdgeInsets.zero, - ), - ), - ], + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.app_name), + backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, + surfaceTintColor: Theme.of(context).colorScheme.surfaceContainerLow, + actions: [ + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'refresh': + _refreshData(); + break; + case 'settings': + MenuActions.showSettings(context); + break; + case 'about': + MenuActions.showAbout(context); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'refresh', + child: ListTile( + leading: const Icon(Symbols.refresh), + title: Text(AppLocalizations.of(context)!.refresh), + contentPadding: EdgeInsets.zero, + ), + ), + PopupMenuItem( + value: 'settings', + child: ListTile( + leading: const Icon(Symbols.settings), + title: Text(AppLocalizations.of(context)!.settings), + contentPadding: EdgeInsets.zero, + ), + ), + PopupMenuItem( + value: 'about', + child: ListTile( + leading: Icon(Symbols.info), + title: Text(AppLocalizations.of(context)!.about), + contentPadding: EdgeInsets.zero, + ), ), ], - bottom: FTabBar( - controller: _tabController, - onTabChanged: (index) { - _tabController.animateTo(index); - }, - items: [ - FloridTabBarItem( - icon: Symbols.home, - label: AppLocalizations.of(context)!.home, - ), - FloridTabBarItem( - icon: Symbols.category, - label: AppLocalizations.of(context)!.categories, - ), - ], - ), ), - SliverFillRemaining( + ], + bottom: FTabBar( + controller: _tabController, + onTabChanged: (index) { + _tabController.animateTo(index); + }, + items: [ + FloridTabBarItem(icon: Symbols.home, label: AppLocalizations.of(context)!.home), + FloridTabBarItem(icon: Symbols.category, label: AppLocalizations.of(context)!.categories), + ], + ), + ), + body: Column( + children: [ + Expanded( child: TabBarView(controller: _tabController, children: tabs), ), ], diff --git a/lib/screens/onboarding_screen.dart b/lib/screens/onboarding_screen.dart index c4ce64e..f693409 100644 --- a/lib/screens/onboarding_screen.dart +++ b/lib/screens/onboarding_screen.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:easy_localization/easy_localization.dart'; import 'package:florid/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -229,10 +230,7 @@ class _OnboardingScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${AppLocalizations.of(context)!.setup_failed}: $e'), - action: SnackBarAction( - label: AppLocalizations.of(context)!.retry, - onPressed: _performSetup, - ), + action: SnackBarAction(label: AppLocalizations.of(context)!.retry, onPressed: _performSetup), ), ); } @@ -594,7 +592,7 @@ class _PermissionCard extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isGranted - ? colorScheme.primaryContainer.withValues(alpha: 0.5) + ? colorScheme.primaryContainer.withOpacity(0.5) : colorScheme.surfaceContainer, border: Border.all( color: isGranted ? colorScheme.primary : colorScheme.outlineVariant, @@ -609,7 +607,7 @@ class _PermissionCard extends StatelessWidget { padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: isGranted - ? colorScheme.primary.withValues(alpha: 0.2) + ? colorScheme.primary.withOpacity(0.2) : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), @@ -697,6 +695,7 @@ class _ProgressStep extends StatelessWidget { child: LinearProgressIndicator( value: progress, minHeight: 8, + year2023: false, borderRadius: BorderRadius.circular(4), ), ), diff --git a/lib/screens/recently_updated_screen.dart b/lib/screens/recently_updated_screen.dart index 46f4364..f3c1c3e 100644 --- a/lib/screens/recently_updated_screen.dart +++ b/lib/screens/recently_updated_screen.dart @@ -65,7 +65,7 @@ class _RecentlyUpdatedScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + CircularProgressIndicator(year2023: false), SizedBox(height: 16), Text('Loading recently updated apps...'), ], diff --git a/lib/screens/repositories_screen.dart b/lib/screens/repositories_screen.dart index 21f2ebf..f73d10c 100644 --- a/lib/screens/repositories_screen.dart +++ b/lib/screens/repositories_screen.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:florid/l10n/app_localizations.dart'; import 'package:florid/widgets/m_list.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -57,308 +56,279 @@ class _RepositoriesScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar.medium( - leading: BackButton( - style: ButtonStyle( - backgroundColor: WidgetStateColor.resolveWith( - (states) => Theme.of(context).colorScheme.surfaceContainer, - ), - ), - ), - title: Text(AppLocalizations.of(context)!.manage_repositories), - ), - SliverToBoxAdapter( - child: Consumer( - builder: (context, provider, _) { - if (provider.isLoading) { - return const Center(child: CircularProgressIndicator()); - } + appBar: AppBar(title: const Text('Manage Repositories')), + body: Consumer( + builder: (context, provider, _) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), - child: Column( - spacing: 16, - children: [ - // Error message if any - if (provider.error != null) - Padding( - padding: const EdgeInsets.all(8.0), - child: Material( - color: Colors.red.shade100, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - Icon( - Symbols.error, - color: Colors.red.shade700, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - provider.error ?? 'Unknown error', - style: TextStyle( - color: Colors.red.shade700, - ), - ), - ), - IconButton( - icon: const Icon(Symbols.close), - onPressed: provider.clearError, - color: Colors.red.shade700, - ), - ], + return Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Column( + spacing: 16, + children: [ + // Error message if any + if (provider.error != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Icon(Symbols.error, color: Colors.red.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + provider.error ?? 'Unknown error', + style: TextStyle(color: Colors.red.shade700), ), ), - ), - ), - // Presets section - if (_presets.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4.0, - children: [ - MListHeader(title: 'Preset'), - MListViewBuilder( - itemCount: _presets.length, - itemBuilder: (index) { - final preset = _presets[index]; - final isAdded = provider.repositories.any( - (repo) => repo.url == preset['url'], - ); - - return MListItemData( - title: preset['name']!, - subtitle: preset['description']!, - onTap: () {}, - suffix: Switch( - value: isAdded, - onChanged: (newValue) async { - if (newValue) { - // Add the preset - final repoProvider = context - .read(); - await repoProvider.addRepository( - preset['name']!, - preset['url']!, - ); - // Only proceed with modal and refresh if addition succeeded (no error) - if (repoProvider.error == null && - context.mounted) { - await _runRepositoryActionWithDialog( - context, - () async { - final apiService = context - .read(); - final appProvider = context - .read(); - - await apiService - .clearRepositoryCache(); - await appProvider.refreshAll( - repositoriesProvider: - repoProvider, - ); - }, - ); - } - } else { - // Remove the preset - Repository? addedRepo; - try { - addedRepo = provider.repositories - .firstWhere( - (repo) => - repo.url == preset['url'], - ); - } catch (e) { - addedRepo = null; - } - if (addedRepo != null) { - final repoProvider = context - .read(); - repoProvider.deleteRepository( - addedRepo.id, - ); - // Only proceed with modal and refresh if deletion succeeded (no error) - if (repoProvider.error == null && - context.mounted) { - await _runRepositoryActionWithDialog( - context, - () async { - final apiService = context - .read(); - final appProvider = context - .read(); - - await apiService - .clearRepositoryCache(); - await appProvider.refreshAll( - repositoriesProvider: - repoProvider, - ); - }, - ); - } - } - } - }, - ), - ); - }, + IconButton( + icon: const Icon(Symbols.close), + onPressed: provider.clearError, + color: Colors.red.shade700, ), ], ), - // Custom Repositories list - Column( - spacing: 4.0, - children: [ - MListHeader(title: 'Your Repositories'), - provider.repositories + ), + ), + ), + // Presets section + if (_presets.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.0, + children: [ + MListHeader(title: 'Preset'), + MListViewBuilder( + itemCount: _presets.length, + itemBuilder: (index) { + final preset = _presets[index]; + final isAdded = provider.repositories.any( + (repo) => repo.url == preset['url'], + ); + + return MListItemData( + title: preset['name']!, + subtitle: preset['description']!, + onTap: () {}, + suffix: Switch( + value: isAdded, + onChanged: (newValue) async { + if (newValue) { + // Add the preset + final repoProvider = context + .read(); + await repoProvider.addRepository( + preset['name']!, + preset['url']!, + ); + // Only proceed with modal and refresh if addition succeeded (no error) + if (repoProvider.error == null && + context.mounted) { + await _runRepositoryActionWithDialog( + context, + () async { + final apiService = context + .read(); + final appProvider = context + .read(); + + await apiService.clearRepositoryCache(); + await appProvider.refreshAll( + repositoriesProvider: repoProvider, + ); + }, + ); + } + } else { + // Remove the preset + Repository? addedRepo; + try { + addedRepo = provider.repositories + .firstWhere( + (repo) => repo.url == preset['url'], + ); + } catch (e) { + addedRepo = null; + } + if (addedRepo != null) { + final repoProvider = context + .read(); + repoProvider.deleteRepository(addedRepo.id); + // Only proceed with modal and refresh if deletion succeeded (no error) + if (repoProvider.error == null && + context.mounted) { + await _runRepositoryActionWithDialog( + context, + () async { + final apiService = context + .read(); + final appProvider = context + .read(); + + await apiService + .clearRepositoryCache(); + await appProvider.refreshAll( + repositoriesProvider: repoProvider, + ); + }, + ); + } + } + } + }, + ), + ); + }, + ), + ], + ), + // Custom Repositories list + Column( + spacing: 4.0, + children: [ + MListHeader(title: 'Your Repositories'), + provider.repositories + .where( + (repo) => !_presets.any( + (preset) => preset['url'] == repo.url, + ), + ) + .toList() + .isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.inbox, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'No custom repositories added', + style: Theme.of( + context, + ).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + 'Add a custom F-Droid repository to get started', + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ), + ) + : MListViewBuilder( + itemCount: provider.repositories + .where( + (repo) => !_presets.any( + (preset) => preset['url'] == repo.url, + ), + ) + .length, + itemBuilder: (index) { + final customRepos = provider.repositories .where( (repo) => !_presets.any( (preset) => preset['url'] == repo.url, ), ) - .toList() - .isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Symbols.inbox, - size: 64, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'No custom repositories added', - style: Theme.of( + .toList(); + final repo = customRepos[index]; + return MListItemData( + title: repo.name, + subtitle: repo.url, + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: repo.isEnabled, + onChanged: (_) async { + await _toggleRepositoryWithDialog( context, - ).textTheme.titleMedium, - ), - const SizedBox(height: 8), - Text( - 'Add a custom F-Droid repository to get started', - style: Theme.of( - context, - ).textTheme.bodySmall, - textAlign: TextAlign.center, - ), - ], - ), - ) - : MListViewBuilder( - itemCount: provider.repositories - .where( - (repo) => !_presets.any( - (preset) => preset['url'] == repo.url, + provider, + repo.id, + ); + }, + ), + PopupMenuButton( + onSelected: (value) async { + switch (value) { + case 'edit': + _RepositoryListItem( + repository: repo, + )._showEditRepositoryDialog( + context, + repo, + ); + break; + case 'delete': + _RepositoryListItem( + repository: repo, + )._showDeleteConfirmation( + context, + repo, + provider, + ); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + spacing: 16.0, + children: [ + Icon(Symbols.edit_rounded), + Text('Edit'), + ], + ), ), - ) - .length, - itemBuilder: (index) { - final customRepos = provider.repositories - .where( - (repo) => !_presets.any( - (preset) => - preset['url'] == repo.url, - ), - ) - .toList(); - final repo = customRepos[index]; - return MListItemData( - title: repo.name, - subtitle: repo.url, - suffix: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Switch( - value: repo.isEnabled, - onChanged: (_) async { - await _toggleRepositoryWithDialog( - context, - provider, - repo.id, - ); - }, - ), - PopupMenuButton( - onSelected: (value) async { - switch (value) { - case 'edit': - _RepositoryListItem( - repository: repo, - )._showEditRepositoryDialog( - context, - repo, - ); - break; - case 'delete': - _RepositoryListItem( - repository: repo, - )._showDeleteConfirmation( - context, - repo, - provider, - ); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'edit', - child: Row( - spacing: 16.0, - children: [ - Icon(Symbols.edit_rounded), - Text('Edit'), - ], - ), + PopupMenuItem( + value: 'delete', + child: Row( + spacing: 16.0, + children: [ + Icon( + Symbols.delete_rounded, + fill: 1, + color: Theme.of( + context, + ).colorScheme.error, ), - PopupMenuItem( - value: 'delete', - child: Row( - spacing: 16.0, - children: [ - Icon( - Symbols.delete_rounded, - fill: 1, - color: Theme.of( - context, - ).colorScheme.error, - ), - Text( - 'Delete', - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.error, - ), - ), - ], + Text( + 'Delete', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.error, ), ), ], ), - ], - ), - onTap: () {}, - ); - }, + ), + ], + ), + ], ), - ], - ), - ], - ), - ); - }, + onTap: () {}, + ); + }, + ), + ], + ), + ], ), - ), - ], + ); + }, ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddRepositoryDialog(context), @@ -627,7 +597,10 @@ Future _toggleRepositoryWithDialog( style: Theme.of(dialogContext).textTheme.bodyMedium, ), SizedBox(height: 16), - LinearProgressIndicator(value: value == 0.0 ? null : value), + LinearProgressIndicator( + value: value == 0.0 ? null : value, + year2023: false, + ), const SizedBox(height: 8), Text('$pct%', textAlign: TextAlign.right), const SizedBox(height: 32), @@ -698,7 +671,10 @@ Future _runRepositoryActionWithDialog( style: Theme.of(dialogContext).textTheme.bodyMedium, ), SizedBox(height: 16), - LinearProgressIndicator(value: value == 0.0 ? null : value), + LinearProgressIndicator( + value: value == 0.0 ? null : value, + year2023: false, + ), const SizedBox(height: 8), Text('$pct%', textAlign: TextAlign.right), const SizedBox(height: 32), @@ -724,12 +700,38 @@ class _AddRepositoryDialog extends StatefulWidget { class _AddRepositoryDialogState extends State<_AddRepositoryDialog> { late TextEditingController _nameController; late TextEditingController _urlController; + List> _presets = []; + final bool _showPresets = true; @override void initState() { super.initState(); _nameController = TextEditingController(); _urlController = TextEditingController(); + _loadPresets(); + } + + Future _loadPresets() async { + try { + final jsonString = await DefaultAssetBundle.of( + context, + ).loadString('assets/repositories.json'); + final jsonData = jsonDecode(jsonString); + final repos = (jsonData['repositories'] as List) + .map( + (e) => { + 'name': e['name'] as String, + 'url': e['url'] as String, + 'description': e['description'] as String? ?? '', + }, + ) + .toList(); + setState(() { + _presets = repos; + }); + } catch (e) { + debugPrint('Error loading presets: $e'); + } } @override diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 1210f46..be19222 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -207,7 +207,7 @@ class _SearchScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + CircularProgressIndicator(year2023: false), SizedBox(height: 16), Text(AppLocalizations.of(context)!.searching), ], diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 7c451f5..99953a1 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -75,271 +75,267 @@ class _SettingsScreenState extends State { return Consumer( builder: (context, settings, _) { return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar.medium( - title: Text(AppLocalizations.of(context)!.settings), - ), - SliverToBoxAdapter( - child: Column( - spacing: 4, - children: [ - MListHeader(title: 'Theme Mode'), - MRadioListView( - items: [ - MRadioListItemData( - title: 'Follow system theme', - subtitle: '', - value: ThemeMode.system, - ), - MRadioListItemData( - title: 'Light theme', - subtitle: '', - value: ThemeMode.light, - ), - MRadioListItemData( - title: 'Dark theme', - subtitle: '', - value: ThemeMode.dark, - ), - ], - groupValue: settings.themeMode, - onChanged: (mode) { - settings.setThemeMode(mode); - }, - ), - MListHeader(title: 'Theme Style'), - MRadioListView( - items: [ - MRadioListItemData( - title: 'Material style', - subtitle: '', - value: ThemeStyle.material, - ), - MRadioListItemData( - title: 'Florid style', - subtitle: '', - suffix: Container( - margin: const EdgeInsets.only(right: 8.0), - child: Material( - color: Theme.of(context).colorScheme.secondary, - borderRadius: BorderRadius.circular(99.0), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, - ), - child: Text( - 'Beta', - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSecondary, + appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + Column( + spacing: 4, + children: [ + MListHeader(title: 'Theme Mode'), + MRadioListView( + items: [ + MRadioListItemData( + title: 'Follow system theme', + subtitle: '', + value: ThemeMode.system, + ), + MRadioListItemData( + title: 'Light theme', + subtitle: '', + value: ThemeMode.light, + ), + MRadioListItemData( + title: 'Dark theme', + subtitle: '', + value: ThemeMode.dark, + ), + ], + groupValue: settings.themeMode, + onChanged: (mode) { + settings.setThemeMode(mode); + }, + ), + MListHeader(title: 'Theme Style'), + MRadioListView( + items: [ + MRadioListItemData( + title: 'Material style', + subtitle: '', + value: ThemeStyle.material, + ), + MRadioListItemData( + title: 'Florid style', + subtitle: '', + suffix: Container( + margin: const EdgeInsets.only(right: 8.0), + child: Material( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(99.0), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 2.0, + ), + child: Text( + 'Beta', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSecondary, + ), ), ), ), ), + value: ThemeStyle.florid, ), - value: ThemeStyle.florid, - ), - ], - groupValue: settings.themeStyle, - onChanged: (style) { - settings.setThemeStyle(style); - }, - ), - MListView( - items: [ - MListItemData( - leading: Icon(Symbols.feedback), - title: 'Fedback on Florid theme', - subtitle: - 'Help improve the Florid theme by providing feedback', - onTap: () { - launchUrl( - Uri.parse( - 'https://github.com/Nandanrmenon/florid/discussions/5', - ), - ); - }, - suffix: Icon(Symbols.open_in_new), - ), - ], - ), - ], - ), - ), - SliverToBoxAdapter( - child: Column( - spacing: 4, - children: [ - MListHeader(title: 'General Settings'), - MListView( - items: [ - MListItemData( - leading: Icon(Symbols.language), - title: 'App content language', - onTap: () => _showLanguageDialog(context, settings), - subtitle: SettingsProvider.getLocaleDisplayName( - settings.locale, + ], + groupValue: settings.themeStyle, + onChanged: (style) { + settings.setThemeStyle(style); + }, + ), + MListView( + items: [ + MListItemData( + leading: Icon(Symbols.feedback), + title: 'Fedback on Florid theme', + subtitle: + 'Help improve the Florid theme by providing feedback', + onTap: () { + launchUrl( + Uri.parse( + 'https://github.com/Nandanrmenon/florid/discussions/5', + ), + ); + }, + suffix: Icon(Symbols.open_in_new), ), - suffix: Icon(Symbols.chevron_right), - ), - MListItemData( - leading: Icon(Symbols.cloud), - title: 'Manage repositories', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const RepositoriesScreen(), - ), - ); - }, - subtitle: 'Add or remove F-Droid repositories', - suffix: Icon(Symbols.chevron_right), - ), - ], - ), - ], - ), - ), + ], + ), + ], + ), + Column( + spacing: 4, + children: [ + MListHeader(title: 'General Settings'), + MListView( + items: [ + MListItemData( + leading: Icon(Symbols.language), + title: 'App content language', + onTap: () => _showLanguageDialog(context, settings), + subtitle: SettingsProvider.getLocaleDisplayName( + settings.locale, + ), + suffix: Icon(Symbols.chevron_right), + ), + MListItemData( + leading: Icon(Symbols.cloud), + title: 'Manage repositories', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const RepositoriesScreen(), + ), + ); + }, + subtitle: 'Add or remove F-Droid repositories', + suffix: Icon(Symbols.chevron_right), + ), + ], + ), + ], + ), - SliverToBoxAdapter( - child: Column( - spacing: 4, - children: [ - MListHeader(title: 'Downloads & Storage'), - MListView( - items: [ - MListItemData( - title: 'Auto-install after download', - onTap: () { - settings.setAutoInstallApk( - !settings.autoInstallApk, - ); - }, - subtitle: - 'Install APKs automatically once download finishes', - suffix: Switch( - value: settings.autoInstallApk, - onChanged: (value) { - settings.setAutoInstallApk(value); + Column( + spacing: 4, + children: [ + MListHeader(title: 'Downloads & Storage'), + MListView( + items: [ + MListItemData( + title: 'Auto-install after download', + onTap: () { + settings.setAutoInstallApk( + !settings.autoInstallApk, + ); + }, + subtitle: + 'Install APKs automatically once download finishes', + suffix: Switch( + value: settings.autoInstallApk, + onChanged: (value) { + settings.setAutoInstallApk(value); + }, + ), + ), + MListItemData( + title: 'Delete APK after install', + onTap: () { + settings.setAutoInstallApk( + !settings.autoInstallApk, + ); + }, + subtitle: + 'Remove installer files after successful installation', + suffix: Switch( + value: settings.autoDeleteApk, + onChanged: (value) { + settings.setAutoDeleteApk(value); + }, + ), + ), + ], + ), + MListView( + items: [ + MListItemData( + leading: Icon(Symbols.cleaning_services), + title: 'Clear repository cache', + onTap: () { + _clearRepoCache(context); + }, + subtitle: + 'Refresh app list and metadata on next load', + ), + MListItemData( + leading: Icon(Symbols.delete_sweep), + title: 'Clear APK downloads', + onTap: () { + _clearRepoCache(context); + }, + subtitle: + 'Remove downloaded installer files from storage', + ), + MListItemData( + leading: Icon(Symbols.image_not_supported), + title: 'Clear image cache', + onTap: () { + _clearRepoCache(context); + }, + subtitle: 'Remove cached icons and screenshots', + ), + ], + ), + ], + ), + + Column( + spacing: 4, + children: [ + MListHeader(title: 'About'), + MListView( + items: [ + MListItemData( + leading: Icon(Symbols.info), + title: 'Version', + subtitle: _appVersion.isEmpty + ? 'Loading…' + : _appVersion, + onTap: () {}, + ), + MListItemData( + leading: Icon(Symbols.code_rounded), + title: 'Source code', + onTap: () async { + final url = Uri.parse( + 'https://github.com/Nandanrmenon/florid', + ); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } }, ), - ), - MListItemData( - title: 'Delete APK after install', - onTap: () { - settings.setAutoInstallApk( - !settings.autoInstallApk, - ); - }, - subtitle: - 'Remove installer files after successful installation', - suffix: Switch( - value: settings.autoDeleteApk, - onChanged: (value) { - settings.setAutoDeleteApk(value); + MListItemData( + leading: Icon(Symbols.bug_report_rounded), + title: 'Report an issue', + onTap: () async { + final url = Uri.parse( + 'https://github.com/Nandanrmenon/florid/issues/new?template=bug_report.md', + ); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } }, ), - ), - ], - ), - MListView( - items: [ - MListItemData( - leading: Icon(Symbols.cleaning_services), - title: 'Clear repository cache', - onTap: () { - _clearRepoCache(context); - }, - subtitle: - 'Refresh app list and metadata on next load', - ), - MListItemData( - leading: Icon(Symbols.delete_sweep), - title: 'Clear APK downloads', - onTap: () { - _clearApkDownloads(context); - }, - subtitle: - 'Remove downloaded installer files from storage', - ), - MListItemData( - leading: Icon(Symbols.image_not_supported), - title: 'Clear image cache', - onTap: () { - _clearImageCache(context); - }, - subtitle: 'Remove cached icons and screenshots', - ), - ], - ), - ], - ), + MListItemData( + leading: Icon(Symbols.share), + title: 'Share Florid', + onTap: () { + SharePlus.instance.share( + ShareParams( + title: 'Check out Florid!', + text: + 'A modern F-Droid client! https://github.com/Nandanrmenon/florid', + ), + ); + }, + ), + ], + ), + ], + ), + ], ), - - SliverToBoxAdapter( - child: Column( - spacing: 4, - children: [ - MListHeader(title: 'About'), - MListView( - items: [ - MListItemData( - leading: Icon(Symbols.info), - title: 'Version', - subtitle: _appVersion.isEmpty - ? 'Loading…' - : _appVersion, - onTap: () {}, - ), - MListItemData( - leading: Icon(Symbols.code_rounded), - title: 'Source code', - onTap: () async { - final url = Uri.parse( - 'https://github.com/Nandanrmenon/florid', - ); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } - }, - ), - MListItemData( - leading: Icon(Symbols.bug_report_rounded), - title: 'Report an issue', - onTap: () async { - final url = Uri.parse( - 'https://github.com/Nandanrmenon/florid/issues/new?template=bug_report.md', - ); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } - }, - ), - MListItemData( - leading: Icon(Symbols.share), - title: 'Share Florid', - onTap: () { - SharePlus.instance.share( - ShareParams( - title: 'Check out Florid!', - text: - 'A modern F-Droid client! https://github.com/Nandanrmenon/florid', - ), - ); - }, - ), - ], - ), - ], - ), - ), - SliverToBoxAdapter(child: const SizedBox(height: 32)), - ], + ), ), ); }, @@ -403,3 +399,21 @@ class _SettingsScreenState extends State { ); } } + +class _SectionHeader extends StatelessWidget { + final String label; + const _SectionHeader({required this.label}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), + child: Text( + label, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + ); + } +} diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index d319336..76255f6 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -224,7 +224,7 @@ class _UpdatesScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ - CircularProgressIndicator(), + CircularProgressIndicator(year2023: false), SizedBox(height: 12), Text('Loading repository…'), ], @@ -471,7 +471,7 @@ class _UpdatesScreenState extends State itemCount: allFDroidApps.length, itemBuilder: (context, index) { final app = allFDroidApps[index]; - final updatableApps = appProvider.getUpdatableApps(); + final installedApp = appProvider.getInstalledApp(app.packageName); final hasUpdate = updatableApps.any( (updateApp) => updateApp.packageName == app.packageName, ); diff --git a/lib/themes/app_themes.dart b/lib/themes/app_themes.dart index 8c44787..6c01c27 100644 --- a/lib/themes/app_themes.dart +++ b/lib/themes/app_themes.dart @@ -1,6 +1,5 @@ import 'package:florid/constants.dart'; import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; class AppThemes { // Material Theme (Original) @@ -33,8 +32,6 @@ class AppThemes { ), filled: true, ), - // ignore: deprecated_member_use - progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false), ); } @@ -67,8 +64,6 @@ class AppThemes { ), filled: true, ), - // ignore: deprecated_member_use - progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false), ); } @@ -96,15 +91,6 @@ class AppThemes { brightness: Brightness.light, ).onSurface, ), - backgroundColor: ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.light, - ).surface, - surfaceTintColor: ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.light, - ).surfaceContainerHigh, - elevation: 0, ), floatingActionButtonTheme: FloatingActionButtonThemeData( elevation: 0, @@ -154,29 +140,7 @@ class AppThemes { ), ), ), - iconButtonTheme: IconButtonThemeData( - style: IconButton.styleFrom( - backgroundColor: ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.light, - ).surfaceContainerLow, - ), - ), switchTheme: SwitchThemeData( - thumbIcon: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.contains(WidgetState.selected)) { - return Icon( - Symbols.check, - color: ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.light, - ).onPrimary, - ); - } - return const Icon(Symbols.close); - }), thumbColor: WidgetStateProperty.resolveWith(( Set states, ) { @@ -184,7 +148,7 @@ class AppThemes { return ColorScheme.fromSeed( seedColor: kAppColor, brightness: Brightness.light, - ).primary; + ).primaryContainer; } return null; // Use the default thumb color }), @@ -195,26 +159,14 @@ class AppThemes { return ColorScheme.fromSeed( seedColor: kAppColor, brightness: Brightness.light, - ).surface; + ).secondary; } - // Change the default track color to a custom color - return ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.light, - ).surfaceContainerLowest; + return null; // Use the default track color }), trackOutlineWidth: WidgetStateProperty.resolveWith(( Set states, ) { - return 2; - }), - trackOutlineColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - return ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.light, - ).surfaceContainerHighest; + return 1; }), ), navigationBarTheme: NavigationBarThemeData( @@ -305,8 +257,6 @@ class AppThemes { ).surfaceContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), - // ignore: deprecated_member_use - progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false), ); } @@ -333,14 +283,6 @@ class AppThemes { brightness: Brightness.dark, ).onSurface, ), - backgroundColor: ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.dark, - ).surface, - surfaceTintColor: ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.dark, - ).surfaceContainerHigh, ), floatingActionButtonTheme: FloatingActionButtonThemeData( elevation: 0, @@ -394,14 +336,6 @@ class AppThemes { ), ), switchTheme: SwitchThemeData( - thumbIcon: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.contains(WidgetState.selected)) { - return const Icon(Symbols.check); - } - return const Icon(Symbols.close); - }), thumbColor: WidgetStateProperty.resolveWith(( Set states, ) { @@ -420,25 +354,14 @@ class AppThemes { return ColorScheme.fromSeed( seedColor: kAppColor, brightness: Brightness.dark, - ).surface; + ).secondary; } - return ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.dark, - ).surfaceContainerLowest; + return null; // Use the default track color }), trackOutlineWidth: WidgetStateProperty.resolveWith(( Set states, ) { - return 2; - }), - trackOutlineColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - return ColorScheme.fromSeed( - seedColor: kAppColor, - brightness: Brightness.dark, - ).surfaceContainerHighest; + return 1; }), ), navigationBarTheme: NavigationBarThemeData( @@ -533,8 +456,6 @@ class AppThemes { ).surfaceContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), - // ignore: deprecated_member_use - progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false), ); } }