diff --git a/lib/src/services/app_settings_service.dart b/lib/src/services/app_settings_service.dart index b51c39c..71291eb 100644 --- a/lib/src/services/app_settings_service.dart +++ b/lib/src/services/app_settings_service.dart @@ -16,6 +16,7 @@ class AppSettingsService extends ChangeNotifier { String _preferredMirrorCountry = ''; String _windowTheme = 'auto'; bool _hasCompletedSetupWizard = false; + String _preferredAurHelper = 'paru'; bool get createSnapshotBeforeUpdate => _createSnapshotBeforeUpdate; bool get usePolkit => _usePolkit; @@ -23,6 +24,7 @@ class AppSettingsService extends ChangeNotifier { String get preferredMirrorCountry => _preferredMirrorCountry; String get windowTheme => _windowTheme; bool get hasCompletedSetupWizard => _hasCompletedSetupWizard; + String get preferredAurHelper => _preferredAurHelper; static String detectDesktopEnvironmentName() { final xdg = (Platform.environment['XDG_CURRENT_DESKTOP'] ?? '').toUpperCase(); @@ -85,6 +87,7 @@ class AppSettingsService extends ChangeNotifier { _preferredMirrorCountry = data['preferredMirrorCountry'] as String? ?? ''; _windowTheme = data['windowTheme'] as String? ?? 'auto'; _hasCompletedSetupWizard = data['hasCompletedSetupWizard'] as bool? ?? false; + _preferredAurHelper = data['preferredAurHelper'] as String? ?? 'paru'; } } catch (_) {} } @@ -99,11 +102,19 @@ class AppSettingsService extends ChangeNotifier { 'preferredMirrorCountry': _preferredMirrorCountry, 'windowTheme': _windowTheme, 'hasCompletedSetupWizard': _hasCompletedSetupWizard, + 'preferredAurHelper': _preferredAurHelper, }; await file.writeAsString(const JsonEncoder.withIndent(' ').convert(data)); } catch (_) {} } + Future setPreferredAurHelper(String value) async { + if (_preferredAurHelper == value) return; + _preferredAurHelper = value; + notifyListeners(); + await _saveSettings(); + } + Future setHasCompletedSetupWizard(bool value) async { if (_hasCompletedSetupWizard == value) return; _hasCompletedSetupWizard = value; diff --git a/lib/src/services/aur_service.dart b/lib/src/services/aur_service.dart index 917f36a..907bc4b 100644 --- a/lib/src/services/aur_service.dart +++ b/lib/src/services/aur_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import '../rust/api/packages.dart'; +import 'app_settings_service.dart'; class AurPackage { final String name; @@ -131,75 +132,110 @@ class AurPackageDetails { class AurService { static String? _cachedHelper; - /// Определение доступного AUR-хелпера (предпочтение paru, затем yay) - static Future getHelper() async { - if (_cachedHelper != null) return _cachedHelper; - - try { - final paruRes = await Process.run('which', ['paru']); - if (paruRes.exitCode == 0 && paruRes.stdout.toString().trim().isNotEmpty) { - _cachedHelper = 'paru'; - return _cachedHelper; - } - } 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; + static void resetCache() { + _cachedHelper = null; } - /// Поиск пакетов в AUR через официальный RPC API v5 - static Future> search(String query) async { - if (query.trim().isEmpty) return []; + /// Определение доступного и работающего AUR-хелпера (учитывает preferred helper: paru или yay) + static Future getHelper({bool forceRefresh = false}) async { + if (!forceRefresh && _cachedHelper != null) return _cachedHelper; - try { - final client = HttpClient(); - client.connectionTimeout = const Duration(seconds: 5); - final uri = Uri.parse('https://aur.archlinux.org/rpc/v5/search/${Uri.encodeComponent(query.trim())}'); - final req = await client.getUrl(uri); - final res = await req.close(); + final preferred = AppSettingsService.instance.preferredAurHelper; + final candidates = preferred == 'yay' ? ['yay', 'paru'] : ['paru', 'yay']; - if (res.statusCode == 200) { - final body = await res.transform(utf8.decoder).join(); - final Map data = jsonDecode(body); - final results = data['results'] as List? ?? []; - - final list = results - .map((item) => AurPackage.fromJson(item as Map)) - .toList(); - - // Сортируем по количеству голосов - list.sort((a, b) => b.numVotes.compareTo(a.numVotes)); - return list; - } - } catch (_) { - // Fallback через curl + for (final helper in candidates) { try { - final curlRes = await Process.run('curl', [ - '-s', - '--max-time', - '5', - 'https://aur.archlinux.org/rpc/v5/search/${Uri.encodeComponent(query.trim())}', - ]); - if (curlRes.exitCode == 0) { - final Map data = jsonDecode(curlRes.stdout.toString()); - final results = data['results'] as List? ?? []; - final list = results - .map((item) => AurPackage.fromJson(item as Map)) - .toList(); - list.sort((a, b) => b.numVotes.compareTo(a.numVotes)); - return list; + final res = await Process.run(helper, ['--version']); + if (res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty) { + _cachedHelper = helper; + return _cachedHelper; } } catch (_) {} } - return []; + return null; + } + + /// Поиск пакетов в AUR через официальный RPC API v5 с приоритизацией точных совпадений и хелперов + static Future> search(String query) async { + final clean = query.trim(); + if (clean.isEmpty) return []; + + Future> fetchQuery(String url) async { + try { + final client = HttpClient(); + client.connectionTimeout = const Duration(seconds: 5); + final uri = Uri.parse(url); + final req = await client.getUrl(uri); + final res = await req.close(); + + if (res.statusCode == 200) { + final body = await res.transform(utf8.decoder).join(); + final Map data = jsonDecode(body); + final results = data['results'] as List? ?? []; + return results + .map((item) => AurPackage.fromJson(item as Map)) + .toList(); + } + } catch (_) { + try { + final curlRes = await Process.run('curl', [ + '-s', + '--max-time', + '5', + url, + ]); + if (curlRes.exitCode == 0) { + final Map data = jsonDecode(curlRes.stdout.toString()); + final results = data['results'] as List? ?? []; + return results + .map((item) => AurPackage.fromJson(item as Map)) + .toList(); + } + } catch (_) {} + } + 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 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 diff --git a/lib/src/services/batch_install_service.dart b/lib/src/services/batch_install_service.dart index ecc66ae..9201ff0 100644 --- a/lib/src/services/batch_install_service.dart +++ b/lib/src/services/batch_install_service.dart @@ -7,6 +7,7 @@ import 'admin_service.dart'; import 'app_settings_service.dart'; import 'aur_service.dart'; import 'catalog_service.dart'; +import 'component_installer_service.dart'; import 'snapshot_service.dart'; class BatchInstallProgress { @@ -186,77 +187,104 @@ class BatchInstallService extends ChangeNotifier { } } - // Шаг 2: AUR пакеты (через yay или paru) + // Шаг 2: AUR пакеты (через paru или yay) if (aurList.isNotEmpty) { currentStage++; - final helper = await AurService.getHelper() ?? 'yay'; - final pkgIds = aurList.map((a) => a.id).toList(); - final pkgLabels = aurList.map((a) => a.name == a.id ? a.id : '${a.name} (${a.id})').toList(); + String? helper = await AurService.getHelper(); - yield BatchInstallProgress( - currentStep: '[$currentStage/$totalStages] Сборка и установка AUR ($helper: ${pkgLabels.join(", ")})...', - percentage: (currentStage - 0.5) / (totalStages + 0.2), - logLine: '🚀 [AUR] Сборка через $helper: ${pkgIds.join(" ")}', - ); - - try { - if (password != null) { - await AdminService.verifyPassword(password); - } - - final proc = await Process.start( - helper, - ['-S', '--noconfirm', '--needed', ...pkgIds], - mode: ProcessStartMode.normal, + // Если в очереди есть 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 на систему...', ); - - final stdoutSub = proc.stdout.transform(utf8.decoder).transform(const LineSplitter()); - final stderrSub = proc.stderr.transform(utf8.decoder).transform(const LineSplitter()); - - await for (final line in stdoutSub) { + await for (final line in ComponentInstallerService.installParu(password)) { yield BatchInstallProgress( - currentStep: '[$currentStage/$totalStages] AUR ($helper): компиляция и сборка...', + currentStep: '[$currentStage/$totalStages] Установка Paru...', percentage: (currentStage - 0.3) / (totalStages + 0.2), logLine: line, ); } - - await for (final errLine in stderrSub) { - yield BatchInstallProgress( - currentStep: '[$currentStage/$totalStages] AUR ($helper): сообщения...', - percentage: (currentStage - 0.2) / (totalStages + 0.2), - logLine: errLine, - ); + for (final p in paruApps) { + p.isInstalled = true; + _queue.remove(p.uniqueKey); + aurList.remove(p); } + helper = await AurService.getHelper(forceRefresh: true); + } - final code = await proc.exitCode; - if (code == 0) { - yield BatchInstallProgress( - currentStep: '[$currentStage/$totalStages] AUR успешно завершен', - percentage: currentStage / (totalStages + 0.2), - logLine: '✅ [AUR] Все пакеты успешно собраны и установлены!', - ); - for (final a in aurList) { - a.isInstalled = true; - _queue.remove(a.uniqueKey); + if (aurList.isNotEmpty) { + helper ??= 'paru'; + final pkgIds = aurList.map((a) => a.id).toList(); + final pkgLabels = aurList.map((a) => a.name == a.id ? a.id : '${a.name} (${a.id})').toList(); + + yield BatchInstallProgress( + currentStep: '[$currentStage/$totalStages] Сборка и установка AUR ($helper: ${pkgLabels.join(", ")})...', + percentage: (currentStage - 0.5) / (totalStages + 0.2), + logLine: '🚀 [AUR] Сборка через $helper: ${pkgIds.join(" ")}', + ); + + try { + if (password != null) { + await AdminService.verifyPassword(password); } - } else { + + final proc = await Process.start( + helper, + ['-S', '--noconfirm', '--needed', ...pkgIds], + mode: ProcessStartMode.normal, + ); + + final stdoutSub = proc.stdout.transform(utf8.decoder).transform(const LineSplitter()); + final stderrSub = proc.stderr.transform(utf8.decoder).transform(const LineSplitter()); + + await for (final line in stdoutSub) { + yield BatchInstallProgress( + currentStep: '[$currentStage/$totalStages] AUR ($helper): компиляция и сборка...', + percentage: (currentStage - 0.3) / (totalStages + 0.2), + logLine: line, + ); + } + + await for (final errLine in stderrSub) { + yield BatchInstallProgress( + currentStep: '[$currentStage/$totalStages] AUR ($helper): сообщения...', + percentage: (currentStage - 0.2) / (totalStages + 0.2), + logLine: errLine, + ); + } + + final code = await proc.exitCode; + if (code == 0) { + yield BatchInstallProgress( + currentStep: '[$currentStage/$totalStages] AUR успешно завершен', + percentage: currentStage / (totalStages + 0.2), + logLine: '✅ [AUR] Все пакеты успешно собраны и установлены!', + ); + for (final a in aurList) { + a.isInstalled = true; + _queue.remove(a.uniqueKey); + } + } else { + hasErrors = true; + yield BatchInstallProgress( + currentStep: '[$currentStage/$totalStages] AUR завершился с ошибкой', + percentage: currentStage / (totalStages + 0.2), + logLine: '❌ [AUR] Ошибка сборки пакетов (код $code)', + isFailed: true, + ); + } + } catch (e) { hasErrors = true; yield BatchInstallProgress( - currentStep: '[$currentStage/$totalStages] AUR завершился с ошибкой', + currentStep: '[$currentStage/$totalStages] Исключение AUR', percentage: currentStage / (totalStages + 0.2), - logLine: '❌ [AUR] Ошибка сборки пакетов (код $code)', + logLine: '❌ [AUR] Ошибка: $e', isFailed: true, ); } - } catch (e) { - hasErrors = true; - yield BatchInstallProgress( - currentStep: '[$currentStage/$totalStages] Исключение AUR', - percentage: currentStage / (totalStages + 0.2), - logLine: '❌ [AUR] Ошибка: $e', - isFailed: true, - ); } } diff --git a/lib/src/services/catalog_service.dart b/lib/src/services/catalog_service.dart index a0f4465..7763d6c 100644 --- a/lib/src/services/catalog_service.dart +++ b/lib/src/services/catalog_service.dart @@ -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', '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', + '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 @@ -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( id: 'com.usebottles.bottles', name: 'Bottles', diff --git a/lib/src/services/component_installer_service.dart b/lib/src/services/component_installer_service.dart index e1e6181..5cac73b 100644 --- a/lib/src/services/component_installer_service.dart +++ b/lib/src/services/component_installer_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'admin_service.dart'; +import 'aur_service.dart'; class SystemComponentStatus { final bool hasFlatpak; @@ -40,13 +41,13 @@ class ComponentInstallerService { } try { - final res = await Process.run('which', ['paru']); - hasParu = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty; + final res = await Process.run('paru', ['--version']); + hasParu = res.exitCode == 0 && res.stdout.toString().toLowerCase().contains('paru'); } catch (_) {} try { - final res = await Process.run('which', ['yay']); - hasYay = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty; + final res = await Process.run('yay', ['--version']); + hasYay = res.exitCode == 0 && res.stdout.toString().toLowerCase().contains('yay'); } catch (_) {} return SystemComponentStatus( @@ -161,10 +162,29 @@ class ComponentInstallerService { final exitCode = await proc.exitCode; if (exitCode == 0) { + AurService.resetCache(); yield '🎉 [Paru] Paru успешно установлен и готов к работе!'; return; } 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(); + 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; + } } } diff --git a/lib/src/ui/screens/discover_screen.dart b/lib/src/ui/screens/discover_screen.dart index a72f584..4d748bf 100644 --- a/lib/src/ui/screens/discover_screen.dart +++ b/lib/src/ui/screens/discover_screen.dart @@ -25,6 +25,7 @@ class _DiscoverScreenState extends State { bool _isSearching = false; String _searchQuery = ''; String _selectedSource = 'Все'; + String _selectedStorefrontSource = 'Все'; String? _aurHelper; final List _featuredApps = CatalogService.getFeaturedApps(); @@ -184,24 +185,33 @@ class _DiscoverScreenState extends State { _buildSearchResultsGrid(macosTheme), ] else ...[ // Mode 2: App Store Storefront + _buildStorefrontSourceFilterBar(macosTheme), + const SizedBox(height: 18), _buildHeroBanner(macosTheme), const SizedBox(height: 28), // Section 1: Featured Apps - _buildSectionHeader('ИЗБРАННОЕ И РЕКОМЕНДАЦИИ', 'Популярные приложения с быстрой установкой'), - const SizedBox(height: 14), - _buildAppsGrid(_featuredApps, macosTheme), - - const SizedBox(height: 32), + ...(() { + final featured = _filterStorefrontApps(_featuredApps); + if (featured.isEmpty) return []; + return [ + _buildSectionHeader('ИЗБРАННОЕ И РЕКОМЕНДАЦИИ', 'Популярные приложения с быстрой установкой'), + const SizedBox(height: 14), + _buildAppsGrid(featured, macosTheme), + const SizedBox(height: 32), + ]; + })(), // Section 2: Curated Categories ..._curatedCategories.entries.map((cat) { + final filteredList = _filterStorefrontApps(cat.value); + if (filteredList.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader(cat.key.toUpperCase(), 'Подборка проверенного ПО'), const SizedBox(height: 14), - _buildAppsGrid(cat.value, macosTheme), + _buildAppsGrid(filteredList, macosTheme), const SizedBox(height: 32), ], ); @@ -226,8 +236,88 @@ class _DiscoverScreenState extends State { ); } + List _filterStorefrontApps(List 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) { final filtered = _getFilteredSearchResults(); + final aurLabel = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR (Paru)'; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -282,8 +372,8 @@ class _DiscoverScreenState extends State { ), ), child: Text( - source == 'AUR' && _aurHelper != null - ? 'AUR ($_aurHelper) ($count)' + source == 'AUR' + ? '$aurLabel ($count)' : '$source ($count)', style: TextStyle( fontSize: 11.5, @@ -634,7 +724,7 @@ class _DiscoverScreenState extends State { case BackendType.aur: bg = const Color(0xFFFF9500).withValues(alpha: 0.15); fg = const Color(0xFFFF9500); - text = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR'; + text = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR (PARU)'; break; case BackendType.flatpak: bg = const Color(0xFF4A90E2).withValues(alpha: 0.15); diff --git a/lib/src/ui/screens/settings_screen.dart b/lib/src/ui/screens/settings_screen.dart index 1b9a7c0..102ba88 100644 --- a/lib/src/ui/screens/settings_screen.dart +++ b/lib/src/ui/screens/settings_screen.dart @@ -475,6 +475,41 @@ class _SettingsScreenState extends State { ), ], ), + 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( + 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 Divider(height: 1, color: CupertinoColors.separator), const SizedBox(height: 12),