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:
+194
-321
@@ -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
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
+1150
-866
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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...'),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user