feat: Add rich AUR details, component installer & initial setup wizard
This commit is contained in:
@@ -15,12 +15,14 @@ class AppSettingsService extends ChangeNotifier {
|
||||
String _preferredBackend = 'pacman';
|
||||
String _preferredMirrorCountry = '';
|
||||
String _windowTheme = 'auto';
|
||||
bool _hasCompletedSetupWizard = false;
|
||||
|
||||
bool get createSnapshotBeforeUpdate => _createSnapshotBeforeUpdate;
|
||||
bool get usePolkit => _usePolkit;
|
||||
String get preferredBackend => _preferredBackend;
|
||||
String get preferredMirrorCountry => _preferredMirrorCountry;
|
||||
String get windowTheme => _windowTheme;
|
||||
bool get hasCompletedSetupWizard => _hasCompletedSetupWizard;
|
||||
|
||||
static String detectDesktopEnvironmentName() {
|
||||
final xdg = (Platform.environment['XDG_CURRENT_DESKTOP'] ?? '').toUpperCase();
|
||||
@@ -82,6 +84,7 @@ class AppSettingsService extends ChangeNotifier {
|
||||
_preferredBackend = data['preferredBackend'] as String? ?? 'pacman';
|
||||
_preferredMirrorCountry = data['preferredMirrorCountry'] as String? ?? '';
|
||||
_windowTheme = data['windowTheme'] as String? ?? 'auto';
|
||||
_hasCompletedSetupWizard = data['hasCompletedSetupWizard'] as bool? ?? false;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -95,11 +98,19 @@ class AppSettingsService extends ChangeNotifier {
|
||||
'preferredBackend': _preferredBackend,
|
||||
'preferredMirrorCountry': _preferredMirrorCountry,
|
||||
'windowTheme': _windowTheme,
|
||||
'hasCompletedSetupWizard': _hasCompletedSetupWizard,
|
||||
};
|
||||
await file.writeAsString(const JsonEncoder.withIndent(' ').convert(data));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> setHasCompletedSetupWizard(bool value) async {
|
||||
if (_hasCompletedSetupWizard == value) return;
|
||||
_hasCompletedSetupWizard = value;
|
||||
notifyListeners();
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
Future<void> setWindowTheme(String value) async {
|
||||
if (_windowTheme == value) return;
|
||||
_windowTheme = value;
|
||||
|
||||
@@ -41,6 +41,93 @@ class AurPackage {
|
||||
}
|
||||
}
|
||||
|
||||
class AurPackageDetails {
|
||||
final String name;
|
||||
final String version;
|
||||
final String description;
|
||||
final String? url;
|
||||
final String packageBase;
|
||||
final String? maintainer;
|
||||
final int numVotes;
|
||||
final double popularity;
|
||||
final DateTime? firstSubmitted;
|
||||
final DateTime? lastModified;
|
||||
final DateTime? outOfDate;
|
||||
final List<String> licenses;
|
||||
final List<String> depends;
|
||||
final List<String> makeDepends;
|
||||
final List<String> optDepends;
|
||||
final List<String> checkDepends;
|
||||
final List<String> conflicts;
|
||||
final List<String> provides;
|
||||
final List<String> replaces;
|
||||
final List<String> keywords;
|
||||
|
||||
const AurPackageDetails({
|
||||
required this.name,
|
||||
required this.version,
|
||||
required this.description,
|
||||
this.url,
|
||||
required this.packageBase,
|
||||
this.maintainer,
|
||||
required this.numVotes,
|
||||
required this.popularity,
|
||||
this.firstSubmitted,
|
||||
this.lastModified,
|
||||
this.outOfDate,
|
||||
required this.licenses,
|
||||
required this.depends,
|
||||
required this.makeDepends,
|
||||
required this.optDepends,
|
||||
required this.checkDepends,
|
||||
required this.conflicts,
|
||||
required this.provides,
|
||||
required this.replaces,
|
||||
required this.keywords,
|
||||
});
|
||||
|
||||
bool get isOutOfDate => outOfDate != null;
|
||||
|
||||
factory AurPackageDetails.fromJson(Map<String, dynamic> json) {
|
||||
List<String> parseList(dynamic val) {
|
||||
if (val is List) {
|
||||
return val.map((e) => e.toString()).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
DateTime? parseTimestamp(dynamic val) {
|
||||
if (val is num && val > 0) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(val.toInt() * 1000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return AurPackageDetails(
|
||||
name: json['Name'] as String? ?? '',
|
||||
version: json['Version'] as String? ?? '1.0.0',
|
||||
description: json['Description'] as String? ?? '',
|
||||
url: json['URL'] as String?,
|
||||
packageBase: json['PackageBase'] as String? ?? (json['Name'] as String? ?? ''),
|
||||
maintainer: json['Maintainer'] as String?,
|
||||
numVotes: (json['NumVotes'] as num?)?.toInt() ?? 0,
|
||||
popularity: (json['Popularity'] as num?)?.toDouble() ?? 0.0,
|
||||
firstSubmitted: parseTimestamp(json['FirstSubmitted']),
|
||||
lastModified: parseTimestamp(json['LastModified']),
|
||||
outOfDate: parseTimestamp(json['OutOfDate']),
|
||||
licenses: parseList(json['License']),
|
||||
depends: parseList(json['Depends']),
|
||||
makeDepends: parseList(json['MakeDepends']),
|
||||
optDepends: parseList(json['OptDepends']),
|
||||
checkDepends: parseList(json['CheckDepends']),
|
||||
conflicts: parseList(json['Conflicts']),
|
||||
provides: parseList(json['Provides']),
|
||||
replaces: parseList(json['Replaces']),
|
||||
keywords: parseList(json['Keywords']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AurService {
|
||||
static String? _cachedHelper;
|
||||
|
||||
@@ -115,6 +202,47 @@ class AurService {
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Получение детальной информации о пакете AUR через RPC v5 info endpoint
|
||||
static Future<AurPackageDetails?> getPackageInfo(String packageName) async {
|
||||
final clean = packageName.trim();
|
||||
if (clean.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 5);
|
||||
final uri = Uri.parse('https://aur.archlinux.org/rpc/v5/info/${Uri.encodeComponent(clean)}');
|
||||
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<String, dynamic> data = jsonDecode(body);
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
if (results.isNotEmpty) {
|
||||
return AurPackageDetails.fromJson(results.first as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
try {
|
||||
final curlRes = await Process.run('curl', [
|
||||
'-s',
|
||||
'--max-time',
|
||||
'5',
|
||||
'https://aur.archlinux.org/rpc/v5/info/${Uri.encodeComponent(clean)}',
|
||||
]);
|
||||
if (curlRes.exitCode == 0) {
|
||||
final Map<String, dynamic> data = jsonDecode(curlRes.stdout.toString());
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
if (results.isNotEmpty) {
|
||||
return AurPackageDetails.fromJson(results.first as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Сборка и установка пакета AUR через найденный хелпер (paru / yay)
|
||||
static Stream<ProgressEvent> installPackage(String packageName) async* {
|
||||
final helper = await getHelper();
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'admin_service.dart';
|
||||
|
||||
class SystemComponentStatus {
|
||||
final bool hasFlatpak;
|
||||
final bool hasFlathub;
|
||||
final bool hasParu;
|
||||
final bool hasYay;
|
||||
|
||||
const SystemComponentStatus({
|
||||
required this.hasFlatpak,
|
||||
required this.hasFlathub,
|
||||
required this.hasParu,
|
||||
required this.hasYay,
|
||||
});
|
||||
|
||||
bool get hasAurHelper => hasParu || hasYay;
|
||||
}
|
||||
|
||||
class ComponentInstallerService {
|
||||
/// Проверка статуса установленных компонентов системы
|
||||
static Future<SystemComponentStatus> checkComponents() async {
|
||||
bool hasFlatpak = false;
|
||||
bool hasFlathub = false;
|
||||
bool hasParu = false;
|
||||
bool hasYay = false;
|
||||
|
||||
try {
|
||||
final res = await Process.run('which', ['flatpak']);
|
||||
hasFlatpak = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty;
|
||||
} catch (_) {}
|
||||
|
||||
if (hasFlatpak) {
|
||||
try {
|
||||
final res = await Process.run('flatpak', ['remotes']);
|
||||
hasFlathub = res.stdout.toString().toLowerCase().contains('flathub');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
final res = await Process.run('which', ['paru']);
|
||||
hasParu = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
final res = await Process.run('which', ['yay']);
|
||||
hasYay = res.exitCode == 0 && res.stdout.toString().trim().isNotEmpty;
|
||||
} catch (_) {}
|
||||
|
||||
return SystemComponentStatus(
|
||||
hasFlatpak: hasFlatpak,
|
||||
hasFlathub: hasFlathub,
|
||||
hasParu: hasParu,
|
||||
hasYay: hasYay,
|
||||
);
|
||||
}
|
||||
|
||||
/// Установка Flatpak и подключение репозитория Flathub
|
||||
static Stream<String> installFlatpak(String? password) async* {
|
||||
yield '🚀 [Flatpak] Проверка и установка пакета flatpak через pacman...';
|
||||
|
||||
// 1. Установка flatpak
|
||||
final proc = await AdminService.runPrivileged(
|
||||
password,
|
||||
'pacman',
|
||||
['-S', '--needed', '--noconfirm', 'flatpak'],
|
||||
);
|
||||
|
||||
final controller = StreamController<String>();
|
||||
proc.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) => controller.add(line),
|
||||
onError: (e) => controller.add('ERR: $e'),
|
||||
);
|
||||
proc.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) => controller.add('STDERR: $line'),
|
||||
onError: (e) => controller.add('ERR: $e'),
|
||||
);
|
||||
|
||||
proc.exitCode.then((_) {
|
||||
Future.delayed(const Duration(milliseconds: 60), () {
|
||||
if (!controller.isClosed) controller.close();
|
||||
});
|
||||
});
|
||||
|
||||
await for (final line in controller.stream) {
|
||||
yield line;
|
||||
}
|
||||
|
||||
final code = await proc.exitCode;
|
||||
if (code != 0) {
|
||||
yield '❌ [Flatpak] Ошибка установки пакета flatpak (код $code).';
|
||||
return;
|
||||
}
|
||||
|
||||
yield '✅ [Flatpak] Пакет flatpak успешно установлен в систему.';
|
||||
yield '🌐 [Flathub] Подключение официального репозитория Flathub...';
|
||||
|
||||
// 2. Добавление репозитория Flathub
|
||||
try {
|
||||
final remoteProc = await Process.run('flatpak', [
|
||||
'remote-add',
|
||||
'--if-not-exists',
|
||||
'flathub',
|
||||
'https://dl.flathub.org/repo/flathub.flatpakrepo',
|
||||
]);
|
||||
|
||||
if (remoteProc.exitCode == 0) {
|
||||
yield '✅ [Flathub] Репозиторий Flathub успешно подключен!';
|
||||
} else {
|
||||
// Попытка с правами суперпользователя
|
||||
final rootRemote = await AdminService.runPrivileged(
|
||||
password,
|
||||
'flatpak',
|
||||
['remote-add', '--if-not-exists', 'flathub', 'https://dl.flathub.org/repo/flathub.flatpakrepo'],
|
||||
);
|
||||
await rootRemote.exitCode;
|
||||
yield '✅ [Flathub] Репозиторий Flathub подключен системно!';
|
||||
}
|
||||
} catch (e) {
|
||||
yield '⚠️ [Flathub] Предупреждение при подключении репозитория: $e';
|
||||
}
|
||||
|
||||
yield '🎉 [Успех] Интеграция Flatpak & Flathub полностью готова!';
|
||||
}
|
||||
|
||||
/// Установка AUR-хелпера Paru (Rust)
|
||||
static Stream<String> installParu(String? password) async* {
|
||||
yield '🚀 [Paru] Начинаем установку Paru (AUR Helper на Rust)...';
|
||||
|
||||
// Если уже есть yay, ставим paru-bin через него
|
||||
final status = await checkComponents();
|
||||
if (status.hasYay) {
|
||||
yield '📦 [Paru] Обнаружен yay! Устанавливаем paru-bin через yay...';
|
||||
final proc = await Process.start(
|
||||
'yay',
|
||||
['-S', '--noconfirm', 'paru-bin'],
|
||||
environment: {'LC_ALL': 'C'},
|
||||
);
|
||||
|
||||
final controller = StreamController<String>();
|
||||
proc.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) => controller.add(line),
|
||||
onError: (e) => controller.add('ERR: $e'),
|
||||
);
|
||||
proc.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) => controller.add('STDERR: $line'),
|
||||
onError: (e) => controller.add('ERR: $e'),
|
||||
);
|
||||
|
||||
proc.exitCode.then((_) {
|
||||
Future.delayed(const Duration(milliseconds: 60), () {
|
||||
if (!controller.isClosed) controller.close();
|
||||
});
|
||||
});
|
||||
|
||||
await for (final line in controller.stream) {
|
||||
yield line;
|
||||
}
|
||||
|
||||
final exitCode = await proc.exitCode;
|
||||
if (exitCode == 0) {
|
||||
yield '🎉 [Paru] Paru успешно установлен и готов к работе!';
|
||||
return;
|
||||
} else {
|
||||
yield '⚠️ [Paru] Не удалось установить через yay (код $exitCode), переключаемся на ручную сборку...';
|
||||
}
|
||||
}
|
||||
|
||||
// Ручная установка paru-bin через makepkg
|
||||
yield '🛠️ [Paru] Проверка базовых инструментов сборки (base-devel, git)...';
|
||||
final pacmanProc = await AdminService.runPrivileged(
|
||||
password,
|
||||
'pacman',
|
||||
['-S', '--needed', '--noconfirm', 'base-devel', 'git'],
|
||||
);
|
||||
await pacmanProc.exitCode;
|
||||
|
||||
// Кэшируем sudo для пользователя чтобы makepkg -si не споткнулся
|
||||
if (password != null && password.isNotEmpty) {
|
||||
await AdminService.verifyPassword(password);
|
||||
}
|
||||
|
||||
Directory? tempDir;
|
||||
try {
|
||||
tempDir = Directory.systemTemp.createTempSync('forge_paru_install_');
|
||||
yield '📥 [Paru] Клонирование paru-bin из AUR...';
|
||||
|
||||
final cloneRes = await Process.run('git', [
|
||||
'clone',
|
||||
'https://aur.archlinux.org/paru-bin.git',
|
||||
'${tempDir.path}/paru-bin',
|
||||
]);
|
||||
|
||||
if (cloneRes.exitCode != 0) {
|
||||
yield '❌ [Paru] Ошибка клонирования репозитория: ${cloneRes.stderr}';
|
||||
return;
|
||||
}
|
||||
|
||||
yield '⚙️ [Paru] Сборка и установка пакета (makepkg -si)...';
|
||||
final makepkgProc = await Process.start(
|
||||
'makepkg',
|
||||
['-si', '--noconfirm'],
|
||||
workingDirectory: '${tempDir.path}/paru-bin',
|
||||
environment: {'LC_ALL': 'C'},
|
||||
);
|
||||
|
||||
final controller = StreamController<String>();
|
||||
makepkgProc.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) => controller.add(line),
|
||||
onError: (e) => controller.add('ERR: $e'),
|
||||
);
|
||||
makepkgProc.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
(line) => controller.add('STDERR: $line'),
|
||||
onError: (e) => controller.add('ERR: $e'),
|
||||
);
|
||||
|
||||
makepkgProc.exitCode.then((_) {
|
||||
Future.delayed(const Duration(milliseconds: 60), () {
|
||||
if (!controller.isClosed) controller.close();
|
||||
});
|
||||
});
|
||||
|
||||
await for (final line in controller.stream) {
|
||||
yield line;
|
||||
}
|
||||
|
||||
final exitCode = await makepkgProc.exitCode;
|
||||
if (exitCode == 0) {
|
||||
yield '🎉 [Paru] Paru успешно собран и установлен в систему!';
|
||||
} else {
|
||||
yield '❌ [Paru] makepkg завершился с кодом ошибки $exitCode.';
|
||||
}
|
||||
} catch (e) {
|
||||
yield '❌ [Paru] Ошибка во время установки: $e';
|
||||
} finally {
|
||||
if (tempDir != null && tempDir.existsSync()) {
|
||||
try {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import 'screens/starter_packs_screen.dart';
|
||||
import 'screens/updates_screen.dart';
|
||||
import 'widgets/keyboard_shortcuts_dialog.dart';
|
||||
import 'widgets/package_inspector.dart';
|
||||
import 'widgets/setup_wizard_dialog.dart';
|
||||
import 'widgets/spotlight_search_dialog.dart';
|
||||
|
||||
class MainLayout extends StatefulWidget {
|
||||
@@ -32,6 +33,12 @@ class _MainLayoutState extends State<MainLayout> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_performInitialBadgeScan();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!AppSettingsService.instance.hasCompletedSetupWizard && mounted) {
|
||||
SetupWizardDialog.show(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _performInitialBadgeScan() async {
|
||||
|
||||
@@ -7,6 +7,8 @@ import '../../services/snapshot_service.dart';
|
||||
import '../widgets/friendly_term.dart';
|
||||
import '../widgets/keyboard_shortcuts_dialog.dart';
|
||||
import '../widgets/reflector_mirror_dialog.dart';
|
||||
import '../widgets/setup_wizard_dialog.dart';
|
||||
import '../../services/component_installer_service.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -23,6 +25,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
SnapshotEngine _snapshotEngine = SnapshotEngine.none;
|
||||
bool _isCreatingSnapshot = false;
|
||||
String? _snapshotMessage;
|
||||
SystemComponentStatus? _componentStatus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -34,6 +37,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final settings = AppSettingsService.instance;
|
||||
await settings.load();
|
||||
final engine = await SnapshotService.detectEngine();
|
||||
final compStatus = await ComponentInstallerService.checkComponents();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_usePolkit = settings.usePolkit;
|
||||
@@ -41,6 +45,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_createSnapshotBeforeUpdate = settings.createSnapshotBeforeUpdate;
|
||||
_windowTheme = settings.windowTheme;
|
||||
_snapshotEngine = engine;
|
||||
_componentStatus = compStatus;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -317,7 +322,196 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Group 3: Recovery Snapshots
|
||||
// Group 3: System Components & Integrations
|
||||
_buildSectionHeader('СИСТЕМНЫЕ КОМПОНЕНТЫ И ХЕЛПЕРЫ (FLATPAK & PARU)'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: macosTheme.canvasColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: macosTheme.dividerColor,
|
||||
width: 0.8,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Flatpak status & button
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF34C759).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: MacosIcon(CupertinoIcons.app_badge_fill, color: Color(0xFF34C759), size: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Поддержка Flatpak & Flathub',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13.5),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (_componentStatus?.hasFlatpak ?? false)
|
||||
? CupertinoColors.systemGreen.withValues(alpha: 0.15)
|
||||
: CupertinoColors.systemGrey.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
(_componentStatus?.hasFlatpak ?? false) ? 'Установлен' : 'Не установлен',
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (_componentStatus?.hasFlatpak ?? false)
|
||||
? CupertinoColors.systemGreen
|
||||
: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Установка приложений из изолированного каталога Flathub',
|
||||
style: TextStyle(fontSize: 12, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PushButton(
|
||||
controlSize: ControlSize.small,
|
||||
secondary: true,
|
||||
onPressed: () async {
|
||||
await SetupWizardDialog.show(context);
|
||||
_loadSettings();
|
||||
},
|
||||
child: Text((_componentStatus?.hasFlatpak ?? false) ? 'Управление' : 'Установить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Divider(height: 1, color: CupertinoColors.separator),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Paru status & button
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF9500).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: MacosIcon(CupertinoIcons.flame_fill, color: Color(0xFFFF9500), size: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'AUR-хелпер Paru (Rust)',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13.5),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (_componentStatus?.hasParu ?? false)
|
||||
? CupertinoColors.systemGreen.withValues(alpha: 0.15)
|
||||
: CupertinoColors.systemGrey.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
(_componentStatus?.hasParu ?? false)
|
||||
? 'Paru активен'
|
||||
: ((_componentStatus?.hasYay ?? false) ? 'Обнаружен yay' : 'Отсутствует'),
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (_componentStatus?.hasParu ?? false)
|
||||
? CupertinoColors.systemGreen
|
||||
: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Сверхбыстрый сборщик пакетов из Arch User Repository',
|
||||
style: TextStyle(fontSize: 12, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PushButton(
|
||||
controlSize: ControlSize.small,
|
||||
secondary: true,
|
||||
onPressed: () async {
|
||||
await SetupWizardDialog.show(context);
|
||||
_loadSettings();
|
||||
},
|
||||
child: Text((_componentStatus?.hasParu ?? false) ? 'Переустановить' : 'Установить Paru'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Divider(height: 1, color: CupertinoColors.separator),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Wizard button
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Мастер первичной настройки',
|
||||
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12.5),
|
||||
),
|
||||
),
|
||||
PushButton(
|
||||
controlSize: ControlSize.small,
|
||||
onPressed: () async {
|
||||
await SetupWizardDialog.show(context);
|
||||
_loadSettings();
|
||||
},
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MacosIcon(CupertinoIcons.sparkles, size: 12),
|
||||
SizedBox(width: 5),
|
||||
Text('Запустить мастер настройки'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Group 4: Recovery Snapshots
|
||||
_buildSectionHeader('ТОЧКИ ВОССТАНОВЛЕНИЯ СИСТЕМЫ (SNAPSHOTS)'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../rust/api/packages.dart';
|
||||
import '../../rust/frb_generated.dart';
|
||||
import '../../services/admin_service.dart';
|
||||
import '../../services/aur_pkgbuild_service.dart';
|
||||
import '../../services/aur_service.dart';
|
||||
import '../../services/flatpak_permissions_service.dart';
|
||||
|
||||
/// Модальное адаптивное окно просмотра свойств пакета («поверх окна каталога»)
|
||||
@@ -58,6 +59,7 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
String? _pkgbuildContent;
|
||||
PkgbuildAuditResult? _pkgbuildAudit;
|
||||
bool _isLoadingPkgbuild = false;
|
||||
AurPackageDetails? _aurDetails;
|
||||
|
||||
// Flatpak Permissions
|
||||
FlatpakAppPermissions? _flatpakPermissions;
|
||||
@@ -130,13 +132,42 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_fileSearchFilter = '';
|
||||
_aurDetails = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final data = await RustBridgeApi.getPackageDetails(
|
||||
_currentBackend,
|
||||
_currentPackageName,
|
||||
);
|
||||
PackageDetails? data;
|
||||
try {
|
||||
data = await RustBridgeApi.getPackageDetails(
|
||||
_currentBackend,
|
||||
_currentPackageName,
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
AurPackageDetails? aurData;
|
||||
if (_currentBackend == BackendType.aur) {
|
||||
try {
|
||||
aurData = await AurService.getPackageInfo(_currentPackageName);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (data == null && aurData != null) {
|
||||
data = PackageDetails(
|
||||
name: aurData.name,
|
||||
version: aurData.version,
|
||||
description: aurData.description,
|
||||
url: aurData.url,
|
||||
licenses: aurData.licenses,
|
||||
dependsOn: aurData.depends,
|
||||
requiredBy: const [],
|
||||
packager: aurData.maintainer ?? 'AUR Community',
|
||||
downloadSizeBytes: 0,
|
||||
installedSizeBytes: 0,
|
||||
buildDate: aurData.lastModified?.toLocal().toString().split(' ').first,
|
||||
repository: 'aur',
|
||||
backend: BackendType.aur,
|
||||
);
|
||||
}
|
||||
|
||||
List<CachedPackageVersion> cached = [];
|
||||
List<String> files = [];
|
||||
@@ -161,6 +192,7 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_details = data;
|
||||
_aurDetails = aurData;
|
||||
_cachedVersions = cached;
|
||||
_packageFiles = files;
|
||||
_isLoading = false;
|
||||
@@ -671,6 +703,29 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(22),
|
||||
children: [
|
||||
if (_aurDetails?.isOutOfDate ?? false)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemRed.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: CupertinoColors.systemRed.withValues(alpha: 0.5), width: 0.8),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
MacosIcon(CupertinoIcons.exclamationmark_triangle_fill, color: CupertinoColors.systemRed, size: 20),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Внимание: Пакет помечен в AUR как устаревший (Out-of-Date)! Возможно, исходный код обновился раньше, чем PKGBUILD.',
|
||||
style: TextStyle(fontSize: 12.5, color: CupertinoColors.systemRed, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Description Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -707,13 +762,81 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
_buildInfoCard(theme, 'Размер загрузки', '${(d.downloadSizeBytes / (1024 * 1024)).toStringAsFixed(1)} MB', CupertinoIcons.arrow_down_to_line),
|
||||
_buildInfoCard(theme, 'Лицензия', d.licenses.isNotEmpty ? d.licenses.join(', ') : 'Не указана', CupertinoIcons.doc_plaintext),
|
||||
if (d.packager != null)
|
||||
_buildInfoCard(theme, 'Сборщик', d.packager!, CupertinoIcons.person_badge_plus),
|
||||
_buildInfoCard(theme, 'Сборщик / Мейнтейнер', d.packager!, CupertinoIcons.person_badge_plus),
|
||||
if (d.buildDate != null)
|
||||
_buildInfoCard(theme, 'Дата сборки', d.buildDate!, CupertinoIcons.calendar),
|
||||
_buildInfoCard(theme, 'Репозиторий', d.repository, CupertinoIcons.archivebox),
|
||||
_buildInfoCard(theme, 'Пакетный менеджер', d.backend.displayName, CupertinoIcons.layers),
|
||||
if (_aurDetails != null) ...[
|
||||
_buildInfoCard(theme, 'Голоса в AUR', '${_aurDetails!.numVotes}', CupertinoIcons.hand_thumbsup),
|
||||
_buildInfoCard(theme, 'Популярность', _aurDetails!.popularity.toStringAsFixed(2), CupertinoIcons.chart_bar_alt_fill),
|
||||
if (_aurDetails!.packageBase.isNotEmpty && _aurDetails!.packageBase != d.name)
|
||||
_buildInfoCard(theme, 'Базовый пакет', _aurDetails!.packageBase, CupertinoIcons.cube_box_fill),
|
||||
if (_aurDetails!.firstSubmitted != null)
|
||||
_buildInfoCard(theme, 'Впервые в AUR', '${_aurDetails!.firstSubmitted!.toLocal()}'.split(' ').first, CupertinoIcons.calendar_today),
|
||||
if (_aurDetails!.lastModified != null)
|
||||
_buildInfoCard(theme, 'Обновлен в AUR', '${_aurDetails!.lastModified!.toLocal()}'.split(' ').first, CupertinoIcons.arrow_clockwise),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
if (_aurDetails != null && _aurDetails!.keywords.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: theme.dividerColor.withValues(alpha: 0.4), width: 0.8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'ТЕГИ / КЛЮЧЕВЫЕ СЛОВА',
|
||||
style: TextStyle(fontSize: 10.5, fontWeight: FontWeight.bold, letterSpacing: 0.7, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: _aurDetails!.keywords.map((kw) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: theme.primaryColor.withValues(alpha: 0.3), width: 0.8),
|
||||
),
|
||||
child: Text(
|
||||
'#$kw',
|
||||
style: TextStyle(fontSize: 11, color: theme.primaryColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (d.backend == BackendType.aur) ...[
|
||||
const SizedBox(height: 20),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: PushButton(
|
||||
controlSize: ControlSize.regular,
|
||||
secondary: true,
|
||||
onPressed: () => _openProjectUrl('https://aur.archlinux.org/packages/${d.name}'),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MacosIcon(CupertinoIcons.arrow_up_right_square, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text('Открыть страницу на aur.archlinux.org'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -753,6 +876,43 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDepChips(MacosThemeData theme, List<String> items, {Color? chipColor, Color? borderColor, Color? textColor}) {
|
||||
if (items.isEmpty) {
|
||||
return const Text('Отсутствуют.', style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 12.5));
|
||||
}
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: items.map((dep) {
|
||||
final cleanName = dep.split(RegExp(r'[><=]')).first.trim();
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
onPressed: () => _navigateTo(cleanName, _currentBackend),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: chipColor ?? theme.primaryColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: borderColor ?? theme.primaryColor.withValues(alpha: 0.3), width: 0.8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dep,
|
||||
style: TextStyle(fontFamily: 'monospace', fontSize: 11.5, color: textColor ?? theme.primaryColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
MacosIcon(CupertinoIcons.arrow_right, size: 10, color: textColor ?? theme.primaryColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDependenciesTab(MacosThemeData theme, PackageDetails d) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(22),
|
||||
@@ -760,40 +920,59 @@ class _PackageInspectorSheetState extends State<PackageInspectorSheet> {
|
||||
// Depends on
|
||||
_buildSectionHeader('НЕОБХОДИМЫЕ ЗАВИСИМОСТИ (${d.dependsOn.length})'),
|
||||
const SizedBox(height: 10),
|
||||
if (d.dependsOn.isEmpty)
|
||||
const Text('Зависимости отсутствуют.', style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 12.5))
|
||||
else
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: d.dependsOn.map((dep) {
|
||||
final cleanName = dep.split(RegExp(r'[><=]')).first.trim();
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
onPressed: () => _navigateTo(cleanName, d.backend),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: theme.primaryColor.withValues(alpha: 0.3), width: 0.8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dep,
|
||||
style: TextStyle(fontFamily: 'monospace', fontSize: 11.5, color: theme.primaryColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
MacosIcon(CupertinoIcons.arrow_right, size: 10, color: theme.primaryColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
_buildDepChips(theme, d.dependsOn),
|
||||
|
||||
if (_aurDetails != null && _aurDetails!.makeDepends.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('СБОРОЧНЫЕ ЗАВИСИМОСТИ (MAKE DEPENDS, ${_aurDetails!.makeDepends.length})'),
|
||||
const SizedBox(height: 10),
|
||||
_buildDepChips(
|
||||
theme,
|
||||
_aurDetails!.makeDepends,
|
||||
chipColor: CupertinoColors.systemIndigo.withValues(alpha: 0.12),
|
||||
borderColor: CupertinoColors.systemIndigo.withValues(alpha: 0.35),
|
||||
textColor: CupertinoColors.systemIndigo,
|
||||
),
|
||||
],
|
||||
|
||||
if (_aurDetails != null && _aurDetails!.optDepends.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('ОПЦИОНАЛЬНЫЕ ЗАВИСИМОСТИ (OPT DEPENDS, ${_aurDetails!.optDepends.length})'),
|
||||
const SizedBox(height: 10),
|
||||
_buildDepChips(
|
||||
theme,
|
||||
_aurDetails!.optDepends,
|
||||
chipColor: CupertinoColors.systemTeal.withValues(alpha: 0.12),
|
||||
borderColor: CupertinoColors.systemTeal.withValues(alpha: 0.35),
|
||||
textColor: CupertinoColors.systemTeal,
|
||||
),
|
||||
],
|
||||
|
||||
if (_aurDetails != null && _aurDetails!.conflicts.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('КОНФЛИКТУЕТ С (${_aurDetails!.conflicts.length})'),
|
||||
const SizedBox(height: 10),
|
||||
_buildDepChips(
|
||||
theme,
|
||||
_aurDetails!.conflicts,
|
||||
chipColor: CupertinoColors.systemRed.withValues(alpha: 0.12),
|
||||
borderColor: CupertinoColors.systemRed.withValues(alpha: 0.35),
|
||||
textColor: CupertinoColors.systemRed,
|
||||
),
|
||||
],
|
||||
|
||||
if (_aurDetails != null && _aurDetails!.provides.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('ПРЕДОСТАВЛЯЕТ (${_aurDetails!.provides.length})'),
|
||||
const SizedBox(height: 10),
|
||||
_buildDepChips(
|
||||
theme,
|
||||
_aurDetails!.provides,
|
||||
chipColor: CupertinoColors.systemGreen.withValues(alpha: 0.12),
|
||||
borderColor: CupertinoColors.systemGreen.withValues(alpha: 0.35),
|
||||
textColor: CupertinoColors.systemGreen,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart' show Divider, SelectableText;
|
||||
import 'package:macos_ui/macos_ui.dart';
|
||||
import '../../services/admin_service.dart';
|
||||
import '../../services/app_settings_service.dart';
|
||||
import '../../services/component_installer_service.dart';
|
||||
import 'reflector_mirror_dialog.dart';
|
||||
|
||||
class SetupWizardDialog extends StatefulWidget {
|
||||
const SetupWizardDialog({super.key});
|
||||
|
||||
static Future<void> show(BuildContext context) {
|
||||
return showMacosSheet(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const SetupWizardDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<SetupWizardDialog> createState() => _SetupWizardDialogState();
|
||||
}
|
||||
|
||||
class _SetupWizardDialogState extends State<SetupWizardDialog> {
|
||||
bool _isLoading = true;
|
||||
SystemComponentStatus? _status;
|
||||
bool _isInstalling = false;
|
||||
String? _installingTitle;
|
||||
final List<String> _installLogs = [];
|
||||
final ScrollController _logScrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _refreshStatus() async {
|
||||
setState(() => _isLoading = true);
|
||||
final status = await ComponentInstallerService.checkComponents();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_status = status;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_logScrollController.hasClients) {
|
||||
_logScrollController.animateTo(
|
||||
_logScrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _installFlatpak() async {
|
||||
final password = await AdminService.promptPassword(
|
||||
context,
|
||||
title: 'Установка Flatpak & Flathub',
|
||||
reason: 'Для установки пакета flatpak через pacman требуются права администратора.',
|
||||
);
|
||||
if (password == null) return;
|
||||
|
||||
setState(() {
|
||||
_isInstalling = true;
|
||||
_installingTitle = 'Установка Flatpak & Flathub';
|
||||
_installLogs.clear();
|
||||
});
|
||||
|
||||
try {
|
||||
await for (final line in ComponentInstallerService.installFlatpak(password)) {
|
||||
if (mounted) {
|
||||
setState(() => _installLogs.add(line));
|
||||
_scrollToBottom();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _installLogs.add('❌ Ошибка: $e'));
|
||||
}
|
||||
} finally {
|
||||
await _refreshStatus();
|
||||
if (mounted) {
|
||||
setState(() => _isInstalling = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _installParu() async {
|
||||
final password = await AdminService.promptPassword(
|
||||
context,
|
||||
title: 'Установка Paru (AUR Helper)',
|
||||
reason: 'Для установки сборочных зависимостей требуются права администратора.',
|
||||
);
|
||||
if (password == null) return;
|
||||
|
||||
setState(() {
|
||||
_isInstalling = true;
|
||||
_installingTitle = 'Установка Paru (AUR Helper)';
|
||||
_installLogs.clear();
|
||||
});
|
||||
|
||||
try {
|
||||
await for (final line in ComponentInstallerService.installParu(password)) {
|
||||
if (mounted) {
|
||||
setState(() => _installLogs.add(line));
|
||||
_scrollToBottom();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _installLogs.add('❌ Ошибка: $e'));
|
||||
}
|
||||
} finally {
|
||||
await _refreshStatus();
|
||||
if (mounted) {
|
||||
setState(() => _isInstalling = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _finishSetup() async {
|
||||
await AppSettingsService.instance.setHasCompletedSetupWizard(true);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = MacosTheme.of(context);
|
||||
final deName = AppSettingsService.detectDesktopEnvironmentName();
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 680,
|
||||
height: 620,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.canvasColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: CupertinoColors.white.withValues(alpha: 0.12)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CupertinoColors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 32,
|
||||
offset: const Offset(0, 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Шапка
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 22, 24, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0A84FF).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF0A84FF).withValues(alpha: 0.3)),
|
||||
),
|
||||
child: const Center(
|
||||
child: MacosIcon(
|
||||
CupertinoIcons.cube_box_fill,
|
||||
color: Color(0xFF0A84FF),
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Добро пожаловать в Forge',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
'Мастер первичной настройки системы • Окружение: Arch Linux ($deName)',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1, color: CupertinoColors.separator),
|
||||
|
||||
// Контент
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: ProgressCircle(radius: 16))
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(22),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'ИСТОЧНИКИ ПАКЕТОВ И ИНТЕГРАЦИИ',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.8,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 1. Flatpak & Flathub
|
||||
_buildComponentCard(
|
||||
theme: theme,
|
||||
icon: CupertinoIcons.app_badge_fill,
|
||||
iconColor: const Color(0xFF34C759),
|
||||
title: 'Flatpak & Flathub',
|
||||
subtitle: 'Песочница приложений: доступ к тысячам графических программ из Flathub',
|
||||
statusText: (_status?.hasFlatpak ?? false)
|
||||
? ((_status?.hasFlathub ?? false) ? 'Установлен (Flathub активен)' : 'Установлен (без Flathub)')
|
||||
: 'Не установлен',
|
||||
isInstalled: _status?.hasFlatpak ?? false,
|
||||
actionButton: PushButton(
|
||||
controlSize: ControlSize.small,
|
||||
secondary: _status?.hasFlatpak ?? false,
|
||||
onPressed: _isInstalling ? null : _installFlatpak,
|
||||
child: Text((_status?.hasFlatpak ?? false) ? 'Перенастроить Flathub' : 'Установить Flatpak'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 2. AUR & Paru
|
||||
_buildComponentCard(
|
||||
theme: theme,
|
||||
icon: CupertinoIcons.flame_fill,
|
||||
iconColor: const Color(0xFFFF9500),
|
||||
title: 'AUR и Paru (Rust AUR Helper)',
|
||||
subtitle: 'Сборка пакетов сообщества из Arch User Repository с максимальной скоростью',
|
||||
statusText: (_status?.hasParu ?? false)
|
||||
? 'Paru установлен (активен)'
|
||||
: ((_status?.hasYay ?? false) ? 'Обнаружен yay (Paru не установлен)' : 'AUR-хелпер отсутствует'),
|
||||
isInstalled: (_status?.hasParu ?? false) || (_status?.hasYay ?? false),
|
||||
actionButton: PushButton(
|
||||
controlSize: ControlSize.small,
|
||||
secondary: _status?.hasParu ?? false,
|
||||
onPressed: _isInstalling ? null : _installParu,
|
||||
child: Text((_status?.hasParu ?? false) ? 'Переустановить Paru' : 'Установить Paru'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const Text(
|
||||
'СТИЛЬ И ЗЕРКАЛА',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.8,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 3. Выбор оформления
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: theme.dividerColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const MacosIcon(CupertinoIcons.paintbrush_fill, size: 20, color: Color(0xFF5856D6)),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Стиль окна приложения',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Адаптировать под текущее графическое окружение',
|
||||
style: TextStyle(fontSize: 11.5, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MacosPopupButton<String>(
|
||||
value: AppSettingsService.instance.windowTheme,
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
AppSettingsService.instance.setWindowTheme(val);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
items: const [
|
||||
MacosPopupMenuItem(value: 'auto', child: Text('Автоматически')),
|
||||
MacosPopupMenuItem(value: 'kde', child: Text('KDE Breeze')),
|
||||
MacosPopupMenuItem(value: 'gnome', child: Text('GNOME Adwaita')),
|
||||
MacosPopupMenuItem(value: 'macos', child: Text('macOS Cupertino')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 4. Оптимизация зеркал
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: theme.dividerColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const MacosIcon(CupertinoIcons.speedometer, size: 20, color: Color(0xFF0A84FF)),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Зеркала репозиториев (Reflector)',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Протестировать пинг и выбрать самые быстрые серверы',
|
||||
style: TextStyle(fontSize: 11.5, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PushButton(
|
||||
controlSize: ControlSize.small,
|
||||
secondary: true,
|
||||
onPressed: () => ReflectorMirrorDialog.show(context),
|
||||
child: const Text('Настроить зеркала'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Терминал логов при установке
|
||||
if (_installLogs.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
_installingTitle != null ? 'ЛОГ ОПЕРАЦИИ: $_installingTitle' : 'ЛОГ УСТАНОВКИ',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.8,
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 120,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.black.withValues(alpha: 0.75),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: CupertinoColors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: ListView.builder(
|
||||
controller: _logScrollController,
|
||||
itemCount: _installLogs.length,
|
||||
itemBuilder: (ctx, idx) => SelectableText(
|
||||
_installLogs[idx],
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: Color(0xFFE0E0E0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1, color: CupertinoColors.separator),
|
||||
|
||||
// Подвал с кнопками
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
PushButton(
|
||||
controlSize: ControlSize.regular,
|
||||
secondary: true,
|
||||
onPressed: _isInstalling ? null : _finishSetup,
|
||||
child: const Text('Настроить позже'),
|
||||
),
|
||||
const Spacer(),
|
||||
PushButton(
|
||||
controlSize: ControlSize.regular,
|
||||
onPressed: _isInstalling ? null : _finishSetup,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isInstalling) ...[
|
||||
const ProgressCircle(radius: 6),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
const Text('Завершить настройку'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildComponentCard({
|
||||
required MacosThemeData theme,
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required String statusText,
|
||||
required bool isInstalled,
|
||||
required Widget actionButton,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: theme.dividerColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(child: MacosIcon(icon, color: iconColor, size: 20)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isInstalled
|
||||
? CupertinoColors.systemGreen.withValues(alpha: 0.15)
|
||||
: CupertinoColors.systemGrey.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isInstalled ? CupertinoColors.systemGreen : CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 11.5, color: CupertinoColors.systemGrey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
actionButton,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:macos_ui/macos_ui.dart';
|
||||
import 'package:forge/src/ui/widgets/setup_wizard_dialog.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('SetupWizardDialog renders initial setup content', (WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1200, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(
|
||||
const MacosApp(
|
||||
home: MacosWindow(
|
||||
child: SetupWizardDialog(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
});
|
||||
for (int i = 0; i < 5; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
expect(find.text('Flatpak & Flathub'), findsOneWidget);
|
||||
expect(find.text('AUR и Paru (Rust AUR Helper)'), findsOneWidget);
|
||||
expect(find.text('СТИЛЬ И ЗЕРКАЛА'), findsOneWidget);
|
||||
expect(find.text('Завершить настройку'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,7 @@ void main() {
|
||||
await tester.runAsync(() async {
|
||||
final catalogItem = find.text('Каталог');
|
||||
expect(catalogItem, findsOneWidget);
|
||||
await tester.tap(catalogItem);
|
||||
await tester.tap(catalogItem, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
await tester.pump();
|
||||
|
||||
Reference in New Issue
Block a user