feat: Add full Paru support in catalog, search, settings, and installer
This commit is contained in:
@@ -16,6 +16,7 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
String _preferredMirrorCountry = '';
|
String _preferredMirrorCountry = '';
|
||||||
String _windowTheme = 'auto';
|
String _windowTheme = 'auto';
|
||||||
bool _hasCompletedSetupWizard = false;
|
bool _hasCompletedSetupWizard = false;
|
||||||
|
String _preferredAurHelper = 'paru';
|
||||||
|
|
||||||
bool get createSnapshotBeforeUpdate => _createSnapshotBeforeUpdate;
|
bool get createSnapshotBeforeUpdate => _createSnapshotBeforeUpdate;
|
||||||
bool get usePolkit => _usePolkit;
|
bool get usePolkit => _usePolkit;
|
||||||
@@ -23,6 +24,7 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
String get preferredMirrorCountry => _preferredMirrorCountry;
|
String get preferredMirrorCountry => _preferredMirrorCountry;
|
||||||
String get windowTheme => _windowTheme;
|
String get windowTheme => _windowTheme;
|
||||||
bool get hasCompletedSetupWizard => _hasCompletedSetupWizard;
|
bool get hasCompletedSetupWizard => _hasCompletedSetupWizard;
|
||||||
|
String get preferredAurHelper => _preferredAurHelper;
|
||||||
|
|
||||||
static String detectDesktopEnvironmentName() {
|
static String detectDesktopEnvironmentName() {
|
||||||
final xdg = (Platform.environment['XDG_CURRENT_DESKTOP'] ?? '').toUpperCase();
|
final xdg = (Platform.environment['XDG_CURRENT_DESKTOP'] ?? '').toUpperCase();
|
||||||
@@ -85,6 +87,7 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
_preferredMirrorCountry = data['preferredMirrorCountry'] as String? ?? '';
|
_preferredMirrorCountry = data['preferredMirrorCountry'] as String? ?? '';
|
||||||
_windowTheme = data['windowTheme'] as String? ?? 'auto';
|
_windowTheme = data['windowTheme'] as String? ?? 'auto';
|
||||||
_hasCompletedSetupWizard = data['hasCompletedSetupWizard'] as bool? ?? false;
|
_hasCompletedSetupWizard = data['hasCompletedSetupWizard'] as bool? ?? false;
|
||||||
|
_preferredAurHelper = data['preferredAurHelper'] as String? ?? 'paru';
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -99,11 +102,19 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
'preferredMirrorCountry': _preferredMirrorCountry,
|
'preferredMirrorCountry': _preferredMirrorCountry,
|
||||||
'windowTheme': _windowTheme,
|
'windowTheme': _windowTheme,
|
||||||
'hasCompletedSetupWizard': _hasCompletedSetupWizard,
|
'hasCompletedSetupWizard': _hasCompletedSetupWizard,
|
||||||
|
'preferredAurHelper': _preferredAurHelper,
|
||||||
};
|
};
|
||||||
await file.writeAsString(const JsonEncoder.withIndent(' ').convert(data));
|
await file.writeAsString(const JsonEncoder.withIndent(' ').convert(data));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setPreferredAurHelper(String value) async {
|
||||||
|
if (_preferredAurHelper == value) return;
|
||||||
|
_preferredAurHelper = value;
|
||||||
|
notifyListeners();
|
||||||
|
await _saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setHasCompletedSetupWizard(bool value) async {
|
Future<void> setHasCompletedSetupWizard(bool value) async {
|
||||||
if (_hasCompletedSetupWizard == value) return;
|
if (_hasCompletedSetupWizard == value) return;
|
||||||
_hasCompletedSetupWizard = value;
|
_hasCompletedSetupWizard = value;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import '../rust/api/packages.dart';
|
import '../rust/api/packages.dart';
|
||||||
|
import 'app_settings_service.dart';
|
||||||
|
|
||||||
class AurPackage {
|
class AurPackage {
|
||||||
final String name;
|
final String name;
|
||||||
@@ -131,37 +132,40 @@ class AurPackageDetails {
|
|||||||
class AurService {
|
class AurService {
|
||||||
static String? _cachedHelper;
|
static String? _cachedHelper;
|
||||||
|
|
||||||
/// Определение доступного AUR-хелпера (предпочтение paru, затем yay)
|
static void resetCache() {
|
||||||
static Future<String?> getHelper() async {
|
_cachedHelper = null;
|
||||||
if (_cachedHelper != null) return _cachedHelper;
|
}
|
||||||
|
|
||||||
|
/// Определение доступного и работающего AUR-хелпера (учитывает preferred helper: paru или yay)
|
||||||
|
static Future<String?> getHelper({bool forceRefresh = false}) async {
|
||||||
|
if (!forceRefresh && _cachedHelper != null) return _cachedHelper;
|
||||||
|
|
||||||
|
final preferred = AppSettingsService.instance.preferredAurHelper;
|
||||||
|
final candidates = preferred == 'yay' ? ['yay', 'paru'] : ['paru', 'yay'];
|
||||||
|
|
||||||
|
for (final helper in candidates) {
|
||||||
try {
|
try {
|
||||||
final paruRes = await Process.run('which', ['paru']);
|
final res = await Process.run(helper, ['--version']);
|
||||||
if (paruRes.exitCode == 0 && paruRes.stdout.toString().trim().isNotEmpty) {
|
if (res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty) {
|
||||||
_cachedHelper = 'paru';
|
_cachedHelper = helper;
|
||||||
return _cachedHelper;
|
return _cachedHelper;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
try {
|
|
||||||
final yayRes = await Process.run('which', ['yay']);
|
|
||||||
if (yayRes.exitCode == 0 && yayRes.stdout.toString().trim().isNotEmpty) {
|
|
||||||
_cachedHelper = 'yay';
|
|
||||||
return _cachedHelper;
|
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Поиск пакетов в AUR через официальный RPC API v5
|
/// Поиск пакетов в AUR через официальный RPC API v5 с приоритизацией точных совпадений и хелперов
|
||||||
static Future<List<AurPackage>> search(String query) async {
|
static Future<List<AurPackage>> search(String query) async {
|
||||||
if (query.trim().isEmpty) return [];
|
final clean = query.trim();
|
||||||
|
if (clean.isEmpty) return [];
|
||||||
|
|
||||||
|
Future<List<AurPackage>> fetchQuery(String url) async {
|
||||||
try {
|
try {
|
||||||
final client = HttpClient();
|
final client = HttpClient();
|
||||||
client.connectionTimeout = const Duration(seconds: 5);
|
client.connectionTimeout = const Duration(seconds: 5);
|
||||||
final uri = Uri.parse('https://aur.archlinux.org/rpc/v5/search/${Uri.encodeComponent(query.trim())}');
|
final uri = Uri.parse(url);
|
||||||
final req = await client.getUrl(uri);
|
final req = await client.getUrl(uri);
|
||||||
final res = await req.close();
|
final res = await req.close();
|
||||||
|
|
||||||
@@ -169,39 +173,71 @@ class AurService {
|
|||||||
final body = await res.transform(utf8.decoder).join();
|
final body = await res.transform(utf8.decoder).join();
|
||||||
final Map<String, dynamic> data = jsonDecode(body);
|
final Map<String, dynamic> data = jsonDecode(body);
|
||||||
final results = data['results'] as List<dynamic>? ?? [];
|
final results = data['results'] as List<dynamic>? ?? [];
|
||||||
|
return results
|
||||||
final list = results
|
|
||||||
.map((item) => AurPackage.fromJson(item as Map<String, dynamic>))
|
.map((item) => AurPackage.fromJson(item as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Сортируем по количеству голосов
|
|
||||||
list.sort((a, b) => b.numVotes.compareTo(a.numVotes));
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Fallback через curl
|
|
||||||
try {
|
try {
|
||||||
final curlRes = await Process.run('curl', [
|
final curlRes = await Process.run('curl', [
|
||||||
'-s',
|
'-s',
|
||||||
'--max-time',
|
'--max-time',
|
||||||
'5',
|
'5',
|
||||||
'https://aur.archlinux.org/rpc/v5/search/${Uri.encodeComponent(query.trim())}',
|
url,
|
||||||
]);
|
]);
|
||||||
if (curlRes.exitCode == 0) {
|
if (curlRes.exitCode == 0) {
|
||||||
final Map<String, dynamic> data = jsonDecode(curlRes.stdout.toString());
|
final Map<String, dynamic> data = jsonDecode(curlRes.stdout.toString());
|
||||||
final results = data['results'] as List<dynamic>? ?? [];
|
final results = data['results'] as List<dynamic>? ?? [];
|
||||||
final list = results
|
return results
|
||||||
.map((item) => AurPackage.fromJson(item as Map<String, dynamic>))
|
.map((item) => AurPackage.fromJson(item as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
list.sort((a, b) => b.numVotes.compareTo(a.numVotes));
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final encoded = Uri.encodeComponent(clean);
|
||||||
|
final results = await Future.wait([
|
||||||
|
fetchQuery('https://aur.archlinux.org/rpc/v5/search/$encoded?by=name'),
|
||||||
|
fetchQuery('https://aur.archlinux.org/rpc/v5/search/$encoded'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final Map<String, AurPackage> map = {};
|
||||||
|
for (final list in results) {
|
||||||
|
for (final item in list) {
|
||||||
|
map[item.name] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final list = map.values.toList();
|
||||||
|
final lower = clean.toLowerCase();
|
||||||
|
|
||||||
|
list.sort((a, b) {
|
||||||
|
final aName = a.name.toLowerCase();
|
||||||
|
final bName = b.name.toLowerCase();
|
||||||
|
|
||||||
|
// Точное совпадение имени на самый верх
|
||||||
|
if (aName == lower && bName != lower) return -1;
|
||||||
|
if (bName == lower && aName != lower) return 1;
|
||||||
|
|
||||||
|
// Префиксное совпадение
|
||||||
|
final aStarts = aName.startsWith(lower);
|
||||||
|
final bStarts = bName.startsWith(lower);
|
||||||
|
if (aStarts && !bStarts) return -1;
|
||||||
|
if (bStarts && !aStarts) return 1;
|
||||||
|
|
||||||
|
// Сортировка по голосам
|
||||||
|
final voteCmp = b.numVotes.compareTo(a.numVotes);
|
||||||
|
if (voteCmp != 0) return voteCmp;
|
||||||
|
|
||||||
|
// Сортировка по популярности
|
||||||
|
return b.popularity.compareTo(a.popularity);
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
/// Получение детальной информации о пакете AUR через RPC v5 info endpoint
|
/// Получение детальной информации о пакете AUR через RPC v5 info endpoint
|
||||||
static Future<AurPackageDetails?> getPackageInfo(String packageName) async {
|
static Future<AurPackageDetails?> getPackageInfo(String packageName) async {
|
||||||
final clean = packageName.trim();
|
final clean = packageName.trim();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'admin_service.dart';
|
|||||||
import 'app_settings_service.dart';
|
import 'app_settings_service.dart';
|
||||||
import 'aur_service.dart';
|
import 'aur_service.dart';
|
||||||
import 'catalog_service.dart';
|
import 'catalog_service.dart';
|
||||||
|
import 'component_installer_service.dart';
|
||||||
import 'snapshot_service.dart';
|
import 'snapshot_service.dart';
|
||||||
|
|
||||||
class BatchInstallProgress {
|
class BatchInstallProgress {
|
||||||
@@ -186,10 +187,36 @@ class BatchInstallService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 2: AUR пакеты (через yay или paru)
|
// Шаг 2: AUR пакеты (через paru или yay)
|
||||||
if (aurList.isNotEmpty) {
|
if (aurList.isNotEmpty) {
|
||||||
currentStage++;
|
currentStage++;
|
||||||
final helper = await AurService.getHelper() ?? 'yay';
|
String? helper = await AurService.getHelper();
|
||||||
|
|
||||||
|
// Если в очереди есть paru / paru-bin и хелпера ещё нет: устанавливаем Paru напрямую
|
||||||
|
final paruApps = aurList.where((a) => a.id == 'paru' || a.id == 'paru-bin').toList();
|
||||||
|
if (paruApps.isNotEmpty && helper == null) {
|
||||||
|
yield BatchInstallProgress(
|
||||||
|
currentStep: '[$currentStage/$totalStages] Сборка и установка Paru через makepkg...',
|
||||||
|
percentage: (currentStage - 0.5) / (totalStages + 0.2),
|
||||||
|
logLine: '🚀 [Paru] Первичная сборка и установка Paru на систему...',
|
||||||
|
);
|
||||||
|
await for (final line in ComponentInstallerService.installParu(password)) {
|
||||||
|
yield BatchInstallProgress(
|
||||||
|
currentStep: '[$currentStage/$totalStages] Установка Paru...',
|
||||||
|
percentage: (currentStage - 0.3) / (totalStages + 0.2),
|
||||||
|
logLine: line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final p in paruApps) {
|
||||||
|
p.isInstalled = true;
|
||||||
|
_queue.remove(p.uniqueKey);
|
||||||
|
aurList.remove(p);
|
||||||
|
}
|
||||||
|
helper = await AurService.getHelper(forceRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aurList.isNotEmpty) {
|
||||||
|
helper ??= 'paru';
|
||||||
final pkgIds = aurList.map((a) => a.id).toList();
|
final pkgIds = aurList.map((a) => a.id).toList();
|
||||||
final pkgLabels = aurList.map((a) => a.name == a.id ? a.id : '${a.name} (${a.id})').toList();
|
final pkgLabels = aurList.map((a) => a.name == a.id ? a.id : '${a.name} (${a.id})').toList();
|
||||||
|
|
||||||
@@ -259,6 +286,7 @@ class BatchInstallService extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Шаг 3: Flatpak пакеты
|
// Шаг 3: Flatpak пакеты
|
||||||
if (flatpakList.isNotEmpty) {
|
if (flatpakList.isNotEmpty) {
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ class CatalogService {
|
|||||||
'com.github.tchx84.Flatseal': 'https://dl.flathub.org/repo/appstream/x86_64/icons/128x128/com.github.tchx84.Flatseal.png',
|
'com.github.tchx84.Flatseal': 'https://dl.flathub.org/repo/appstream/x86_64/icons/128x128/com.github.tchx84.Flatseal.png',
|
||||||
'neovim': 'https://dl.flathub.org/repo/appstream/x86_64/icons/128x128/io.neovim.nvim.png',
|
'neovim': 'https://dl.flathub.org/repo/appstream/x86_64/icons/128x128/io.neovim.nvim.png',
|
||||||
'io.neovim.nvim': 'https://dl.flathub.org/repo/appstream/x86_64/icons/128x128/io.neovim.nvim.png',
|
'io.neovim.nvim': 'https://dl.flathub.org/repo/appstream/x86_64/icons/128x128/io.neovim.nvim.png',
|
||||||
|
'paru': 'https://raw.githubusercontent.com/Morganamilo/paru/master/assets/logo.png',
|
||||||
|
'paru-bin': 'https://raw.githubusercontent.com/Morganamilo/paru/master/assets/logo.png',
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Получение кураторских подборок для витрины App Store
|
/// Получение кураторских подборок для витрины App Store
|
||||||
@@ -268,6 +270,30 @@ class CatalogService {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
'Утилиты и Система': [
|
'Утилиты и Система': [
|
||||||
|
CatalogApp(
|
||||||
|
id: 'paru',
|
||||||
|
name: 'Paru',
|
||||||
|
summary: 'Современный и быстрый AUR-хелпер на Rust для сборки и управления пакетами Arch Linux.',
|
||||||
|
category: 'Утилиты',
|
||||||
|
backend: BackendType.aur,
|
||||||
|
iconUrl: _knownIconUrls['paru'],
|
||||||
|
fallbackIcon: CupertinoIcons.flame_fill,
|
||||||
|
accentColor: const Color(0xFFFF9500),
|
||||||
|
rating: 4.95,
|
||||||
|
votes: 1256,
|
||||||
|
),
|
||||||
|
CatalogApp(
|
||||||
|
id: 'paru-bin',
|
||||||
|
name: 'Paru (Binary)',
|
||||||
|
summary: 'Прекомпилированная бинарная версия AUR-хелпера Paru для быстрой установки без сборки.',
|
||||||
|
category: 'Утилиты',
|
||||||
|
backend: BackendType.aur,
|
||||||
|
iconUrl: _knownIconUrls['paru-bin'],
|
||||||
|
fallbackIcon: CupertinoIcons.flame_fill,
|
||||||
|
accentColor: const Color(0xFFFF9500),
|
||||||
|
rating: 4.9,
|
||||||
|
votes: 324,
|
||||||
|
),
|
||||||
CatalogApp(
|
CatalogApp(
|
||||||
id: 'com.usebottles.bottles',
|
id: 'com.usebottles.bottles',
|
||||||
name: 'Bottles',
|
name: 'Bottles',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'admin_service.dart';
|
import 'admin_service.dart';
|
||||||
|
import 'aur_service.dart';
|
||||||
|
|
||||||
class SystemComponentStatus {
|
class SystemComponentStatus {
|
||||||
final bool hasFlatpak;
|
final bool hasFlatpak;
|
||||||
@@ -40,13 +41,13 @@ class ComponentInstallerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final res = await Process.run('which', ['paru']);
|
final res = await Process.run('paru', ['--version']);
|
||||||
hasParu = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty;
|
hasParu = res.exitCode == 0 && res.stdout.toString().toLowerCase().contains('paru');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final res = await Process.run('which', ['yay']);
|
final res = await Process.run('yay', ['--version']);
|
||||||
hasYay = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty;
|
hasYay = res.exitCode == 0 && res.stdout.toString().toLowerCase().contains('yay');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
return SystemComponentStatus(
|
return SystemComponentStatus(
|
||||||
@@ -161,10 +162,29 @@ class ComponentInstallerService {
|
|||||||
|
|
||||||
final exitCode = await proc.exitCode;
|
final exitCode = await proc.exitCode;
|
||||||
if (exitCode == 0) {
|
if (exitCode == 0) {
|
||||||
|
AurService.resetCache();
|
||||||
yield '🎉 [Paru] Paru успешно установлен и готов к работе!';
|
yield '🎉 [Paru] Paru успешно установлен и готов к работе!';
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
yield '⚠️ [Paru] Не удалось установить через yay (код $exitCode), переключаемся на ручную сборку...';
|
yield '⚠️ [Paru] Не удалось установить через yay (код $exitCode), пробуем сборку из исходников paru...';
|
||||||
|
final procSrc = await Process.start(
|
||||||
|
'yay',
|
||||||
|
['-S', '--noconfirm', 'paru'],
|
||||||
|
environment: {'LC_ALL': 'C'},
|
||||||
|
);
|
||||||
|
final srcCtrl = StreamController<String>();
|
||||||
|
procSrc.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((l) => srcCtrl.add(l));
|
||||||
|
procSrc.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((l) => srcCtrl.add(l));
|
||||||
|
procSrc.exitCode.then((_) => Future.delayed(const Duration(milliseconds: 50), () => srcCtrl.close()));
|
||||||
|
await for (final l in srcCtrl.stream) {
|
||||||
|
yield l;
|
||||||
|
}
|
||||||
|
final srcExit = await procSrc.exitCode;
|
||||||
|
if (srcExit == 0) {
|
||||||
|
AurService.resetCache();
|
||||||
|
yield '🎉 [Paru] Paru успешно скомпилирован и установлен!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
|||||||
bool _isSearching = false;
|
bool _isSearching = false;
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
String _selectedSource = 'Все';
|
String _selectedSource = 'Все';
|
||||||
|
String _selectedStorefrontSource = 'Все';
|
||||||
String? _aurHelper;
|
String? _aurHelper;
|
||||||
|
|
||||||
final List<CatalogApp> _featuredApps = CatalogService.getFeaturedApps();
|
final List<CatalogApp> _featuredApps = CatalogService.getFeaturedApps();
|
||||||
@@ -184,24 +185,33 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
|||||||
_buildSearchResultsGrid(macosTheme),
|
_buildSearchResultsGrid(macosTheme),
|
||||||
] else ...[
|
] else ...[
|
||||||
// Mode 2: App Store Storefront
|
// Mode 2: App Store Storefront
|
||||||
|
_buildStorefrontSourceFilterBar(macosTheme),
|
||||||
|
const SizedBox(height: 18),
|
||||||
_buildHeroBanner(macosTheme),
|
_buildHeroBanner(macosTheme),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
// Section 1: Featured Apps
|
// Section 1: Featured Apps
|
||||||
|
...(() {
|
||||||
|
final featured = _filterStorefrontApps(_featuredApps);
|
||||||
|
if (featured.isEmpty) return <Widget>[];
|
||||||
|
return <Widget>[
|
||||||
_buildSectionHeader('ИЗБРАННОЕ И РЕКОМЕНДАЦИИ', 'Популярные приложения с быстрой установкой'),
|
_buildSectionHeader('ИЗБРАННОЕ И РЕКОМЕНДАЦИИ', 'Популярные приложения с быстрой установкой'),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildAppsGrid(_featuredApps, macosTheme),
|
_buildAppsGrid(featured, macosTheme),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
];
|
||||||
|
})(),
|
||||||
|
|
||||||
// Section 2: Curated Categories
|
// Section 2: Curated Categories
|
||||||
..._curatedCategories.entries.map((cat) {
|
..._curatedCategories.entries.map((cat) {
|
||||||
|
final filteredList = _filterStorefrontApps(cat.value);
|
||||||
|
if (filteredList.isEmpty) return const SizedBox.shrink();
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader(cat.key.toUpperCase(), 'Подборка проверенного ПО'),
|
_buildSectionHeader(cat.key.toUpperCase(), 'Подборка проверенного ПО'),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildAppsGrid(cat.value, macosTheme),
|
_buildAppsGrid(filteredList, macosTheme),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -226,8 +236,88 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<CatalogApp> _filterStorefrontApps(List<CatalogApp> apps) {
|
||||||
|
if (_selectedStorefrontSource == 'Все') return apps;
|
||||||
|
if (_selectedStorefrontSource == 'Pacman') {
|
||||||
|
return apps.where((a) => a.backend == BackendType.pacman).toList();
|
||||||
|
}
|
||||||
|
if (_selectedStorefrontSource == 'AUR') {
|
||||||
|
return apps.where((a) => a.backend == BackendType.aur).toList();
|
||||||
|
}
|
||||||
|
if (_selectedStorefrontSource == 'Flatpak') {
|
||||||
|
return apps.where((a) => a.backend == BackendType.flatpak).toList();
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStorefrontSourceFilterBar(MacosThemeData macosTheme) {
|
||||||
|
final aurLabel = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR (Paru)';
|
||||||
|
final filters = ['Все', 'Pacman', 'AUR', 'Flatpak'];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
...filters.map((source) {
|
||||||
|
final isSelected = _selectedStorefrontSource == source;
|
||||||
|
final displayLabel = source == 'AUR' ? aurLabel : source;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: CupertinoButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
onPressed: () => setState(() => _selectedStorefrontSource = source),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF0A84FF)
|
||||||
|
: macosTheme.canvasColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF0A84FF)
|
||||||
|
: macosTheme.dividerColor,
|
||||||
|
width: 0.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
displayLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected ? CupertinoColors.white : CupertinoColors.systemGrey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFF9500).withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: const Color(0xFFFF9500).withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const MacosIcon(CupertinoIcons.flame_fill, size: 12, color: Color(0xFFFF9500)),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
'Хелпер: ${_aurHelper ?? "Paru (активен)"}',
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFFFF9500), fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSearchHeader(MacosThemeData macosTheme) {
|
Widget _buildSearchHeader(MacosThemeData macosTheme) {
|
||||||
final filtered = _getFilteredSearchResults();
|
final filtered = _getFilteredSearchResults();
|
||||||
|
final aurLabel = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR (Paru)';
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -282,8 +372,8 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
source == 'AUR' && _aurHelper != null
|
source == 'AUR'
|
||||||
? 'AUR ($_aurHelper) ($count)'
|
? '$aurLabel ($count)'
|
||||||
: '$source ($count)',
|
: '$source ($count)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11.5,
|
fontSize: 11.5,
|
||||||
@@ -634,7 +724,7 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
|||||||
case BackendType.aur:
|
case BackendType.aur:
|
||||||
bg = const Color(0xFFFF9500).withValues(alpha: 0.15);
|
bg = const Color(0xFFFF9500).withValues(alpha: 0.15);
|
||||||
fg = const Color(0xFFFF9500);
|
fg = const Color(0xFFFF9500);
|
||||||
text = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR';
|
text = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR (PARU)';
|
||||||
break;
|
break;
|
||||||
case BackendType.flatpak:
|
case BackendType.flatpak:
|
||||||
bg = const Color(0xFF4A90E2).withValues(alpha: 0.15);
|
bg = const Color(0xFF4A90E2).withValues(alpha: 0.15);
|
||||||
|
|||||||
@@ -475,6 +475,41 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if ((_componentStatus?.hasParu ?? false) && (_componentStatus?.hasYay ?? false)) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Предпочитаемый AUR-хелпер',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12.5),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Инструмент для сборки пакетов сообщества из AUR',
|
||||||
|
style: TextStyle(fontSize: 11, color: CupertinoColors.systemGrey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MacosPopupButton<String>(
|
||||||
|
value: AppSettingsService.instance.preferredAurHelper,
|
||||||
|
items: const [
|
||||||
|
MacosPopupMenuItem(value: 'paru', child: Text('Paru (Rust)')),
|
||||||
|
MacosPopupMenuItem(value: 'yay', child: Text('Yay (Go)')),
|
||||||
|
],
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) {
|
||||||
|
AppSettingsService.instance.setPreferredAurHelper(val);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
const Divider(height: 1, color: CupertinoColors.separator),
|
const Divider(height: 1, color: CupertinoColors.separator),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
Reference in New Issue
Block a user