Merge branch 'master'

This commit is contained in:
NicKoehler
2023-02-26 10:12:52 +01:00
77 changed files with 4121 additions and 1618 deletions

View File

@@ -151,10 +151,7 @@ class _ConnectionPageState extends State<ConnectionPage>
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
var id = _idController.id;
var forceRelay = id.endsWith(r'/r');
if (forceRelay) id = id.substring(0, id.length - 2);
connect(context, id,
isFileTransfer: isFileTransfer, forceRelay: forceRelay);
connect(context, id, isFileTransfer: isFileTransfer);
}
/// UI for the remote ID TextField.

View File

@@ -319,7 +319,7 @@ class _GeneralState extends State<_General> {
bind.mainSetOption(key: 'audio-input', value: device);
}
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (Platform.isWindows) {
devices.insert(0, 'System Sound');
@@ -346,7 +346,7 @@ class _GeneralState extends State<_General> {
}
Widget record(BuildContext context) {
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
String customDirectory =
await bind.mainGetOption(key: 'video-save-directory');
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
@@ -399,7 +399,7 @@ class _GeneralState extends State<_General> {
}
Widget language() {
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
String langs = await bind.mainGetLangs();
String lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
return {'langs': langs, 'lang': lang};
@@ -487,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget _permissions(context, bool stopService) {
bool enabled = !locked;
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
return await bind.mainGetOption(key: 'access-mode');
}(), hasData: (data) {
String accessMode = data! as String;
@@ -744,7 +744,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return [
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
update: update, enabled: !locked),
_futureBuilder(
futureBuilder(
future: () async {
String enabled = await bind.mainGetOption(key: 'direct-server');
String port = await bind.mainGetOption(key: 'direct-access-port');
@@ -805,7 +805,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget whitelist() {
bool enabled = !locked;
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
return await bind.mainGetOption(key: 'whitelist');
}(), hasData: (data) {
RxBool hasWhitelist = (data as String).isNotEmpty.obs;
@@ -931,7 +931,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
server(bool enabled) {
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
return await bind.mainGetOptions();
}(), hasData: (data) {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
@@ -1366,7 +1366,7 @@ class _About extends StatefulWidget {
class _AboutState extends State<_About> {
@override
Widget build(BuildContext context) {
return _futureBuilder(future: () async {
return futureBuilder(future: () async {
final license = await bind.mainGetLicense();
final version = await bind.mainGetVersion();
final buildDate = await bind.mainGetBuildDate();
@@ -1500,7 +1500,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
bool enabled = true,
Icon? checkedIcon,
bool? fakeValue}) {
return _futureBuilder(
return futureBuilder(
future: bind.mainGetOption(key: key),
hasData: (data) {
bool value = option2bool(key, data.toString());
@@ -1633,22 +1633,6 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
).marginOnly(left: _kContentHSubMargin);
}
Widget _futureBuilder(
{required Future? future, required Widget Function(dynamic data) hasData}) {
return FutureBuilder(
future: future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return hasData(snapshot.data!);
} else {
if (snapshot.hasError) {
debugPrint(snapshot.error.toString());
}
return Container();
}
});
}
Widget _lock(
bool locked,
String label,

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/gestures.dart';
@@ -78,6 +79,10 @@ class _FileManagerPageState extends State<FileManagerPage>
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
final _listSearchBufferLocal = TimeoutStringBuffer();
final _listSearchBufferRemote = TimeoutStringBuffer();
final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs;
final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs;
final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs;
final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs;
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime =
@@ -297,11 +302,12 @@ class _FileManagerPageState extends State<FileManagerPage>
}
var searchResult = entries
.skip(skipCount)
.where((element) => element.name.startsWith(buffer));
.where((element) => element.name.toLowerCase().startsWith(buffer));
if (searchResult.isEmpty) {
// cannot find next, lets restart search from head
debugPrint("restart search from head");
searchResult =
entries.where((element) => element.name.startsWith(buffer));
entries.where((element) => element.name.toLowerCase().startsWith(buffer));
}
if (searchResult.isEmpty) {
setState(() {
@@ -310,13 +316,13 @@ class _FileManagerPageState extends State<FileManagerPage>
return;
}
_jumpToEntry(isLocal, searchResult.first, scrollController,
kDesktopFileTransferRowHeight, buffer);
kDesktopFileTransferRowHeight);
},
onSearch: (buffer) {
debugPrint("searching for $buffer");
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
entries.where((element) => element.name.startsWith(buffer));
entries.where((element) => element.name.toLowerCase().startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
setState(() {
@@ -325,7 +331,7 @@ class _FileManagerPageState extends State<FileManagerPage>
return;
}
_jumpToEntry(isLocal, searchResult.first, scrollController,
kDesktopFileTransferRowHeight, buffer);
kDesktopFileTransferRowHeight);
},
child: ObxValue<RxString>(
(searchText) {
@@ -362,37 +368,41 @@ class _FileManagerPageState extends State<FileManagerPage>
child: Row(
children: [
GestureDetector(
child: Container(
width: kDesktopFileTransferNameColWidth,
child: Tooltip(
waitDuration:
Duration(milliseconds: 500),
message: entry.name,
child: Row(children: [
entry.isDrive
? Image(
image: iconHardDrive,
fit: BoxFit.scaleDown,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7))
.paddingAll(4)
: SvgPicture.asset(
entry.isFile
? "assets/file.svg"
: "assets/folder.svg",
color: Theme.of(context)
.tabBarTheme
.labelColor,
),
Expanded(
child: Text(
entry.name.nonBreaking,
overflow:
TextOverflow.ellipsis))
]),
)),
child: Obx(
() => Container(
width: isLocal
? _nameColWidthLocal.value
: _nameColWidthRemote.value,
child: Tooltip(
waitDuration:
Duration(milliseconds: 500),
message: entry.name,
child: Row(children: [
entry.isDrive
? Image(
image: iconHardDrive,
fit: BoxFit.scaleDown,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7))
.paddingAll(4)
: SvgPicture.asset(
entry.isFile
? "assets/file.svg"
: "assets/folder.svg",
color: Theme.of(context)
.tabBarTheme
.labelColor,
),
Expanded(
child: Text(
entry.name.nonBreaking,
overflow:
TextOverflow.ellipsis))
]),
)),
),
onTap: () {
final items = getSelectedItems(isLocal);
// handle double click
@@ -406,24 +416,35 @@ class _FileManagerPageState extends State<FileManagerPage>
items, filteredEntries, entry, isLocal);
},
),
SizedBox(
width: 2.0,
),
GestureDetector(
child: SizedBox(
width: kDesktopFileTransferModifiedColWidth,
child: Tooltip(
waitDuration:
Duration(milliseconds: 500),
message: lastModifiedStr,
child: Text(
lastModifiedStr,
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
)),
child: Obx(
() => SizedBox(
width: isLocal
? _modifiedColWidthLocal.value
: _modifiedColWidthRemote.value,
child: Tooltip(
waitDuration:
Duration(milliseconds: 500),
message: lastModifiedStr,
child: Text(
lastModifiedStr,
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
)),
),
),
),
// Divider from header.
SizedBox(
width: 100,
width: 2.0,
),
Expanded(
// width: 100,
child: GestureDetector(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
@@ -450,7 +471,11 @@ class _FileManagerPageState extends State<FileManagerPage>
return Column(
children: [
// Header
_buildFileBrowserHeader(context, isLocal),
Row(
children: [
Expanded(child: _buildFileBrowserHeader(context, isLocal)),
],
),
// Body
Expanded(
child: ListView.builder(
@@ -472,7 +497,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}
void _jumpToEntry(bool isLocal, Entry entry,
ScrollController scrollController, double rowHeight, String buffer) {
ScrollController scrollController, double rowHeight) {
final entries = model.getCurrentDir(isLocal).entries;
final index = entries.indexOf(entry);
if (index == -1) {
@@ -480,7 +505,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
entries.where((element) => element.name.startsWith(buffer));
entries.where((element) => element == entry);
selectedEntries.clear();
if (searchResult.isEmpty) {
return;
@@ -1396,17 +1421,23 @@ class _FileManagerPageState extends State<FileManagerPage>
height: kDesktopFileTransferHeaderHeight,
child: Row(
children: [
Text(
name,
style: headerTextStyle,
).marginSymmetric(horizontal: 4),
ascending.value != null
Flexible(
flex: 2,
child: Text(
name,
style: headerTextStyle,
overflow: TextOverflow.ellipsis,
).marginSymmetric(horizontal: 4),
),
Flexible(
flex: 1,
child: ascending.value != null
? Icon(
ascending.value!
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
)
: const Offstage()
: const Offstage())
],
),
),
@@ -1420,16 +1451,48 @@ class _FileManagerPageState extends State<FileManagerPage>
}
Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) {
return Row(
children: [
headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name,
translate("Name"), isLocal),
headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified,
translate("Modified"), isLocal),
Expanded(
child:
headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
],
final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote;
final modifiedColWidth =
isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote;
final padding = EdgeInsets.all(1.0);
return SizedBox(
height: kDesktopFileTransferHeaderHeight,
child: Row(
children: [
Obx(
() => headerItemFunc(
nameColWidth.value, SortBy.name, translate("Name"), isLocal),
),
DraggableDivider(
axis: Axis.vertical,
onPointerMove: (dx) {
nameColWidth.value += dx;
nameColWidth.value = min(
kDesktopFileTransferMaximumWidth,
max(kDesktopFileTransferMinimumWidth,
nameColWidth.value));
},
padding: padding,
),
Obx(
() => headerItemFunc(modifiedColWidth.value, SortBy.modified,
translate("Modified"), isLocal),
),
DraggableDivider(
axis: Axis.vertical,
onPointerMove: (dx) {
modifiedColWidth.value += dx;
modifiedColWidth.value = min(
kDesktopFileTransferMaximumWidth,
max(kDesktopFileTransferMinimumWidth,
modifiedColWidth.value));
},
padding: padding),
Expanded(
child:
headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
],
),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
class DraggableDivider extends StatefulWidget {
final Axis axis;
final double thickness;
final Color color;
final Function(double)? onPointerMove;
final VoidCallback? onHover;
final EdgeInsets padding;
const DraggableDivider({
super.key,
this.axis = Axis.horizontal,
this.thickness = 1.0,
this.color = const Color.fromARGB(200, 177, 175, 175),
this.onPointerMove,
this.padding = const EdgeInsets.symmetric(horizontal: 1.0),
this.onHover,
});
@override
State<DraggableDivider> createState() => _DraggableDividerState();
}
class _DraggableDividerState extends State<DraggableDivider> {
@override
Widget build(BuildContext context) {
return Listener(
onPointerMove: (event) {
final dl =
widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx;
widget.onPointerMove?.call(dl);
},
onPointerHover: (event) => widget.onHover?.call(),
child: MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: Padding(
padding: widget.padding,
child: Container(
decoration: BoxDecoration(color: widget.color),
width: widget.axis == Axis.horizontal
? double.infinity
: widget.thickness,
height: widget.axis == Axis.horizontal
? widget.thickness
: double.infinity,
),
),
),
);
}
}

View File

@@ -55,6 +55,7 @@ class TimeoutStringBuffer {
}
ListSearchAction input(String ch) {
ch = ch.toLowerCase();
final curr = DateTime.now();
try {
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {

File diff suppressed because it is too large Load Diff