diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 21dc427cc..3138dd14d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -112,18 +112,21 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { required this.border2, required this.highlight, required this.drag_indicator, + required this.shadow, }); final Color? border; final Color? border2; final Color? highlight; final Color? drag_indicator; + final Color? shadow; static final light = ColorThemeExtension( border: Color(0xFFCCCCCC), border2: Color(0xFFBBBBBB), highlight: Color(0xFFE5E5E5), drag_indicator: Colors.grey[800], + shadow: Colors.black, ); static final dark = ColorThemeExtension( @@ -131,19 +134,24 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { border2: Color(0xFFE5E5E5), highlight: Color(0xFF3F3F3F), drag_indicator: Colors.grey, + shadow: Colors.grey, ); @override - ThemeExtension<ColorThemeExtension> copyWith( - {Color? border, - Color? border2, - Color? highlight, - Color? drag_indicator}) { + ThemeExtension<ColorThemeExtension> copyWith({ + Color? border, + Color? border2, + Color? highlight, + Color? drag_indicator, + Color? shadow, + }) { return ColorThemeExtension( - border: border ?? this.border, - border2: border2 ?? this.border2, - highlight: highlight ?? this.highlight, - drag_indicator: drag_indicator ?? this.drag_indicator); + border: border ?? this.border, + border2: border2 ?? this.border2, + highlight: highlight ?? this.highlight, + drag_indicator: drag_indicator ?? this.drag_indicator, + shadow: shadow ?? this.shadow, + ); } @override @@ -157,6 +165,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { border2: Color.lerp(border2, other.border2, t), highlight: Color.lerp(highlight, other.highlight, t), drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t), + shadow: Color.lerp(shadow, other.shadow, t), ); } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6cfbf4675..8a1b2fbd6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -372,6 +372,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> { offstage: _dragging.isTrue, child: Material( elevation: _MenubarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, child: _DraggableShowHide( dragging: _dragging, fractionX: _fractionX, @@ -421,6 +422,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> { children: [ Material( elevation: _MenubarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, borderRadius: BorderRadius.all(Radius.circular(4.0)), color: Theme.of(context) .menuBarTheme diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4170a5461..ebb3339ef 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -45,6 +45,7 @@ class FileModel { late final GetSessionID getSessionID; String get sessionID => getSessionID(); + late final FileDialogEventLoop evtLoop; FileModel(this.parent) { getSessionID = () => parent.target?.id ?? ""; @@ -64,14 +65,17 @@ class FileModel { jobController: jobController, fileFetcher: fileFetcher, getOtherSideDirectoryData: () => localController.directoryData()); + evtLoop = FileDialogEventLoop(); } Future<void> onReady() async { + await evtLoop.onReady(); await localController.onReady(); await remoteController.onReady(); } Future<void> close() async { + await evtLoop.close(); parent.target?.dialogManager.dismissAll(); await localController.close(); await remoteController.close(); @@ -90,14 +94,26 @@ class FileModel { fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); } - void overrideFileConfirm(Map<String, dynamic> evt) async { - final resp = await showFileConfirmDialog( - translate("Overwrite"), "${evt['read_path']}", true); + Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async { + evtLoop.pushEvent( + _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); + } + + Future<void> overrideFileConfirm(Map<String, dynamic> evt, + {bool? overrideConfirm, bool skip = false}) async { + // If `skip == true`, it means to skip this file without showing dialog. + // Because `resp` may be null after the user operation or the last remembered operation, + // and we should distinguish them. + final resp = overrideConfirm ?? + (!skip + ? await showFileConfirmDialog(translate("Overwrite"), + "${evt['read_path']}", true, evt['is_identical'] == "true") + : null); final id = int.tryParse(evt['id']) ?? 0; if (false == resp) { final jobIndex = jobController.getJob(id); if (jobIndex != -1) { - jobController.cancelJob(id); + await jobController.cancelJob(id); final job = jobController.jobTable[jobIndex]; job.state = JobState.done; jobController.jobTable.refresh(); @@ -111,7 +127,11 @@ class FileModel { // overwrite need_override = true; } - bind.sessionSetConfirmOverrideFile( + // Update the loop config. + if (fileConfirmCheckboxRemember) { + evtLoop.setSkip(!need_override); + } + await bind.sessionSetConfirmOverrideFile( id: sessionID, actId: id, fileNum: int.parse(evt['file_num']), @@ -119,12 +139,16 @@ class FileModel { remember: fileConfirmCheckboxRemember, isUpload: evt['is_upload'] == "true"); } + // Update the loop config. + if (fileConfirmCheckboxRemember) { + evtLoop.setOverrideConfirm(resp); + } } bool fileConfirmCheckboxRemember = false; Future<bool?> showFileConfirmDialog( - String title, String content, bool showCheckbox) async { + String title, String content, bool showCheckbox, bool isIdentical) async { fileConfirmCheckboxRemember = false; return await parent.target?.dialogManager.show<bool?>( (setState, Function(bool? v) close) { @@ -149,6 +173,17 @@ class FileModel { style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 5), Text(content), + Offstage( + offstage: !isIdentical, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + Text(translate("identical_file_tip"), + style: const TextStyle(fontWeight: FontWeight.w500)) + ], + ), + ), showCheckbox ? CheckboxListTile( contentPadding: const EdgeInsets.all(0), @@ -377,12 +412,12 @@ class FileController { } job.totalSize = totalSize; job.fileCount = fileCount; - debugPrint("update receive details:${fd.path}"); + debugPrint("update receive details: ${fd.path}"); jobController.jobTable.refresh(); } } else if (options.value.home.isEmpty) { options.value.home = fd.path; - debugPrint("init remote home:${fd.path}"); + debugPrint("init remote home: ${fd.path}"); directory.value = fd; } } catch (e) { @@ -414,7 +449,7 @@ class FileController { includeHidden: showHidden, isRemote: isRemoteToLocal); 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)}"); } } @@ -639,7 +674,7 @@ class JobController { jobTable.refresh(); } } catch (e) { - debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}"); + debugPrint("Failed to tryUpdateJobProgress, evt: ${evt.toString()}"); } } @@ -677,8 +712,8 @@ class JobController { debugPrint("jobError $evt"); } - void cancelJob(int id) async { - bind.sessionCancelJob(id: sessionID, actId: id); + Future<void> cancelJob(int id) async { + await bind.sessionCancelJob(id: sessionID, actId: id); } void loadLastJob(Map<String, dynamic> evt) { @@ -806,7 +841,7 @@ class FileFetcher { Timer(Duration(seconds: 2), () { tasks.remove(path); if (c.isCompleted) return; - c.completeError("Failed to read dir,timeout"); + c.completeError("Failed to read dir, timeout"); }); return c.future; } @@ -822,7 +857,7 @@ class FileFetcher { Timer(Duration(seconds: 2), () { tasks.remove(actID); if (c.isCompleted) return; - c.completeError("Failed to read dir,timeout"); + c.completeError("Failed to read dir, timeout"); }); return c.future; } @@ -846,7 +881,7 @@ class FileFetcher { completer?.complete(fd); } } catch (e) { - debugPrint("tryCompleteJob err :$e"); + debugPrint("tryCompleteJob err: $e"); } } @@ -1167,3 +1202,74 @@ List<Entry> _sortList(List<Entry> list, SortBy sortType, bool ascending) { } return []; } + +/// Define a general queue which can accepts different dialog type. +/// +/// [Visibility] +/// The `_FileDialogType` and `_DialogEvent` are invisible for other models. +enum FileDialogType { overwrite, unknown } + +class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> { + WeakReference<FileModel> fileModel; + bool? _overrideConfirm; + bool _skip = false; + + _FileDialogEvent(this.fileModel, super.type, super.data); + + void setOverrideConfirm(bool? confirm) { + _overrideConfirm = confirm; + } + + void setSkip(bool skip) { + _skip = skip; + } + + @override + EventCallback<Map<String, dynamic>>? findCallback(FileDialogType type) { + final model = fileModel.target; + if (model == null) { + return null; + } + switch (type) { + case FileDialogType.overwrite: + return (data) async { + return await model.overrideFileConfirm(data, + overrideConfirm: _overrideConfirm, skip: _skip); + }; + default: + debugPrint("Unknown event type: $type with $data"); + return null; + } + } +} + +class FileDialogEventLoop + extends BaseEventLoop<FileDialogType, Map<String, dynamic>> { + bool? _overrideConfirm; + bool _skip = false; + + @override + Future<void> onPreConsume( + BaseEvent<FileDialogType, Map<String, dynamic>> evt) async { + var event = evt as _FileDialogEvent; + event.setOverrideConfirm(_overrideConfirm); + event.setSkip(_skip); + debugPrint( + "FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>"); + } + + @override + Future<void> onEventsClear() { + _overrideConfirm = null; + _skip = false; + return super.onEventsClear(); + } + + void setOverrideConfirm(bool? confirm) { + _overrideConfirm = confirm; + } + + void setSkip(bool skip) { + _skip = skip; + } +} diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 94e28ea21..fd88a5332 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -173,7 +173,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'job_error') { parent.target?.fileModel.jobController.jobError(evt); } else if (name == 'override_file_confirm') { - parent.target?.fileModel.overrideFileConfirm(evt); + parent.target?.fileModel.postOverrideFileConfirm(evt); } else if (name == 'load_last_job') { parent.target?.fileModel.jobController.loadLastJob(evt); } else if (name == 'update_folder_files') { diff --git a/flutter/lib/utils/event_loop.dart b/flutter/lib/utils/event_loop.dart new file mode 100644 index 000000000..a982cf9e2 --- /dev/null +++ b/flutter/lib/utils/event_loop.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +typedef EventCallback<Data> = Future<dynamic> Function(Data data); + +abstract class BaseEvent<EventType, Data> { + EventType type; + Data data; + + /// Constructor. + BaseEvent(this.type, this.data); + + /// Consume this event. + @visibleForTesting + Future<dynamic> consume() async { + final cb = findCallback(type); + if (cb == null) { + return null; + } else { + return cb(data); + } + } + + EventCallback<Data>? findCallback(EventType type); +} + +abstract class BaseEventLoop<EventType, Data> { + final List<BaseEvent<EventType, Data>> _evts = []; + Timer? _timer; + + List<BaseEvent<EventType, Data>> get evts => _evts; + + Future<void> onReady() async { + // Poll every 100ms. + _timer = Timer.periodic(Duration(milliseconds: 100), _handleTimer); + } + + /// An Event is about to be consumed. + Future<void> onPreConsume(BaseEvent<EventType, Data> evt) async {} + /// An Event was consumed. + Future<void> onPostConsume(BaseEvent<EventType, Data> evt) async {} + /// Events are all handled and cleared. + Future<void> onEventsClear() async {} + /// Events start to consume. + Future<void> onEventsStartConsuming() async {} + + Future<void> _handleTimer(Timer timer) async { + if (_evts.isEmpty) { + return; + } + timer.cancel(); + _timer = null; + // Handle the logic. + await onEventsStartConsuming(); + while (_evts.isNotEmpty) { + final evt = _evts.first; + _evts.remove(evt); + await onPreConsume(evt); + await evt.consume(); + await onPostConsume(evt); + } + await onEventsClear(); + // Now events are all processed. + _timer = Timer.periodic(Duration(milliseconds: 100), _handleTimer); + } + + Future<void> close() async { + _timer?.cancel(); + } + + void pushEvent(BaseEvent<EventType, Data> evt) { + _evts.add(evt); + } + + void clear() { + _evts.clear(); + } +} diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index be3a1e51e..0c29a5f19 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -301,6 +301,7 @@ message FileTransferDigest { uint64 last_modified = 3; uint64 file_size = 4; bool is_upload = 5; + bool is_identical = 6; } message FileTransferBlock { diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto index 1ac60f3f3..d86226290 100644 --- a/libs/hbb_common/protos/rendezvous.proto +++ b/libs/hbb_common/protos/rendezvous.proto @@ -27,6 +27,7 @@ message PunchHole { bytes socket_addr = 1; string relay_server = 2; NatType nat_type = 3; + string request_region = 4; } message TestNatRequest { @@ -51,6 +52,7 @@ message PunchHoleSent { string relay_server = 3; NatType nat_type = 4; string version = 5; + string request_region = 6; } message RegisterPk { @@ -105,6 +107,7 @@ message RequestRelay { string licence_key = 6; ConnType conn_type = 7; string token = 8; + string request_region = 9; } message RelayResponse { @@ -117,6 +120,7 @@ message RelayResponse { } string refuse_reason = 6; string version = 7; + string request_region = 8; } message SoftwareUpdate { string url = 1; } @@ -128,6 +132,7 @@ message SoftwareUpdate { string url = 1; } message FetchLocalAddr { bytes socket_addr = 1; string relay_server = 2; + string request_region = 3; } message LocalAddr { @@ -136,6 +141,7 @@ message LocalAddr { string relay_server = 3; string id = 4; string version = 5; + string request_region = 6; } message PeerDiscovery { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 4ce2cf07e..4c4f065ab 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -43,6 +43,7 @@ lazy_static::lazy_static! { static ref CONFIG: Arc<RwLock<Config>> = Arc::new(RwLock::new(Config::load())); static ref CONFIG2: Arc<RwLock<Config2>> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc<RwLock<LocalConfig>> = Arc::new(RwLock::new(LocalConfig::load())); + pub static ref CONFIG_OIDC: Arc<RwLock<ConfigOidc>> = Arc::new(RwLock::new(ConfigOidc::load())); pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = Default::default(); pub static ref PROD_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { Some(key) if !key.is_empty() => key, @@ -257,6 +258,35 @@ pub struct PeerInfoSerde { pub platform: String, } +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct ConfigOidc { + #[serde(default)] + pub max_auth_count: usize, + #[serde(default)] + pub callback_url: String, + #[serde(default)] + pub providers: HashMap<String, ConfigOidcProvider>, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct ConfigOidcProvider { + // seconds. 0 means never expires + #[serde(default)] + pub refresh_token_expires_in: u32, + #[serde(default)] + pub client_id: String, + #[serde(default)] + pub client_secret: String, + #[serde(default)] + pub issuer: Option<String>, + #[serde(default)] + pub authorization_endpoint: Option<String>, + #[serde(default)] + pub token_endpoint: Option<String>, + #[serde(default)] + pub userinfo_endpoint: Option<String>, +} + #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct TransferSerde { #[serde(default)] @@ -1366,6 +1396,29 @@ impl UserDefaultConfig { } } +impl ConfigOidc { + fn suffix() -> &'static str { + "_oidc" + } + + fn load() -> Self { + Config::load_::<Self>(Self::suffix())._load_env() + } + + fn _load_env(mut self) -> Self { + use std::env; + for (k, mut v) in &mut self.providers { + if let Ok(client_id) = env::var(format!("OIDC-{}-CLIENT-ID", k.to_uppercase())) { + v.client_id = client_id; + } + if let Ok(client_secret) = env::var(format!("OIDC-{}-CLIENT-SECRET", k.to_uppercase())) { + v.client_secret = client_secret; + } + } + self + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index ea54e113a..41160f49d 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -823,14 +823,19 @@ pub fn is_write_need_confirmation( let modified_time = metadata.modified()?; let remote_mt = Duration::from_secs(digest.last_modified); let local_mt = modified_time.duration_since(UNIX_EPOCH)?; + // [Note] + // We decide to give the decision whether to override the existing file to users, + // which obey the behavior of the file manager in our system. + let mut is_identical = false; if remote_mt == local_mt && digest.file_size == metadata.len() { - return Ok(DigestCheckResult::IsSame); + is_identical = true; } Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { id: digest.id, file_num: digest.file_num, last_modified: local_mt.as_secs(), file_size: metadata.len(), + is_identical, ..Default::default() })) } else { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1c7788193..c37f235c0 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -954,6 +954,7 @@ impl<T: InvokeUiSession> Remote<T> { digest.file_num, read_path, true, + digest.is_identical ); } } @@ -997,6 +998,7 @@ impl<T: InvokeUiSession> Remote<T> { digest.file_num, write_path, false, + digest.is_identical ); } } diff --git a/src/flutter.rs b/src/flutter.rs index 354e418eb..2262df660 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -420,7 +420,7 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter // TEST flutter fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {} - fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool, is_identical: bool) { self.push_event( "override_file_confirm", vec![ @@ -428,6 +428,7 @@ impl InvokeUiSession for FlutterHandler { ("file_num", &file_num.to_string()), ("read_path", &to), ("is_upload", &is_upload.to_string()), + ("is_identical", &is_identical.to_string()) ], ); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index b086cbb01..585b784a6 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index aee739f84..ef2d6f50b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", "此文件与对方的一致"), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 9ebe4cff9..ac0e9e6bb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 478d7b925..14bcc5158 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Tom brugernavn"), ("Empty Password", "Tom adgangskode"), ("Me", "Mig"), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 32f720f4b..94e9832f1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Leerer Benutzername"), ("Empty Password", "Leeres Passwort"), ("Me", "Ich"), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index f62912cd4..6ab774b3c 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Κενό όνομα χρήστη"), ("Empty Password", "Κενός κωδικός πρόσβασης"), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index b44ff2e40..389909245 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -52,5 +52,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "No favorite peers yet?\nLet's find someone to connect with and add it to your favorites!"), ("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."), ("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."), + ("identical_file_tip", "This file is identical with the peer's one."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 5403c8883..0a5930412 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d7c753096..b144d6a8c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Nombre de usuario vacío"), ("Empty Password", "Contraseña vacía"), ("Me", "Yo"), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index f3da03d21..3a9f58749 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 06bc82bfe..3a28bbb9d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 73e3c41a7..2aa1558e5 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 4c0fefd82..1927d582c 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b0728ddfb..4ca577823 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Nome Utente Vuoto"), ("Empty Password", "Password Vuota"), ("Me", "Io"), + ("identical_file_tip", "Questo file è identico a quello del peer."), ("Show monitors in menu bar", "Mostra schermi nella barra di menù"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index be6f337b0..31fa936ce 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c0e729ba0..a857dd56f 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 18fee1e00..4bcd4949d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 033e7b8d4..0dc7e6bb0 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Gebruikersnaam Leeg"), ("Empty Password", "Wachtwoord Leeg"), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 8b256a4c2..8a8f51abf 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d583daa9e..603dd0d59 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 43ceb9a25..7eaaabd70 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index a9b82e788..0b26f382d 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1f5328638..7c17c2b46 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c1ea44554..16c70f7c9 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index dd6050e57..90a6ee459 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1eedfdfad..25dd55f8d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d7c932cf0..b07cca46b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 49be9ef16..abc344820 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 771ca4fcb..c0adc7a2a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c1c55927a..63456778b 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index c31789ee5..b68e2f3e8 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b123c73bc..d314f5f6f 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 5779617ba..62a7382ed 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 55e294831..4f94c5a38 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -477,6 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), + ("identical_file_tip", ""), ("Show monitors in menu bar", ""), ].iter().cloned().collect(); } diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index f69f6d323..6c741b31f 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -778,12 +778,14 @@ handler.confirmDeleteFiles = function(id, i, name) { }); } -handler.overrideFileConfirm = function(id, file_num, to, is_upload) { +handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) { var jt = file_transfer.job_table; + var identical_msg = is_identical ? translate("identical_file_tip"): ""; msgbox("custom-skip", "Confirm Write Strategy", "<div .form> \ - <div>" + translate('Overwrite') + translate('files') + ".</div> \ + <div>" + translate('Overwrite') + " " + translate('files') + ".</div> \ <div>" + translate('This file exists, skip or overwrite this file?') + "</div> \ <div.ellipsis style=\"font-weight: bold;\" .text>" + to + "</div> \ + <div>" + identical_msg + "</div> \ <div><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \ </div>", "", function(res=null) { if (!res) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ed16f1e0e..68decf955 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -197,10 +197,10 @@ impl InvokeUiSession for SciterHandler { self.call("confirmDeleteFiles", &make_args!(id, i, name)); } - fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool, is_identical: bool) { self.call( "overrideFileConfirm", - &make_args!(id, file_num, to, is_upload), + &make_args!(id, file_num, to, is_upload, is_identical), ); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 11bcff925..b90f5fbea 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -872,7 +872,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { only_count: bool, ); fn confirm_delete_files(&self, id: i32, i: i32, name: String); - fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool, is_identical: bool); fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self);