Merge pull request #1740 from Heap-Hop/master

update file transfer and chat page
This commit is contained in:
RustDesk 2022-10-18 08:53:08 +08:00 committed by GitHub
commit b8c9113ee5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 335 additions and 259 deletions

View File

@ -36,7 +36,7 @@ class DraggableChatWindow extends StatelessWidget {
appBar: CustomAppBar(
onPanUpdate: onPanUpdate,
appBar: isDesktop
? _buildDesktopAppBar()
? _buildDesktopAppBar(context)
: _buildMobileAppBar(context),
),
body: ChatPage(chatModel: chatModel),
@ -82,33 +82,33 @@ class DraggableChatWindow extends StatelessWidget {
);
}
Widget _buildDesktopAppBar() {
Widget _buildDesktopAppBar(BuildContext context) {
return Container(
color: MyTheme.accent50,
height: 35,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).hintColor.withOpacity(0.4)))),
height: 38,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Text(
translate("Chat"),
style: const TextStyle(
color: Colors.white,
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold),
)),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ActionIcon(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
child: Row(children: [
Icon(Icons.chat_bubble_outline,
size: 20, color: Theme.of(context).colorScheme.primary),
SizedBox(width: 6),
Text(translate("Chat"))
])),
Padding(
padding: EdgeInsets.all(2),
child: ActionIcon(
message: 'Close',
icon: IconFont.close,
onTap: chatModel.hideChatWindowOverlay,
isClose: true,
)
],
)
size: 32,
))
],
),
);

View File

@ -31,6 +31,8 @@ const int kMobileMaxDisplayHeight = 1280;
const int kDesktopMaxDisplayWidth = 1920;
const int kDesktopMaxDisplayHeight = 1080;
const int kDesktopDoubleClickTimeMilli = 200;
const Size kConnectionManagerWindowSize = Size(300, 400);
// Tabbar transition duration, now we remove the duration
const Duration kTabTransitionDuration = Duration.zero;

View File

