feat: Add full Paru support in catalog, search, settings, and installer

This commit is contained in:
RarDog
2026-09-13 19:03:46 +03:00
parent 524d99c7ca
commit 728a057242
7 changed files with 373 additions and 127 deletions
@@ -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;
+64 -28
View File
@@ -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();
+30 -2
View File
@@ -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) {
+26
View File
@@ -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;
}
} }
} }
+96 -6
View File
@@ -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);
+35
View File
@@ -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),