Fix/windows install directory confirmation (#16783)

* fix(windows): warn before removing installation directory

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(windows): show inline installation directory warning

Signed-off-by: fufesou <linlong1266@gmail.com>

* refactor(windows): simplify installation directory warning

Key the warning widget by its path and remove the optional widget test suite.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(windows): limit installation warnings to the selected directory

Share install-directory-not-empty-tip between inline and confirmation warnings, and remove the separate previous-installation warning.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(windows): skip directory warnings when reinstalling in place

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou authored and GitHub committed 2026-11-01 13:39:54 +08:00
1 parent fa611556f8
commit 82301f1776
56 files changed
+201 -21

No files matched your search

+51 -21
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/widgets/install_directory_confirmation.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -164,27 +165,46 @@ class _InstallPageBodyState extends State<_InstallPageBody>
children: [
Text(translate('Installation'),
style: Theme.of(context).textTheme.headlineMedium),
Row(
Table(
columnWidths: const {
0: IntrinsicColumnWidth(),
2: IntrinsicColumnWidth(),
},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
Text('${translate('Installation Path')}:')
.marginOnly(right: 10),
Expanded(
child: TextField(
controller: controller,
readOnly: true,
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
TableRow(
children: [
Text('${translate('Installation Path')}:')
.marginOnly(right: 10),
TextField(
controller: controller,
readOnly: true,
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
),
).workaroundFreezeLinuxMint().marginOnly(right: 10),
Obx(
() => OutlinedButton.icon(
icon: Icon(Icons.folder_outlined, size: 16),
onPressed:
btnEnabled.value ? selectInstallPath : null,
style: buttonStyle,
label: Text(translate('Change Path')),
),
),
).workaroundFreezeLinuxMint().marginOnly(right: 10),
],
),
TableRow(
children: [
const SizedBox.shrink(),
ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, child) =>
InstallDirectoryWarning(directoryPath: value.text),
).marginOnly(right: 10),
const SizedBox.shrink(),
],
),
Obx(
() => OutlinedButton.icon(
icon: Icon(Icons.folder_outlined, size: 16),
onPressed: btnEnabled.value ? selectInstallPath : null,
style: buttonStyle,
label: Text(translate('Change Path')),
),
)
],
).marginSymmetric(vertical: 2 * em),
Option(startmenu, label: 'Create start menu shortcuts')
@@ -277,7 +297,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
));
}
void install() {
Future<void> install() async {
if (!btnEnabled.value) return;
final installPath = controller.text;
do_install() {
btnEnabled.value = false;
cancelEnabled.value = false;
@@ -286,10 +308,18 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (startmenu.value) args += ' startmenu';
if (desktopicon.value) args += ' desktopicon';
if (printer.value) args += ' printer';
bind.installInstallMe(options: args, path: controller.text);
bind.installInstallMe(options: args, path: installPath);
}
do_install();
btnEnabled.value = false;
try {
final confirmed =
await confirmInstallDirectory(this.context, targetPath: installPath);
if (!mounted || !confirmed) return;
do_install();
} finally {
if (mounted && !showProgress.value) btnEnabled.value = true;
}
}
void selectInstallPath() async {
@@ -0,0 +1,96 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:path/path.dart' as path;
const _nonemptyDirectoryWarning = 'install-directory-not-empty-tip';
Future<bool> confirmInstallDirectory(BuildContext context,
{required String targetPath}) async {
try {
final needsWarning = await _needsDirectoryWarning(targetPath);
if (!context.mounted) return false;
if (!needsWarning) return true;
return await _showDirectoryDialog(
context, '${translate(_nonemptyDirectoryWarning)}\n$targetPath');
} on FileSystemException catch (error) {
debugPrint('Failed to inspect installation directory: $error');
if (context.mounted) {
await _showDirectoryDialog(context, error.toString(), isError: true);
}
return false;
}
}
Future<bool> _needsDirectoryWarning(String directoryPath) async {
if (bind.mainIsInstalled() &&
path.equals(directoryPath, bind.installInstallPath())) {
return false;
}
final directory = Directory(directoryPath);
return await directory.exists() &&
!await directory.list(followLinks: false).isEmpty;
}
Future<bool> _showDirectoryDialog(BuildContext context, String message,
{bool isError = false}) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
scrollable: true,
title: Text(translate(isError ? 'Error' : 'Warning')),
content: SelectableText(message),
actions: [
dialogButton(isError ? 'OK' : 'Cancel',
isOutline: !isError,
onPressed: () => Navigator.of(dialogContext).pop(false)),
if (!isError)
dialogButton('Continue',
onPressed: () => Navigator.of(dialogContext).pop(true)),
],
),
);
return confirmed == true;
}
class InstallDirectoryWarning extends StatefulWidget {
InstallDirectoryWarning({required this.directoryPath})
: super(key: ValueKey(directoryPath));
final String directoryPath;
@override
State<InstallDirectoryWarning> createState() =>
_InstallDirectoryWarningState();
}
class _InstallDirectoryWarningState extends State<InstallDirectoryWarning> {
static const _spacing = 8.0;
late final Future<bool> _needsWarning =
_needsDirectoryWarning(widget.directoryPath);
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _needsWarning,
builder: (context, snapshot) {
if (!snapshot.hasError && snapshot.data != true) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: _spacing),
child: Text(
snapshot.hasError
? '${translate('Error')}: ${snapshot.error}'
: translate(_nonemptyDirectoryWarning),
style: TextStyle(
color: snapshot.hasError
? Theme.of(context).colorScheme.error
: Colors.orange.shade700)),
);
},
);
}
}