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 _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<void> setPreferredAurHelper(String value) async {
|
||||
if (_preferredAurHelper == value) return;
|
||||
_preferredAurHelper = value;
|
||||
notifyListeners();
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
Future<void> setHasCompletedSetupWizard(bool value) async {
|
||||
if (_hasCompletedSetupWizard == value) return;
|
||||
_hasCompletedSetupWizard = value;
|
||||
|
||||
@@ -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,37 +132,40 @@ class AurPackageDetails {
|
||||
class AurService {
|
||||
static String? _cachedHelper;
|
||||
|
||||
/// Определение доступного AUR-хелпера (предпочтение paru, затем yay)
|
||||
static Future<String?> getHelper() async {
|
||||
if (_cachedHelper != null) return _cachedHelper;
|
||||
static void resetCache() {
|
||||
_cachedHelper = null;
|
||||
}
|
||||
|
||||
/// Определение доступного и работающего 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 {
|
||||
final paruRes = await Process.run('which', ['paru']);
|
||||
if (paruRes.exitCode == 0 && paruRes.stdout.toString().trim().isNotEmpty) {
|
||||
_cachedHelper = 'paru';
|
||||
final res = await Process.run(helper, ['--version']);
|
||||
if (res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty) {
|
||||
_cachedHelper = helper;
|
||||
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;
|
||||
}
|
||||
|
||||
/// Поиск пакетов в AUR через официальный RPC API v5
|
||||
/// Поиск пакетов в AUR через официальный RPC API v5 с приоритизацией точных совпадений и хелперов
|
||||
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 {
|
||||
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 uri = Uri.parse(url);
|
||||
final req = await client.getUrl(uri);
|
||||
final res = await req.close();
|
||||
|
||||
@@ -169,39 +173,71 @@ class AurService {
|
||||
final body = await res.transform(utf8.decoder).join();
|
||||
final Map<String, dynamic> data = jsonDecode(body);
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
|
||||
final list = results
|
||||
return results
|
||||
.map((item) => AurPackage.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
// Сортируем по количеству голосов
|
||||
list.sort((a, b) => b.numVotes.compareTo(a.numVotes));
|
||||
return list;
|
||||
}
|
||||
} catch (_) {
|
||||
// Fallback через curl
|
||||
try {
|
||||
final curlRes = await Process.run('curl', [
|
||||
'-s',
|
||||
'--max-time',
|
||||
'5',
|
||||
'https://aur.archlinux.org/rpc/v5/search/${Uri.encodeComponent(query.trim())}',
|
||||
url,
|
||||
]);
|
||||
if (curlRes.exitCode == 0) {
|
||||
final Map<String, dynamic> data = jsonDecode(curlRes.stdout.toString());
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
final list = results
|
||||
return results
|
||||
.map((item) => AurPackage.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
list.sort((a, b) => b.numVotes.compareTo(a.numVotes));
|
||||
return list;
|
||||
}
|
||||
} 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<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
|
||||
static Future<AurPackageDetails?> getPackageInfo(String packageName) async {
|
||||
final clean = packageName.trim();
|
||||
|
||||
@@ -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,10 +187,36 @@ class BatchInstallService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Шаг 2: AUR пакеты (через yay или paru)
|
||||
// Шаг 2: AUR пакеты (через paru или yay)
|
||||
if (aurList.isNotEmpty) {
|
||||
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 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 пакеты
|
||||
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',
|
||||
'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',
|
||||
|
||||
@@ -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<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;
|
||||
String _searchQuery = '';
|
||||
String _selectedSource = 'Все';
|
||||
String _selectedStorefrontSource = 'Все';
|
||||
String? _aurHelper;
|
||||
|
||||
final List<CatalogApp> _featuredApps = CatalogService.getFeaturedApps();
|
||||
@@ -184,24 +185,33 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
||||
_buildSearchResultsGrid(macosTheme),
|
||||
] else ...[
|
||||
// Mode 2: App Store Storefront
|
||||
_buildStorefrontSourceFilterBar(macosTheme),
|
||||
const SizedBox(height: 18),
|
||||
_buildHeroBanner(macosTheme),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Section 1: Featured Apps
|
||||
...(() {
|
||||
final featured = _filterStorefrontApps(_featuredApps);
|
||||
if (featured.isEmpty) return <Widget>[];
|
||||
return <Widget>[
|
||||
_buildSectionHeader('ИЗБРАННОЕ И РЕКОМЕНДАЦИИ', 'Популярные приложения с быстрой установкой'),
|
||||
const SizedBox(height: 14),
|
||||
_buildAppsGrid(_featuredApps, macosTheme),
|
||||
|
||||
_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<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) {
|
||||
final filtered = _getFilteredSearchResults();
|
||||
final aurLabel = _aurHelper != null ? 'AUR ($_aurHelper)' : 'AUR (Paru)';
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -282,8 +372,8 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
||||
),
|
||||
),
|
||||
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<DiscoverScreen> {
|
||||
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);
|
||||
|
||||
@@ -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 Divider(height: 1, color: CupertinoColors.separator),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Reference in New Issue
Block a user