web file transfer (#9587)

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages 2024-10-12 09:03:13 +08:00 committed by GitHub
parent 29b01e9cef
commit eb1ef0969c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 411 additions and 194 deletions

View File

@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
@ -2370,18 +2371,33 @@ connect(BuildContext context, String id,
}
} else {
if (isFileTransfer) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
if (isAndroid) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
}
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
if (isWeb) {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
desktop_file_manager.FileManagerPage(
id: id,
password: password,
isSharedPassword: isSharedPassword),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
} else {
if (isWeb) {
Navigator.push(

View File

@ -879,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
if (!isWeb) _transferFileAction(context),
_transferFileAction(context),
];
final List favs = (await bind.mainGetFav()).toList();

View File

@ -17,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
@ -55,14 +57,14 @@ class FileManagerPage extends StatefulWidget {
required this.id,
required this.password,
required this.isSharedPassword,
required this.tabController,
this.tabController,
this.forceRelay})
: super(key: key);
final String id;
final String? password;
final bool? isSharedPassword;
final bool? forceRelay;
final DesktopTabController tabController;
final DesktopTabController? tabController;
@override
State<StatefulWidget> createState() => _FileManagerPageState();
@ -97,11 +99,14 @@ class _FileManagerPageState extends State<FileManagerPage>
if (!isLinux) {
WakelockPlus.enable();
}
if (isWeb) {
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
}
debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
widget.tabController?.onSelected?.call(widget.id);
});
WidgetsBinding.instance.addObserver(this);
}
@ -140,10 +145,11 @@ class _FileManagerPageState extends State<FileManagerPage>
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row(
children: [
Flexible(
flex: 3,
child: dropArea(FileManagerView(
model.localController, _ffi, _mouseFocusScope))),
if (!isWeb)
Flexible(
flex: 3,
child: dropArea(FileManagerView(
model.localController, _ffi, _mouseFocusScope))),
Flexible(
flex: 3,
child: dropArea(FileManagerView(
@ -192,7 +198,13 @@ class _FileManagerPageState extends State<FileManagerPage>
return Icon(Icons.delete_outline, color: color);
default:
return Transform.rotate(
angle: job.isRemoteToLocal ? pi : 0,
angle: isWeb
? job.isRemoteToLocal
? pi / 2
: pi / 2 * 3
: job.isRemoteToLocal
? pi
: 0,
child: Icon(Icons.arrow_forward_ios, color: color),
);
}
@ -800,6 +812,50 @@ class _FileManagerViewState extends State<FileManagerView> {
],
),
),
if (isWeb)
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
isLocal
? EdgeInsets.only(left: 10)
: EdgeInsets.only(right: 10)),
backgroundColor: MaterialStateProperty.all(
selectedItems.items.isEmpty
? MyTheme.accent80
: MyTheme.accent,
),
),
onPressed: () => {webselectFiles(is_folder: true)},
icon: Offstage(),
label: Text(
translate('Upload folder'),
textAlign: TextAlign.right,
style: TextStyle(
color: Colors.white,
),
))).marginOnly(left: 16),
if (isWeb)
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
isLocal
? EdgeInsets.only(left: 10)
: EdgeInsets.only(right: 10)),
backgroundColor: MaterialStateProperty.all(
selectedItems.items.isEmpty
? MyTheme.accent80
: MyTheme.accent,
),
),
onPressed: () => {webselectFiles(is_folder: false)},
icon: Offstage(),
label: Text(
translate('Upload files'),
textAlign: TextAlign.right,
style: TextStyle(
color: Colors.white,
),
))).marginOnly(left: 16),
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
@ -833,19 +889,22 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white,
),
)
: RotatedBox(
quarterTurns: 2,
child: SvgPicture.asset(
"assets/arrow.svg",
colorFilter: svgColor(selectedItems.items.isEmpty
? Theme.of(context).brightness ==
Brightness.light
? MyTheme.grayBg
: MyTheme.darkGray
: Colors.white),
alignment: Alignment.bottomRight,
),
),
: isWeb
? Offstage()
: RotatedBox(
quarterTurns: 2,
child: SvgPicture.asset(
"assets/arrow.svg",
colorFilter: svgColor(
selectedItems.items.isEmpty
? Theme.of(context).brightness ==
Brightness.light
? MyTheme.grayBg
: MyTheme.darkGray
: Colors.white),
alignment: Alignment.bottomRight,
),
),
label: isLocal
? SvgPicture.asset(
"assets/arrow.svg",
@ -857,7 +916,7 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white),
)
: Text(
translate('Receive'),
translate(isWeb ? 'Download' : 'Receive'),
style: TextStyle(
color: selectedItems.items.isEmpty
? Theme.of(context).brightness ==

View File

@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/utils/event_loop.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../consts.dart';
import 'model.dart';
@ -74,7 +76,7 @@ class FileModel {
Future<void> onReady() async {
await evtLoop.onReady();
await localController.onReady();
if (!isWeb) await localController.onReady();
await remoteController.onReady();
}
@ -86,7 +88,7 @@ class FileModel {
}
Future<void> refreshAll() async {
await localController.refresh();
if (!isWeb) await localController.refresh();
await remoteController.refresh();
}
@ -228,6 +230,33 @@ class FileModel {
);
}, useAnimation: false);
}
void onSelectedFiles(dynamic obj) {
localController.selectedItems.clear();
try {
int handleIndex = int.parse(obj['handleIndex']);
final file = jsonDecode(obj['file']);
var entry = Entry.fromJson(file);
entry.path = entry.name;
final otherSideData = remoteController.directoryData();
final toPath = otherSideData.directory.path;
final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden;
final jobID = jobController.addTransferJob(entry, false);
webSendLocalFiles(
handleIndex: handleIndex,
actId: jobID,
path: entry.path,
to: PathUtil.join(toPath, entry.name, isWindows),
fileNum: 0,
includeHidden: showHidden,
isRemote: false,
);
} catch (e) {
debugPrint("Failed to decode onSelectedFiles: $e");
}
}
}
class DirectoryData {
@ -462,7 +491,8 @@ class FileController {
to: PathUtil.join(toPath, from.name, isWindows),
fileNum: 0,
includeHidden: showHidden,
isRemote: isRemoteToLocal);
isRemote: isRemoteToLocal,
isDir: from.isDirectory);
debugPrint(
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
}
@ -489,7 +519,7 @@ class FileController {
} else if (item.isDirectory) {
title = translate("Not an empty directory");
dialogManager?.showLoading(translate("Waiting"));
final fd = await fileFetcher.fetchDirectoryRecursive(
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
jobID, item.path, items.isLocal, true);
if (fd.path.isEmpty) {
fd.path = item.path;
@ -809,7 +839,6 @@ class JobController {
job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']);
job.recvJobRes = true;
debugPrint("update job $id with $evt");
jobTable.refresh();
}
} catch (e) {
@ -1116,11 +1145,11 @@ class FileFetcher {
}
}
Future<FileDirectory> fetchDirectoryRecursive(
Future<FileDirectory> fetchDirectoryRecursiveToRemove(
int actID, String path, bool isLocal, bool showHidden) async {
// TODO test Recursive is show hidden default?
try {
await bind.sessionReadDirRecursive(
await bind.sessionReadDirToRemoveRecursive(
sessionId: sessionId,
actId: actID,
path: path,

View File

@ -390,6 +390,10 @@ class FfiModel with ChangeNotifier {
handleFollowCurrentDisplay(evt, sessionId, peerId);
} else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId);
} else if (name == "selected_files") {
if (isWeb) {
parent.target?.fileModel.onSelectedFiles(evt);
}
} else {
debugPrint('Event is not handled in the fixed branch: $name');
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
Future<void> webselectFiles({required bool is_folder}) async {
throw UnimplementedError("webselectFiles");
}
Future<void> webSendLocalFiles(
{required int handleIndex,
required int actId,
required String path,
required String to,
required int fileNum,
required bool includeHidden,
required bool isRemote}) {
throw UnimplementedError("webSendLocalFiles");
}

View File

@ -0,0 +1,30 @@
import 'dart:async';
import 'dart:convert';
import 'dart:js' as js;
Future<void> webselectFiles({required bool is_folder}) async {
return Future(
() => js.context.callMethod('setByName', ['select_files', is_folder]));
}
Future<void> webSendLocalFiles(
{required int handleIndex,
required int actId,
required String path,
required String to,
required int fileNum,
required bool includeHidden,
required bool isRemote}) {
return Future(() => js.context.callMethod('setByName', [
'send_local_files',
jsonEncode({
'id': actId,
'handle_index': handleIndex,
'path': path,
'to': to,
'file_num': fileNum,
'include_hidden': includeHidden,
'is_remote': isRemote,
})
]));
}

View File

@ -602,6 +602,7 @@ pub fn session_send_files(
file_num: i32,
include_hidden: bool,
is_remote: bool,
_is_dir: bool,
) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.send_files(act_id, path, to, file_num, include_hidden, is_remote);
@ -633,7 +634,7 @@ pub fn session_remove_file(
}
}
pub fn session_read_dir_recursive(
pub fn session_read_dir_to_remove_recursive(
session_id: SessionID,
act_id: i32,
path: String,