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

View File

@ -879,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); 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:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:wakelock_plus/wakelock_plus.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 '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
@ -55,14 +57,14 @@ class FileManagerPage extends StatefulWidget {
required this.id, required this.id,
required this.password, required this.password,
required this.isSharedPassword, required this.isSharedPassword,
required this.tabController, this.tabController,
this.forceRelay}) this.forceRelay})
: super(key: key); : super(key: key);
final String id; final String id;
final String? password; final String? password;
final bool? isSharedPassword; final bool? isSharedPassword;
final bool? forceRelay; final bool? forceRelay;
final DesktopTabController tabController; final DesktopTabController? tabController;
@override @override
State<StatefulWidget> createState() => _FileManagerPageState(); State<StatefulWidget> createState() => _FileManagerPageState();
@ -97,11 +99,14 @@ class _FileManagerPageState extends State<FileManagerPage>
if (!isLinux) { if (!isLinux) {
WakelockPlus.enable(); WakelockPlus.enable();
} }
if (isWeb) {
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
}
debugPrint("File manager page init success with id ${widget.id}"); debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState); _ffi.dialogManager.setOverlayState(_overlayKeyState);
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id); widget.tabController?.onSelected?.call(widget.id);
}); });
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
} }
@ -140,6 +145,7 @@ class _FileManagerPageState extends State<FileManagerPage>
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row( body: Row(
children: [ children: [
if (!isWeb)
Flexible( Flexible(
flex: 3, flex: 3,
child: dropArea(FileManagerView( child: dropArea(FileManagerView(
@ -192,7 +198,13 @@ class _FileManagerPageState extends State<FileManagerPage>
return Icon(Icons.delete_outline, color: color); return Icon(Icons.delete_outline, color: color);
default: default:
return Transform.rotate( 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), 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( Obx(() => ElevatedButton.icon(
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>( padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
@ -833,11 +889,14 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white, : Colors.white,
), ),
) )
: isWeb
? Offstage()
: RotatedBox( : RotatedBox(
quarterTurns: 2, quarterTurns: 2,
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/arrow.svg", "assets/arrow.svg",
colorFilter: svgColor(selectedItems.items.isEmpty colorFilter: svgColor(
selectedItems.items.isEmpty
? Theme.of(context).brightness == ? Theme.of(context).brightness ==
Brightness.light Brightness.light
? MyTheme.grayBg ? MyTheme.grayBg
@ -857,7 +916,7 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white), : Colors.white),
) )
: Text( : Text(
translate('Receive'), translate(isWeb ? 'Download' : 'Receive'),
style: TextStyle( style: TextStyle(
color: selectedItems.items.isEmpty color: selectedItems.items.isEmpty
? Theme.of(context).brightness == ? 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:flutter_hbb/utils/event_loop.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart' as path; 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 '../consts.dart';
import 'model.dart'; import 'model.dart';
@ -74,7 +76,7 @@ class FileModel {
Future<void> onReady() async { Future<void> onReady() async {
await evtLoop.onReady(); await evtLoop.onReady();
await localController.onReady(); if (!isWeb) await localController.onReady();
await remoteController.onReady(); await remoteController.onReady();
} }
@ -86,7 +88,7 @@ class FileModel {
} }
Future<void> refreshAll() async { Future<void> refreshAll() async {
await localController.refresh(); if (!isWeb) await localController.refresh();
await remoteController.refresh(); await remoteController.refresh();
} }
@ -228,6 +230,33 @@ class FileModel {
); );
}, useAnimation: false); }, 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 { class DirectoryData {
@ -462,7 +491,8 @@ class FileController {
to: PathUtil.join(toPath, from.name, isWindows), to: PathUtil.join(toPath, from.name, isWindows),
fileNum: 0, fileNum: 0,
includeHidden: showHidden, includeHidden: showHidden,
isRemote: isRemoteToLocal); isRemote: isRemoteToLocal,
isDir: from.isDirectory);
debugPrint( debugPrint(
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
} }
@ -489,7 +519,7 @@ class FileController {
} else if (item.isDirectory) { } else if (item.isDirectory) {
title = translate("Not an empty directory"); title = translate("Not an empty directory");
dialogManager?.showLoading(translate("Waiting")); dialogManager?.showLoading(translate("Waiting"));
final fd = await fileFetcher.fetchDirectoryRecursive( final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
jobID, item.path, items.isLocal, true); jobID, item.path, items.isLocal, true);
if (fd.path.isEmpty) { if (fd.path.isEmpty) {
fd.path = item.path; fd.path = item.path;
@ -809,7 +839,6 @@ class JobController {
job.speed = double.parse(evt['speed']); job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']); job.finishedSize = int.parse(evt['finished_size']);
job.recvJobRes = true; job.recvJobRes = true;
debugPrint("update job $id with $evt");
jobTable.refresh(); jobTable.refresh();
} }
} catch (e) { } catch (e) {
@ -1116,11 +1145,11 @@ class FileFetcher {
} }
} }
Future<FileDirectory> fetchDirectoryRecursive( Future<FileDirectory> fetchDirectoryRecursiveToRemove(
int actID, String path, bool isLocal, bool showHidden) async { int actID, String path, bool isLocal, bool showHidden) async {
// TODO test Recursive is show hidden default? // TODO test Recursive is show hidden default?
try { try {
await bind.sessionReadDirRecursive( await bind.sessionReadDirToRemoveRecursive(
sessionId: sessionId, sessionId: sessionId,
actId: actID, actId: actID,
path: path, path: path,

View File

@ -390,6 +390,10 @@ class FfiModel with ChangeNotifier {
handleFollowCurrentDisplay(evt, sessionId, peerId); handleFollowCurrentDisplay(evt, sessionId, peerId);
} else if (name == 'use_texture_render') { } else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId); _handleUseTextureRender(evt, sessionId, peerId);
} else if (name == "selected_files") {
if (isWeb) {
parent.target?.fileModel.onSelectedFiles(evt);
}
} else { } else {
debugPrint('Event is not handled in the fixed branch: $name'); 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, file_num: i32,
include_hidden: bool, include_hidden: bool,
is_remote: bool, is_remote: bool,
_is_dir: bool,
) { ) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) { 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); 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, session_id: SessionID,
act_id: i32, act_id: i32,
path: String, path: String,