Merge pull request #22 from Nandanrmenon/copilot/allow-unstable-version-opt-in

Fix unstable version filtering and implement per-app preferences
This commit is contained in:
nahnah
2026-02-04 23:43:08 +00:00
committed by GitHub
22 changed files with 2351 additions and 2062 deletions
+194 -321
View File
@@ -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<AppLocalizations> delegate = _CrowdinLocalizationsDelegate();
static const LocalizationsDelegate<AppLocalizations> delegate =
_CrowdinLocalizationsDelegate();
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <
LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
static const List<Locale> supportedLocales = AppLocalizations.supportedLocales;
@override
String get app_name => Crowdin.getText(localeName, 'app_name') ?? _fallbackTexts.app_name;
static const List<Locale> 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<AppLocalizations> {
class _CrowdinLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _CrowdinLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) => AppLocalizations.delegate
.load(locale)
.then((fallback) => CrowdinLocalization(locale.toString(), fallback));
Future<AppLocalizations> 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;
+9 -3
View File
@@ -55,12 +55,18 @@ class MainApp extends StatelessWidget {
return service;
},
),
ChangeNotifierProxyProvider<FDroidApiService, AppProvider>(
ChangeNotifierProxyProvider2<FDroidApiService, SettingsProvider, AppProvider>(
create: (context) => AppProvider(
Provider.of<FDroidApiService>(context, listen: false),
Provider.of<SettingsProvider>(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,
+32 -1
View File
@@ -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()
+58 -19
View File
@@ -3,8 +3,10 @@ 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';
enum LoadingState { idle, loading, success, error }
@@ -25,8 +27,15 @@ class AppInfo {
class AppProvider extends ChangeNotifier {
final FDroidApiService _apiService;
SettingsProvider? _settingsProvider;
final AppPreferencesService _preferencesService = AppPreferencesService();
AppProvider(this._apiService);
AppProvider(this._apiService, [this._settingsProvider]);
void updateSettings(SettingsProvider settings) {
_settingsProvider = settings;
notifyListeners();
}
// Latest apps state
List<FDroidApp> _latestApps = [];
@@ -172,7 +181,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]!;
@@ -180,13 +189,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,
@@ -196,10 +205,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),
],
);
}
@@ -248,12 +254,13 @@ class AppProvider extends ChangeNotifier {
final availableReposList = <RepositorySource>[];
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),
);
}
}
@@ -265,19 +272,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;
}),
@@ -540,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');
@@ -549,7 +562,7 @@ class AppProvider extends ChangeNotifier {
}
/// Gets apps that have updates available
List<FDroidApp> getUpdatableApps() {
Future<List<FDroidApp>> getUpdatableApps() async {
if (_repository == null || _installedApps.isEmpty) {
return [];
}
@@ -561,14 +574,22 @@ class AppProvider extends ChangeNotifier {
final fdroidApp = _repository!.apps[installedApp.packageName];
if (fdroidApp == null) continue;
// Get the latest version based on per-app unstable preference
final includeUnstable = await _preferencesService.getIncludeUnstable(
installedApp.packageName,
);
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);
}
}
@@ -593,6 +614,24 @@ class AppProvider extends ChangeNotifier {
}
}
/// Gets the latest version for an app based on per-app unstable preference
Future<FDroidVersion?> 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<bool> 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<void> setIncludeUnstable(String packageName, bool include) async {
await _preferencesService.setIncludeUnstable(packageName, include);
notifyListeners();
}
/// Attempts to launch an installed app by package name
Future<bool> openInstalledApp(String packageName) async {
try {
+18
View File
@@ -7,7 +7,9 @@ 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';
enum DownloadStatus { idle, downloading, completed, error, cancelled }
@@ -77,6 +79,8 @@ class DownloadProvider extends ChangeNotifier {
SettingsProvider _settingsProvider;
final Map<String, DownloadInfo> _downloads = {};
final NotificationService _notificationService = NotificationService();
final InstallationTrackingService _trackingService = InstallationTrackingService();
final AppPreferencesService _preferencesService = AppPreferencesService();
DownloadProvider(this._apiService, this._settingsProvider) {
_initNotifications();
@@ -273,6 +277,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 +431,19 @@ 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);
await _preferencesService.removeIncludeUnstable(packageName);
} catch (e) {
throw Exception('Failed to uninstall app: $e');
}
}
/// Gets the repository URL that an app was downloaded from
Future<String?> getAppSource(String packageName) async {
return await _trackingService.getAppSource(packageName);
}
}
+1 -1
View File
@@ -89,7 +89,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];
}
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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<CategoriesScreen>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const CircularProgressIndicator(year2023: false),
const SizedBox(height: 16),
Text(AppLocalizations.of(context)!.loading_categories),
],
+1 -1
View File
@@ -80,7 +80,7 @@ class _CategoryAppsScreenState extends State<CategoryAppsScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
CircularProgressIndicator(year2023: false),
SizedBox(height: 16),
Text(AppLocalizations.of(context)!.loading_apps),
],
+59 -46
View File
@@ -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<FloridApp> {
body: IndexedStack(index: _currentIndex, children: _screens),
bottomNavigationBar: Consumer<AppProvider>(
builder: (context, appProvider, child) {
final updatableAppsCount = appProvider.getUpdatableApps().length;
return FutureBuilder<List<FDroidApp>>(
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,
);
},
),
+9 -8
View File
@@ -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<HomeScreen>
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<HomeScreen>
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<HomeScreen>
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<HomeScreen>
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(
+2 -1
View File
@@ -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<LatestScreen>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
CircularProgressIndicator(year2023: false),
SizedBox(height: 16),
Text(AppLocalizations.of(context)!.loading_latest_apps),
],
+60 -65
View File
@@ -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<LibraryScreen>
@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<String>(
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<String>(
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),
),
],
+5 -6
View File
@@ -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<OnboardingScreen> {
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),
),
),
+1 -1
View File
@@ -65,7 +65,7 @@ class _RecentlyUpdatedScreenState extends State<RecentlyUpdatedScreen>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
CircularProgressIndicator(year2023: false),
SizedBox(height: 16),
Text('Loading recently updated apps...'),
],
+287 -285
View File
@@ -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<RepositoriesScreen> {
@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<RepositoriesProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
appBar: AppBar(title: const Text('Manage Repositories')),
body: Consumer<RepositoriesProvider>(
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<RepositoriesProvider>();
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<FDroidApiService>();
final appProvider = context
.read<AppProvider>();
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<RepositoriesProvider>();
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<FDroidApiService>();
final appProvider = context
.read<AppProvider>();
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<RepositoriesProvider>();
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<FDroidApiService>();
final appProvider = context
.read<AppProvider>();
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<RepositoriesProvider>();
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<FDroidApiService>();
final appProvider = context
.read<AppProvider>();
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<String>(
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<String>(
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<void> _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<void> _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<Map<String, String>> _presets = [];
final bool _showPresets = true;
@override
void initState() {
super.initState();
_nameController = TextEditingController();
_urlController = TextEditingController();
_loadPresets();
}
Future<void> _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
+1 -1
View File
@@ -207,7 +207,7 @@ class _SearchScreenState extends State<SearchScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
CircularProgressIndicator(year2023: false),
SizedBox(height: 16),
Text(AppLocalizations.of(context)!.searching),
],
+267 -253
View File
@@ -75,271 +75,267 @@ class _SettingsScreenState extends State<SettingsScreen> {
return Consumer<SettingsProvider>(
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<ThemeMode>(
title: 'Follow system theme',
subtitle: '',
value: ThemeMode.system,
),
MRadioListItemData<ThemeMode>(
title: 'Light theme',
subtitle: '',
value: ThemeMode.light,
),
MRadioListItemData<ThemeMode>(
title: 'Dark theme',
subtitle: '',
value: ThemeMode.dark,
),
],
groupValue: settings.themeMode,
onChanged: (mode) {
settings.setThemeMode(mode);
},
),
MListHeader(title: 'Theme Style'),
MRadioListView(
items: [
MRadioListItemData<ThemeStyle>(
title: 'Material style',
subtitle: '',
value: ThemeStyle.material,
),
MRadioListItemData<ThemeStyle>(
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<ThemeMode>(
title: 'Follow system theme',
subtitle: '',
value: ThemeMode.system,
),
MRadioListItemData<ThemeMode>(
title: 'Light theme',
subtitle: '',
value: ThemeMode.light,
),
MRadioListItemData<ThemeMode>(
title: 'Dark theme',
subtitle: '',
value: ThemeMode.dark,
),
],
groupValue: settings.themeMode,
onChanged: (mode) {
settings.setThemeMode(mode);
},
),
MListHeader(title: 'Theme Style'),
MRadioListView(
items: [
MRadioListItemData<ThemeStyle>(
title: 'Material style',
subtitle: '',
value: ThemeStyle.material,
),
MRadioListItemData<ThemeStyle>(
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<SettingsScreen> {
);
}
}
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),
),
);
}
}
+103 -97
View File
@@ -98,108 +98,113 @@ class _UpdatesScreenState extends State<UpdatesScreen>
);
}
final updatableApps = repositoryLoaded
? appProvider.getUpdatableApps()
: <FDroidApp>[];
return FutureBuilder<List<FDroidApp>>(
future: repositoryLoaded
? appProvider.getUpdatableApps()
: Future.value(<FDroidApp>[]),
builder: (context, snapshot) {
final updatableApps = snapshot.data ?? <FDroidApp>[];
// 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<String>(
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<String>(
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),
],
),
),
);
},
);
},
);
@@ -219,7 +224,7 @@ class _UpdatesScreenState extends State<UpdatesScreen>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
CircularProgressIndicator(year2023: false),
SizedBox(height: 12),
Text('Loading repository…'),
],
@@ -430,6 +435,7 @@ class _UpdatesScreenState extends State<UpdatesScreen>
BuildContext context,
AppProvider appProvider,
List<FDroidApp> allFDroidApps,
List<FDroidApp> updatableApps,
) {
if (allFDroidApps.isEmpty) {
return SingleChildScrollView(
@@ -465,7 +471,7 @@ class _UpdatesScreenState extends State<UpdatesScreen>
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,
);
+44
View File
@@ -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<bool> 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<void> 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<void> removeIncludeUnstable(String packageName) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('$_unstableKeyPrefix$packageName');
}
/// Gets all apps that have unstable version preferences set
Future<Set<String>> 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<void> cleanupUninstalledApps(Set<String> installedPackages) async {
final appsWithPrefs = await getAllAppsWithUnstablePreference();
for (final packageName in appsWithPrefs) {
if (!installedPackages.contains(packageName)) {
await removeIncludeUnstable(packageName);
}
}
}
}
@@ -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<void> 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<String?> 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<void> removeAppSource(String packageName) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('$_keyPrefix$packageName');
}
/// Gets all tracked app sources
Future<Map<String, String>> getAllAppSources() async {
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys().where((key) => key.startsWith(_keyPrefix));
final Map<String, String> 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;
}
}
+7 -86
View File
@@ -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<Icon?>((
Set<WidgetState> 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<Color?>((
Set<WidgetState> 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<double?>((
Set<WidgetState> states,
) {
return 2;
}),
trackOutlineColor: WidgetStateProperty.resolveWith<Color?>((
Set<WidgetState> 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<Icon?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return const Icon(Symbols.check);
}
return const Icon(Symbols.close);
}),
thumbColor: WidgetStateProperty.resolveWith<Color?>((
Set<WidgetState> 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<double?>((
Set<WidgetState> states,
) {
return 2;
}),
trackOutlineColor: WidgetStateProperty.resolveWith<Color?>((
Set<WidgetState> 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),
);
}
}