[imp]: Show repository availability and add localizations

This commit is contained in:
nahnah
2026-03-16 19:48:29 +00:00
parent adccf599ec
commit a66e5162c9
8 changed files with 187 additions and 27 deletions
+15 -1
View File
@@ -354,5 +354,19 @@
"auth_all_apps_desc": "Require authentication for all installations",
"auth_all_apps_w_anti_feat": "Apps with Anti-Features",
"auth_all_apps_w_anti_feat_desc": "Require authentication only for apps that have anti-features.",
"support_the_developer":"Support the Developer"
"support_the_developer":"Support the Developer",
"available_on_repository": "Available on: {repositoryName}",
"@available_on_repository": {
"description": "Label showing the primary repository name for an app.",
"placeholders": {
"repositoryName": {}
}
},
"also_available_from_repositories": "Also available from: {repositoryNames}",
"@also_available_from_repositories": {
"description": "Label listing additional repository names where the app is available.",
"placeholders": {
"repositoryNames": {}
}
}
}
+12
View File
@@ -1686,6 +1686,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Support the Developer'**
String get support_the_developer;
/// Label showing the primary repository name for an app.
///
/// In en, this message translates to:
/// **'Available on: {repositoryName}'**
String available_on_repository(Object repositoryName);
/// Label listing additional repository names where the app is available.
///
/// In en, this message translates to:
/// **'Also available from: {repositoryNames}'**
String also_available_from_repositories(Object repositoryNames);
}
class _AppLocalizationsDelegate
+10
View File
@@ -859,4 +859,14 @@ class AppLocalizationsCs extends AppLocalizations {
@override
String get support_the_developer => 'Support the Developer';
@override
String available_on_repository(Object repositoryName) {
return 'Available on: $repositoryName';
}
@override
String also_available_from_repositories(Object repositoryNames) {
return 'Also available from: $repositoryNames';
}
}
+10
View File
@@ -860,4 +860,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get support_the_developer => 'Support the Developer';
@override
String available_on_repository(Object repositoryName) {
return 'Available on: $repositoryName';
}
@override
String also_available_from_repositories(Object repositoryNames) {
return 'Also available from: $repositoryNames';
}
}
+10
View File
@@ -857,4 +857,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get support_the_developer => 'Support the Developer';
@override
String available_on_repository(Object repositoryName) {
return 'Available on: $repositoryName';
}
@override
String also_available_from_repositories(Object repositoryNames) {
return 'Also available from: $repositoryNames';
}
}
+10
View File
@@ -837,6 +837,16 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get support_the_developer => 'Support the Developer';
@override
String available_on_repository(Object repositoryName) {
return 'Available on: $repositoryName';
}
@override
String also_available_from_repositories(Object repositoryNames) {
return 'Also available from: $repositoryNames';
}
}
/// The translations for Chinese, as used in China (`zh_CN`).
+7 -10
View File
@@ -327,12 +327,9 @@ class AppProvider extends ChangeNotifier {
/// This is useful when displaying app details to show which repositories host the app
Future<FDroidApp> enrichAppWithRepositories(
FDroidApp app,
RepositoriesProvider? repositoriesProvider,
) async {
if (repositoriesProvider == null) {
return app;
}
RepositoriesProvider repositoriesProvider, {
bool allowNetworkFallback = true,
}) async {
try {
// Fast path: app already carries multi-repo info.
if (app.availableRepositories != null &&
@@ -410,8 +407,9 @@ class AppProvider extends ChangeNotifier {
// If DB misses but multiple repos are enabled, do a limited network fallback
// to recover selector visibility for first-time sync.
// Note: disable network fallback for lazy-loaded contexts like All Versions sheet
if (availableReposList.length <= 1 && enabledRepos.length > 1) {
if (allowNetworkFallback &&
availableReposList.length <= 1 &&
enabledRepos.length > 1) {
for (final repo in enabledRepos) {
if (repo.url == app.repositoryUrl) continue;
if (availableReposList.any((r) => r.url == repo.url)) continue;
@@ -419,8 +417,7 @@ class AppProvider extends ChangeNotifier {
final exists = await _apiService.repositoryContainsPackage(
app.packageName,
repo.url,
allowNetworkFallback:
false, // Use only database, no network fallback
allowNetworkFallback: true,
);
if (exists) {
+113 -16
View File
@@ -2574,6 +2574,31 @@ class AppExtraInfoSection extends StatefulWidget {
}
class _AppExtraInfoSectionState extends State<AppExtraInfoSection> {
String _normalizeRepoUrl(String url) {
var normalized = url.trim();
if (normalized.endsWith('/index-v2.json')) {
normalized = normalized.substring(
0,
normalized.length - '/index-v2.json'.length,
);
}
if (normalized.endsWith('/')) {
normalized = normalized.substring(0, normalized.length - 1);
}
return normalized;
}
String _normalizeRepoPath(String url) {
final normalized = _normalizeRepoUrl(url);
final uri = Uri.tryParse(normalized);
if (uri == null) return normalized;
var path = uri.path;
if (path.endsWith('/')) {
path = path.substring(0, path.length - 1);
}
return '${uri.host}$path';
}
List<MapEntry<String, String>> _buildDonationItems() {
final entries = <MapEntry<String, String>>[];
final seen = <String>{};
@@ -2763,8 +2788,64 @@ class _AppExtraInfoSectionState extends State<AppExtraInfoSection> {
@override
Widget build(BuildContext context) {
final donationItems = _buildDonationItems();
if (donationItems.isEmpty) {
return const SizedBox.shrink();
final repositoriesProvider = context.watch<RepositoriesProvider>();
if (repositoriesProvider.repositories.isEmpty &&
!repositoriesProvider.isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.read<RepositoriesProvider>().loadRepositories();
});
}
final configuredRepos = repositoriesProvider.repositories;
final availableRepos =
widget.app.availableRepositories ?? const <RepositorySource>[];
final currentRepoUrl = _normalizeRepoUrl(widget.app.repositoryUrl);
final currentRepoPath = _normalizeRepoPath(widget.app.repositoryUrl);
final currentRepoHost = Uri.tryParse(currentRepoUrl)?.host ?? '';
final currentConfiguredName = configuredRepos
.where(
(repo) =>
_normalizeRepoUrl(repo.url) == currentRepoUrl ||
_normalizeRepoPath(repo.url) == currentRepoPath ||
(currentRepoHost.isNotEmpty &&
(Uri.tryParse(_normalizeRepoUrl(repo.url))?.host ?? '') ==
currentRepoHost),
)
.map((repo) => repo.name)
.firstWhere((name) => name.isNotEmpty, orElse: () => '');
final currentAvailableRepo = availableRepos.firstWhere(
(repo) => _normalizeRepoUrl(repo.url) == currentRepoUrl,
orElse: () => RepositorySource(name: '', url: ''),
);
final currentRepositoryName = currentConfiguredName.isNotEmpty
? currentConfiguredName
: (currentAvailableRepo.name.isNotEmpty
? currentAvailableRepo.name
: AppLocalizations.of(context)!.unknown);
final otherRepositoryNames = <String>{};
for (final repo in availableRepos) {
final repoUrl = _normalizeRepoUrl(repo.url);
if (repoUrl == currentRepoUrl) continue;
final configuredName = configuredRepos
.where(
(r) =>
_normalizeRepoUrl(r.url) == repoUrl ||
_normalizeRepoPath(r.url) == _normalizeRepoPath(repo.url),
)
.map((r) => r.name)
.firstWhere((name) => name.isNotEmpty, orElse: () => '');
final resolvedName = configuredName.isNotEmpty
? configuredName
: repo.name;
if (resolvedName.isNotEmpty) {
otherRepositoryNames.add(resolvedName);
}
}
return Column(
@@ -2807,21 +2888,36 @@ class _AppExtraInfoSectionState extends State<AppExtraInfoSection> {
],
),
SizedBox(height: 16),
MListHeader(
icon: Symbols.volunteer_activism_rounded,
title: AppLocalizations.of(context)!.support_the_developer,
),
MListView(
items: [
for (final item in donationItems)
MListItemData(
title: item.key,
subtitle: item.value,
suffix: const Icon(Symbols.open_in_new_rounded),
onTap: () => _openDonateLink(context, item.key, item.value),
),
],
if (donationItems.isNotEmpty) ...[
MListHeader(
icon: Symbols.volunteer_activism_rounded,
title: AppLocalizations.of(context)!.support_the_developer,
),
MListView(
items: [
for (final item in donationItems)
MListItemData(
title: item.key,
subtitle: item.value,
suffix: const Icon(Symbols.open_in_new_rounded),
onTap: () => _openDonateLink(context, item.key, item.value),
),
],
),
],
SizedBox(height: 16),
Text(
AppLocalizations.of(
context,
)!.available_on_repository(currentRepositoryName),
style: Theme.of(context).textTheme.labelMedium,
),
if (otherRepositoryNames.isNotEmpty)
Text(
AppLocalizations.of(context)!.also_available_from_repositories(
otherRepositoryNames.join(', '),
),
),
],
);
}
@@ -2887,6 +2983,7 @@ class _AllVersionsSectionState extends State<_AllVersionsSection> {
final enrichedApp = await appProvider.enrichAppWithRepositories(
widget.app,
context.read<RepositoriesProvider>(),
allowNetworkFallback: false,
);
void addFallbackCurrentRepo() {