@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@ -44,15 +45,17 @@ class _FileManagerPageState extends State<FileManagerPage>
final _locationStatusLocal = LocationStatus.bread.obs;
final _locationStatusRemote = LocationStatus.bread.obs;
final FocusNode _locationNodeLocal =
FocusNode(debugLabel: "locationNodeLocal");
final FocusNode _locationNodeRemote =
FocusNode(debugLabel: "locationNodeRemote");
final _locationNodeLocal = FocusNode(debugLabel: "locationNodeLocal");
final _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote");
final _searchTextLocal = "".obs;
final _searchTextRemote = "".obs;
final _breadCrumbScrollerLocal = ScrollController();
final _breadCrumbScrollerRemote = ScrollController();
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime = DateTime.now().millisecondsSinceEpoch;
Entry? _lastClickEntry;
final _dropMaskVisible = false.obs; // TODO impl drop mask
ScrollController getBreadCrumbScrollController(bool isLocal) {
@ -171,22 +174,6 @@ class _FileManagerPageState extends State<FileManagerPage>
}
Widget body({bool isLocal = false}) {
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
final sortIndex = (SortBy style) {
switch (style) {
case SortBy.Name:
return 0;
case SortBy.Type:
return 0;
case SortBy.Modified:
return 1;
case SortBy.Size:
return 2;
}
}(model.getSortStyle(isLocal));
final sortAscending =
isLocal ? model.localSortAscending : model.remoteSortAscending;
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
margin: const EdgeInsets.all(16.0),
@ -208,126 +195,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Expanded(
child: SingleChildScrollView(
controller: ScrollController(),
child: ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
return DataTable(
key: ValueKey(isLocal ? 0 : 1),
showCheckboxColumn: true,
dataRowHeight: 25,
headingRowHeight: 30,
horizontalMargin: 8,
columnSpacing: 8,
showBottomBorder: true,
sortColumnIndex: sortIndex,
sortAscending: sortAscending,
columns: [
DataColumn(
label: Text(
translate("Name"),
).marginSymmetric(horizontal: 4),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Name,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(
translate("Modified"),
),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Modified,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(translate("Size")),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Size,
isLocal: isLocal, ascending: ascending);
}),
],
rows: filteredEntries.map((entry) {
final sizeStr = entry.isFile
? readableFileSize(entry.size.toDouble())
: "";
return DataRow(
key: ValueKey(entry.name),
onSelectChanged: (s) {
if (s != null) {
if (s) {
getSelectedItem(isLocal)
.add(isLocal, entry);
} else {
getSelectedItem(isLocal).remove(entry);
}
setState(() {});
}
},
selected:
getSelectedItem(isLocal).contains(entry),
cells: [
DataCell(
Container(
width: 180,
child: Tooltip(
message: entry.name,
child: Row(children: [
Icon(
entry.isFile
? Icons.feed_outlined
: Icons.folder,
size: 20,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7),
).marginSymmetric(horizontal: 2),
Expanded(
child: Text(entry.name,
overflow:
TextOverflow.ellipsis))
]),
)), onTap: () {
if (entry.isDirectory) {
openDirectory(entry.path, isLocal: isLocal);
if (isLocal) {
_localSelectedItems.clear();
} else {
_remoteSelectedItems.clear();
}
} else {
// Perform file-related tasks.
final selectedItems =
getSelectedItem(isLocal);
if (selectedItems.contains(entry)) {
selectedItems.remove(entry);
} else {
selectedItems.add(isLocal, entry);
}
setState(() {});
}
}),
DataCell(FittedBox(
child: Text(
"${entry.lastModified().toString().replaceAll(".000", "")} ",
style: TextStyle(
fontSize: 12, color: MyTheme.darkGray),
))),
DataCell(Text(
sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10, color: MyTheme.darkGray),
)),
]);
}).toList(growable: false),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
),
child: _buildDataTable(context, isLocal),
),
)
],
@ -337,6 +205,183 @@ class _FileManagerPageState extends State<FileManagerPage>
);
}
Widget _buildDataTable(BuildContext context, bool isLocal) {
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
final sortIndex = (SortBy style) {
switch (style) {
case SortBy.Name:
return 0;
case SortBy.Type:
return 0;
case SortBy.Modified:
return 1;
case SortBy.Size:
return 2;
}
}(model.getSortStyle(isLocal));
final sortAscending =
isLocal ? model.localSortAscending : model.remoteSortAscending;
return ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
return DataTable(
key: ValueKey(isLocal ? 0 : 1),
showCheckboxColumn: false,
dataRowHeight: 25,
headingRowHeight: 30,
horizontalMargin: 8,
columnSpacing: 8,
showBottomBorder: true,
sortColumnIndex: sortIndex,
sortAscending: sortAscending,
columns: [
DataColumn(
label: Text(
translate("Name"),
).marginSymmetric(horizontal: 4),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Name,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(
translate("Modified"),
),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Modified,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(translate("Size")),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.Size,
isLocal: isLocal, ascending: ascending);
}),
],
rows: filteredEntries.map((entry) {
final sizeStr =
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
final lastModifiedStr =
"${entry.lastModified().toString().replaceAll(".000", "")} ";
return DataRow(
key: ValueKey(entry.name),
onSelectChanged: (s) {
_onSelectedChanged(getSelectedItem(isLocal), filteredEntries,
entry, isLocal);
setState(() {});
},
selected: getSelectedItem(isLocal).contains(entry),
cells: [
DataCell(
Container(
width: 200,
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: entry.name,
child: Row(children: [
Icon(
entry.isFile ? Icons.feed_outlined : Icons.folder,
size: 20,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7),
).marginSymmetric(horizontal: 2),
Expanded(
child: Text(entry.name,
overflow: TextOverflow.ellipsis))
]),
)),
onTap: () {
final items = getSelectedItem(isLocal);
// handle double click
if (_checkDoubleClick(entry)) {
openDirectory(entry.path, isLocal: isLocal);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
),
DataCell(FittedBox(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: lastModifiedStr,
child: Text(
lastModifiedStr,
style: TextStyle(
fontSize: 12, color: MyTheme.darkGray),
)))),
DataCell(Tooltip(
waitDuration: Duration(milliseconds: 500),
message: sizeStr,
child: Text(
sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10, color: MyTheme.darkGray),
))),
]);
}).toList(growable: false),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
);
}
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
Entry entry, bool isLocal) {
final isCtrlDown = RawKeyboard.instance.keysPressed
.contains(LogicalKeyboardKey.controlLeft);
final isShiftDown =
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft);
if (isCtrlDown) {
if (selectedItems.contains(entry)) {
selectedItems.remove(entry);
} else {
selectedItems.add(isLocal, entry);
}
} else if (isShiftDown) {
final List<int> indexGroup = [];
for (var selected in selectedItems.items) {
indexGroup.add(entries.indexOf(selected));
}
indexGroup.add(entries.indexOf(entry));
indexGroup.removeWhere((e) => e == -1);
final maxIndex = indexGroup.reduce(max);
final minIndex = indexGroup.reduce(min);
selectedItems.clear();
entries
.getRange(minIndex, maxIndex + 1)
.forEach((e) => selectedItems.add(isLocal, e));
} else {
selectedItems.clear();
selectedItems.add(isLocal, entry);
}
setState(() {});
}
bool _checkDoubleClick(Entry entry) {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
_lastClickTime = current;
if (_lastClickEntry == entry) {
if (elapsed < kDesktopDoubleClickTimeMilli) {
return true;
}
} else {
_lastClickEntry = entry;
}
return false;
}
/// transfer status list
/// watch transfer status
Widget statusList() {
@ -369,6 +414,7 @@ class _FileManagerPageState extends State<FileManagerPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tooltip(
waitDuration: Duration(milliseconds: 500),
message: item.jobName,
child: Text(
item.jobName,
@ -787,8 +833,10 @@ class _FileManagerPageState extends State<FileManagerPage>
onSearchText(String searchText, bool isLocal) {
if (isLocal) {
_localSelectedItems.clear();
_searchTextLocal.value = searchText;
} else {
_remoteSelectedItems.clear();
_searchTextRemote.value = searchText;
}
}

View File

@ -191,6 +191,8 @@ class DesktopTab extends StatelessWidget {
final DesktopTabController controller;
Rx<DesktopTabState> get state => controller.state;
final isMaximized = false.obs;
late final DesktopTabType tabType;
late final bool isMainWindow;
@ -299,8 +301,10 @@ class DesktopTab extends StatelessWidget {
width: 78,
)),
GestureDetector(
onDoubleTap: () =>
showMaximize ? toggleMaximize(isMainWindow) : null,
onDoubleTap: showMaximize
? () => toggleMaximize(isMainWindow)
.then((value) => isMaximized.value = value)
: null,
onPanStart: (_) => startDragging(isMainWindow),
child: Row(children: [
Offstage(
@ -333,6 +337,7 @@ class DesktopTab extends StatelessWidget {
tabType: tabType,
state: state,
tail: tail,
isMaximized: isMaximized,
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
@ -347,6 +352,7 @@ class WindowActionPanel extends StatefulWidget {
final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final RxBool isMaximized;
final bool showMinimize;
final bool showMaximize;
@ -359,6 +365,7 @@ class WindowActionPanel extends StatefulWidget {
required this.isMainWindow,
required this.tabType,
required this.state,
required this.isMaximized,
this.tail,
this.showMinimize = true,
this.showMaximize = true,
@ -374,30 +381,31 @@ class WindowActionPanel extends StatefulWidget {
class WindowActionPanelState extends State<WindowActionPanel>
with MultiWindowListener, WindowListener {
bool isMaximized = false;
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (isMaximized != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => isMaximized = maximized));
}
});
} else {
final wc = WindowController.fromWindowId(windowId!);
wc.isMaximized().then((maximized) {
if (isMaximized != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => isMaximized = maximized));
}
});
}
Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (widget.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized));
}
});
} else {
final wc = WindowController.fromWindowId(windowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (widget.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized));
}
});
}
});
}
@override
@ -410,8 +418,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override
void onWindowMaximize() {
// catch maximize from system
if (!isMaximized) {
setState(() => isMaximized = true);
if (!widget.isMaximized.value) {
widget.isMaximized.value = true;
}
super.onWindowMaximize();
}
@ -419,8 +427,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
@override
void onWindowUnmaximize() {
// catch unmaximize from system
if (isMaximized) {
setState(() => isMaximized = false);
if (widget.isMaximized.value) {
widget.isMaximized.value = false;
}
super.onWindowUnmaximize();
}
@ -452,12 +460,14 @@ class WindowActionPanelState extends State<WindowActionPanel>
)),
Offstage(
offstage: !widget.showMaximize,
child: ActionIcon(
message: isMaximized ? "Restore" : "Maximize",
icon: isMaximized ? IconFont.restore : IconFont.max,
onTap: _toggleMaximize,
isClose: false,
)),
child: Obx(() => ActionIcon(
message: widget.isMaximized.value ? "Restore" : "Maximize",
icon: widget.isMaximized.value
? IconFont.restore
: IconFont.max,
onTap: _toggleMaximize,
isClose: false,
))),
Offstage(
offstage: !widget.showClose,
child: ActionIcon(
@ -484,9 +494,9 @@ class WindowActionPanelState extends State<WindowActionPanel>
void _toggleMaximize() {
toggleMaximize(widget.isMainWindow).then((maximize) {
if (isMaximized != maximize) {
// setState for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
setState(() => isMaximized = !isMaximized);
if (widget.isMaximized.value != maximize) {
// update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
widget.isMaximized.value = maximize;
}
});
}
@ -801,13 +811,15 @@ class ActionIcon extends StatelessWidget {
final IconData icon;
final Function() onTap;
final bool isClose;
const ActionIcon({
Key? key,
required this.message,
required this.icon,
required this.onTap,
required this.isClose,
}) : super(key: key);
final double? size;
const ActionIcon(
{Key? key,
required this.message,
required this.icon,
required this.onTap,
required this.isClose,
this.size})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -822,8 +834,8 @@ class ActionIcon extends StatelessWidget {
onHover: (value) => hover.value = value,
onTap: onTap,
child: SizedBox(
height: _kTabBarHeight - 1,
width: _kTabBarHeight - 1,
height: size ?? (_kTabBarHeight - 1),
width: size ?? (_kTabBarHeight - 1),
child: Icon(
icon,
color: hover.value && isClose

View File

@ -61,19 +61,36 @@ class ChatPage extends StatelessWidget implements PageShape {
[],
inputOptions: InputOptions(
sendOnEnter: true,
inputDecoration: defaultInputDecoration(
hintText: "${translate('Write a message')}...",
fillColor: Theme.of(context).backgroundColor),
sendButtonBuilder: defaultSendButton(
color: Theme.of(context)
.textTheme
.titleLarge!
.color!),
inputTextStyle: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.titleLarge
?.color)),
?.color),
inputDecoration: isDesktop
? InputDecoration(
isDense: true,
hintText:
"${translate('Write a message')}...",
filled: true,
fillColor: Theme.of(context).backgroundColor,
contentPadding: EdgeInsets.all(10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(
width: 0,
style: BorderStyle.none,
),
),
)
: defaultInputDecoration(
hintText:
"${translate('Write a message')}...",
fillColor: Theme.of(context).backgroundColor),
sendButtonBuilder: defaultSendButton(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 0),
color: Theme.of(context).colorScheme.primary)),
messageOptions: MessageOptions(
showOtherUsersAvatar: false,
showTime: true,

View File

@ -555,50 +555,3 @@ class BottomSheetBody extends StatelessWidget {
);
}
}
class SelectedItems {
bool? _isLocal;
final List<Entry> _items = [];
List<Entry> get items => _items;
int get length => _items.length;
bool? get isLocal => _isLocal;
add(bool isLocal, Entry e) {
if (_isLocal == null) {
_isLocal = isLocal;
}
if (_isLocal != null && _isLocal != isLocal) {
return;
}
if (!_items.contains(e)) {
_items.add(e);
}
}
bool contains(Entry e) {
return _items.contains(e);
}
remove(Entry e) {
_items.remove(e);
if (_items.length == 0) {
_isLocal = null;
}
}
bool isOtherPage(bool currentIsLocal) {
if (_isLocal == null) {
return false;
} else {
return _isLocal != currentIsLocal;
}
}
clear() {
_items.clear();
_isLocal = null;
}
}

View File

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as Path;
@ -1123,6 +1122,51 @@ class DirectoryOption {
}
}
class SelectedItems {
bool? _isLocal;
final List<Entry> _items = [];
List<Entry> get items => _items;
int get length => _items.length;
bool? get isLocal => _isLocal;
add(bool isLocal, Entry e) {
_isLocal ??= isLocal;
if (_isLocal != null && _isLocal != isLocal) {
return;
}
if (!_items.contains(e)) {
_items.add(e);
}
}
bool contains(Entry e) {
return _items.contains(e);
}
remove(Entry e) {
_items.remove(e);
if (_items.length == 0) {
_isLocal = null;
}
}
bool isOtherPage(bool currentIsLocal) {
if (_isLocal == null) {
return false;
} else {
return _isLocal != currentIsLocal;
}
}
clear() {
_items.clear();
_isLocal = null;
}
}
// code from file_manager pkg after edit
List<Entry> _sortList(List<Entry> list, SortBy sortType, bool ascending) {
if (sortType == SortBy.Name) {