From 58619a7ffcd3f75d60bbf8fbf85e2d165fa52115 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:07:22 +0000 Subject: [PATCH 1/4] Initial plan From 39d5b5c686803548759141a0dfdb89b37cfb6094 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:16:14 +0000 Subject: [PATCH 2/4] Add sync summary - translation files already synced to WpfUi Co-authored-by: Alex4SSB <68120101+Alex4SSB@users.noreply.github.com> --- ADB Explorer _WpfUi/ADB Explorer.csproj | 130 + ADB Explorer _WpfUi/App.config | 21 + ADB Explorer _WpfUi/App.xaml | 27 + ADB Explorer _WpfUi/App.xaml.cs | 241 + ADB Explorer _WpfUi/AssemblyInfo.cs | 10 + .../Assets/APK_format_icon_2014-2019_.ico | Bin 0 -> 175896 bytes .../Assets/app_icon_2023_combined.ico | Bin 0 -> 327038 bytes .../Assets/app_icon_black_2023_256px.png | Bin 0 -> 5719 bytes .../Controls/AdbContentDialog.xaml | 49 + .../Controls/AdbContentDialog.xaml.cs | 67 + .../Controls/AdbContextMenu.cs | 10 + ADB Explorer _WpfUi/Controls/AdbMenu.cs | 10 + .../Controls/DevicesPageHeader.xaml | 136 + .../Controls/DevicesPageHeader.xaml.cs | 64 + .../Controls/ExplorerPageHeader.xaml | 268 ++ .../Controls/ExplorerPageHeader.xaml.cs | 398 ++ .../Controls/Icons/InstallIcon.xaml | 19 + .../Controls/Icons/InstallIcon.xaml.cs | 12 + .../Controls/Icons/PullIcon.xaml | 23 + .../Controls/Icons/PullIcon.xaml.cs | 24 + .../Controls/Icons/PushIcon.xaml | 23 + .../Controls/Icons/PushIcon.xaml.cs | 24 + .../Controls/Icons/RecycleIcon.xaml | 19 + .../Controls/Icons/RecycleIcon.xaml.cs | 12 + .../Controls/Icons/ResetSettingsIcon.xaml | 18 + .../Controls/Icons/ResetSettingsIcon.xaml.cs | 12 + .../Controls/Icons/SettingsIcon.xaml | 38 + .../Controls/Icons/SettingsIcon.xaml.cs | 12 + .../Controls/Icons/UninstallIcon.xaml | 19 + .../Controls/Icons/UninstallIcon.xaml.cs | 12 + .../Controls/MaskedTextBox.xaml | 39 + .../Controls/MaskedTextBox.xaml.cs | 151 + ADB Explorer _WpfUi/Controls/MasonryPanel.cs | 86 + .../Controls/SettingsPageHeader.xaml | 620 +++ .../Controls/SettingsPageHeader.xaml.cs | 41 + .../Controls/SimpleStackPanel.cs | 188 + .../Converters/CellConverter.cs | 13 + .../Converters/CompletedStatsConverter.cs | 27 + ADB Explorer _WpfUi/Converters/ControlSize.cs | 22 + .../Converters/DoubleEquals.cs | 24 + .../Converters/EnumToBooleanConverter.cs | 31 + .../Converters/FileOpProgressConverter.cs | 22 + .../Converters/FileOpTreeStatusConverter.cs | 90 + .../Converters/MarginConverter.cs | 45 + .../Converters/MenuItemConverter.cs | 27 + .../Converters/SizeConverter.cs | 108 + .../Converters/StringFormatConverter.cs | 24 + .../Converters/TreeViewIndentConverter.cs | 17 + .../Converters/TrimmedTooltipConverter.cs | 37 + .../Converters/UnixTimeConverter.cs | 28 + ADB Explorer _WpfUi/Helpers/AdbHelper.cs | 93 + .../Helpers/AppInfra/AsyncHelper.cs | 14 + .../Helpers/AppInfra/ByteHelper.cs | 47 + .../Helpers/AppInfra/CommandHandler.cs | 64 + .../Helpers/AppInfra/DictionaryHelper.cs | 44 + .../Helpers/AppInfra/ListHelper.cs | 46 + .../Helpers/AppInfra/ObservableList.cs | 144 + .../Helpers/AppInfra/ObservableProperty.cs | 34 + .../Helpers/AppInfra/RandomString.cs | 28 + .../Helpers/AppInfra/SettingsHelper.cs | 203 + .../Helpers/Attachable/ContextMenuHelper.cs | 77 + .../Helpers/Attachable/ExpanderHelper.cs | 167 + .../Helpers/Attachable/MenuHelper.cs | 84 + .../Helpers/Attachable/SelectionHelper.cs | 181 + .../Helpers/Attachable/StyleHelper.cs | 221 + .../Helpers/Attachable/TextHelper.cs | 286 ++ .../Helpers/Attachable/VisibilityHelper.cs | 12 + ADB Explorer _WpfUi/Helpers/DeviceHelper.cs | 810 ++++ ADB Explorer _WpfUi/Helpers/DriveHelper.cs | 31 + .../Helpers/EnumToBooleanConverter.cs | 37 + ADB Explorer _WpfUi/Helpers/ExplorerHelper.cs | 26 + .../Helpers/File/FileHelper.cs | 384 ++ .../Helpers/File/FileToIcon.cs | 333 ++ .../Helpers/File/FolderHelper.cs | 128 + .../Helpers/File/TrashHelper.cs | 62 + ADB Explorer _WpfUi/Helpers/QrGenerator.cs | 17 + .../Helpers/TabularDateFormatter.cs | 95 + .../DeviceTemplateSelector.cs | 24 + .../DriveTemplateSelector.cs | 16 + .../FileOpFileNameTemplateSelector.cs | 25 + .../FileOpProgressTemplateSelector.cs | 32 + .../FileOpTreeTemplateSelector.cs | 20 + .../FileOperationTemplateSelector.cs | 21 + .../TemplateSelectors/MenuTemplateSelector.cs | 74 + .../SettingsTemplateSelector.cs | 31 + ADB Explorer _WpfUi/Models/Battery.cs | 348 ++ ADB Explorer _WpfUi/Models/Device/Device.cs | 93 + .../Models/Device/HistoryDevice.cs | 31 + .../Models/Device/LogicalDevice.cs | 249 ++ .../Models/Device/NewDevice.cs | 24 + .../Models/Device/ServiceDevice.cs | 63 + .../Models/Device/WsaPkgDevice.cs | 17 + ADB Explorer _WpfUi/Models/DirectoryLister.cs | 223 + ADB Explorer _WpfUi/Models/Drive/Drive.cs | 79 + .../Models/Drive/LogicalDrive.cs | 100 + .../Models/Drive/VirtualDrive.cs | 25 + ADB Explorer _WpfUi/Models/File/FileClass.cs | 498 +++ ADB Explorer _WpfUi/Models/File/FilePath.cs | 182 + ADB Explorer _WpfUi/Models/File/FileStat.cs | 33 + ADB Explorer _WpfUi/Models/File/IBaseFile.cs | 35 + ADB Explorer _WpfUi/Models/File/Package.cs | 76 + ADB Explorer _WpfUi/Models/File/SyncFile.cs | 209 + .../Models/File/TrashIndexer.cs | 83 + .../Models/FileOpColumnConfig.cs | 265 ++ ADB Explorer _WpfUi/Models/FileOpFilter.cs | 121 + ADB Explorer _WpfUi/Models/Log.cs | 19 + ADB Explorer _WpfUi/Models/Static/AdbRegEx.cs | 71 + ADB Explorer _WpfUi/Models/Static/Const.cs | 123 + ADB Explorer _WpfUi/Models/Static/Data.cs | 48 + .../Models/Static/NavHistory.cs | 275 ++ .../Properties/AppGlobal.Designer.cs | 100 + ADB Explorer _WpfUi/Properties/AppGlobal.resx | 133 + .../Fonts/Nunito-VariableFont_wght.ttf | Bin 0 -> 277844 bytes ADB Explorer _WpfUi/Resources/Links.cs | 27 + ADB Explorer _WpfUi/Services/ADB/ADBDevice.cs | 349 ++ .../Services/ADB/ADBService.cs | 463 ++ .../Services/ADB/FileOpProgressInfo.cs | 128 + ADB Explorer _WpfUi/Services/ADB/MDNS.cs | 85 + .../Services/ADB/ShellCommands.cs | 171 + .../Services/ADB/WiFiPairingService.cs | 45 + .../Services/AppInfra/AppRuntimeSettings.cs | 397 ++ .../Services/AppInfra/AppSettings.cs | 291 ++ .../Services/AppInfra/CopyPasteService.cs | 980 ++++ .../Services/AppInfra/DebugLog.cs | 16 + .../Services/AppInfra/DevicePollingService.cs | 95 + .../Services/AppInfra/DialogService.cs | 81 + .../AppInfra/FileAction/ActionMenu.cs | 470 ++ .../AppInfra/FileAction/FileAction.cs | 613 +++ .../AppInfra/FileAction/FileActionLogic.cs | 1252 ++++++ .../AppInfra/FileAction/FileActionsEnable.cs | 550 +++ .../AppInfra/FileAction/ToggleMenu.cs | 57 + .../Services/AppInfra/FileAction/ToolBar.cs | 345 ++ .../Services/AppInfra/IpcService.cs | 66 + .../Services/AppInfra/LowLevel/DataFormats.cs | 92 + .../Services/AppInfra/LowLevel/DiskUsage.cs | 186 + .../AppInfra/LowLevel/FileDescriptor.cs | 235 + .../Services/AppInfra/LowLevel/MonitorInfo.cs | 65 + .../AppInfra/LowLevel/ProcessHandling.cs | 62 + .../AppInfra/LowLevel/ThemeService.cs | 49 + .../LowLevel/VirtualFileDataObject.cs | 763 ++++ .../Services/AppInfra/LowLevel/WindowStyle.cs | 20 + .../AppInfra/NativeMethods/DragDropNative.cs | 216 + .../NativeMethods/InterceptClipboard.cs | 81 + .../AppInfra/NativeMethods/InterceptMouse.cs | 104 + .../AppInfra/NativeMethods/NativeMethods.cs | 757 ++++ .../AppInfra/NativeMethods/SysImageList.cs | 797 ++++ .../Services/AppInfra/Network.cs | 86 + .../Services/AppInfra/Security.cs | 159 + .../Services/AppInfra/SettingsModel.cs | 615 +++ .../Services/AppInfra/Storage.cs | 50 + .../Services/ApplicationHostService.cs | 55 + .../FileOperation/FileArchiveOperation.cs | 6 + .../FileChangeModifiedOperation.cs | 62 + .../FileOperation/FileDeleteOperation.cs | 68 + .../FileOperation/FileMoveOperation.cs | 128 + .../Services/FileOperation/FileOperation.cs | 355 ++ .../FileOperation/FileOperationQueue.cs | 345 ++ .../FileOperation/FileRenameOperation.cs | 60 + .../FileOperation/FileSyncOperation.cs | 266 ++ .../FileOperation/PackageInstallOperation.cs | 108 + .../FileOperation/ShellFileOperation.cs | 545 +++ .../Services/SettingsService.cs | 31 + .../Strings/Resources.Designer.cs | 3955 +++++++++++++++++ ADB Explorer _WpfUi/Strings/Resources.ar.resx | 280 ++ ADB Explorer _WpfUi/Strings/Resources.bn.resx | 126 + ADB Explorer _WpfUi/Strings/Resources.cs.resx | 123 + ADB Explorer _WpfUi/Strings/Resources.de.resx | 1429 ++++++ ADB Explorer _WpfUi/Strings/Resources.es.resx | 297 ++ ADB Explorer _WpfUi/Strings/Resources.fa.resx | 141 + ADB Explorer _WpfUi/Strings/Resources.fr.resx | 1435 ++++++ .../Strings/Resources.he-IL.resx | 1438 ++++++ ADB Explorer _WpfUi/Strings/Resources.id.resx | 186 + ADB Explorer _WpfUi/Strings/Resources.it.resx | 1429 ++++++ ADB Explorer _WpfUi/Strings/Resources.ja.resx | 1429 ++++++ ADB Explorer _WpfUi/Strings/Resources.ko.resx | 424 ++ ADB Explorer _WpfUi/Strings/Resources.nl.resx | 120 + ADB Explorer _WpfUi/Strings/Resources.pl.resx | 1429 ++++++ .../Strings/Resources.pt-BR.resx | 285 ++ ADB Explorer _WpfUi/Strings/Resources.resx | 1765 ++++++++ ADB Explorer _WpfUi/Strings/Resources.ru.resx | 1438 ++++++ ADB Explorer _WpfUi/Strings/Resources.ta.resx | 120 + ADB Explorer _WpfUi/Strings/Resources.tr.resx | 490 ++ .../Strings/Resources.zh-CN.resx | 1429 ++++++ .../Strings/Resources.zh-TW.resx | 1429 ++++++ ADB Explorer _WpfUi/Styles/BasicStyles.xaml | 485 ++ ADB Explorer _WpfUi/Styles/Colors.xaml | 156 + ADB Explorer _WpfUi/Styles/CustomStyles.xaml | 947 ++++ ADB Explorer _WpfUi/Styles/Device.xaml | 361 ++ ADB Explorer _WpfUi/Styles/FileAction.xaml | 414 ++ ADB Explorer _WpfUi/Themes/Dark.xaml | 12 + ADB Explorer _WpfUi/Themes/Light.xaml | 12 + ADB Explorer _WpfUi/Usings.cs | 38 + .../ViewModels/Device/DeviceAction.cs | 60 + .../ViewModels/Device/DeviceViewModel.cs | 227 + .../ViewModels/Device/Devices.cs | 319 ++ .../Device/HistoryDeviceViewModel.cs | 98 + .../Device/LogicalDeviceViewModel.cs | 241 + .../ViewModels/Device/MdnsDeviceViewModel.cs | 18 + .../ViewModels/Device/NewDeviceViewModel.cs | 139 + .../Device/ServiceDeviceViewModel.cs | 103 + .../Device/WsaPkgDeviceViewModel.cs | 36 + .../ViewModels/Drive/DriveViewModel.cs | 93 + .../ViewModels/Drive/LogicalDriveViewModel.cs | 84 + .../ViewModels/Drive/VirtualDriveViewModel.cs | 36 + .../FileOp/CanceledOpProgressViewModel.cs | 9 + .../FileOp/CompletedShellProgressViewModel.cs | 16 + .../FileOp/CompletedSyncProgressViewModel.cs | 52 + .../FileOp/FailedOpProgressViewModel.cs | 18 + .../FileOp/FileOpProgressViewModel.cs | 32 + .../FileOp/InProgShellProgressViewModel.cs | 13 + .../FileOp/InProgSyncProgressViewModel.cs | 35 + .../FileOp/WaitingOpProgressViewModel.cs | 9 + .../ViewModels/Pages/DevicesViewModel.cs | 39 + .../ViewModels/Pages/ExplorerViewModel.cs | 6 + .../ViewModels/Pages/SettingsViewModel.cs | 109 + .../ViewModels/SavedLocation.cs | 50 + .../ViewModels/ViewModelBase.cs | 30 + .../ViewModels/Windows/MainWindowViewModel.cs | 46 + .../Views/Battery/CompactBatteryControl.xaml | 47 + .../Battery/CompactBatteryControl.xaml.cs | 12 + .../Views/Battery/DetailedBatteryControl.xaml | 92 + .../Battery/DetailedBatteryControl.xaml.cs | 12 + .../Views/Device/HistoryDeviceControl.xaml | 164 + .../Views/Device/HistoryDeviceControl.xaml.cs | 12 + .../Views/Device/LogicalDeviceControl.xaml | 274 ++ .../Views/Device/LogicalDeviceControl.xaml.cs | 12 + .../Views/Device/MdnsDeviceControl.xaml | 181 + .../Views/Device/MdnsDeviceControl.xaml.cs | 40 + .../Views/Device/NewDeviceControl.xaml | 128 + .../Views/Device/NewDeviceControl.xaml.cs | 12 + .../Views/Device/ServiceDeviceControl.xaml | 65 + .../Views/Device/ServiceDeviceControl.xaml.cs | 12 + .../Views/Device/WsaPkgDeviceControl.xaml | 58 + .../Views/Device/WsaPkgDeviceControl.xaml.cs | 13 + .../Views/Drive/LogicalDriveControl.xaml | 108 + .../Views/Drive/LogicalDriveControl.xaml.cs | 26 + .../Views/Drive/VirtualDriveControl.xaml | 181 + .../Views/Drive/VirtualDriveControl.xaml.cs | 33 + .../Views/Pages/DevicesPage.xaml | 16 + .../Views/Pages/DevicesPage.xaml.cs | 21 + .../Views/Pages/ExplorerPage.xaml | 27 + .../Views/Pages/ExplorerPage.xaml.cs | 18 + .../Views/Pages/SettingsPage.xaml | 19 + .../Views/Pages/SettingsPage.xaml.cs | 21 + .../Views/Windows/MainWindow.xaml | 65 + .../Views/Windows/MainWindow.xaml.cs | 127 + ADB Explorer _WpfUi/app.manifest | 75 + ADB Explorer/ADB Explorer _ModernWPF.csproj | 96 + SYNC_SUMMARY.md | 78 + 249 files changed, 51204 insertions(+) create mode 100644 ADB Explorer _WpfUi/ADB Explorer.csproj create mode 100644 ADB Explorer _WpfUi/App.config create mode 100644 ADB Explorer _WpfUi/App.xaml create mode 100644 ADB Explorer _WpfUi/App.xaml.cs create mode 100644 ADB Explorer _WpfUi/AssemblyInfo.cs create mode 100644 ADB Explorer _WpfUi/Assets/APK_format_icon_2014-2019_.ico create mode 100644 ADB Explorer _WpfUi/Assets/app_icon_2023_combined.ico create mode 100644 ADB Explorer _WpfUi/Assets/app_icon_black_2023_256px.png create mode 100644 ADB Explorer _WpfUi/Controls/AdbContentDialog.xaml create mode 100644 ADB Explorer _WpfUi/Controls/AdbContentDialog.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/AdbContextMenu.cs create mode 100644 ADB Explorer _WpfUi/Controls/AdbMenu.cs create mode 100644 ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml create mode 100644 ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml create mode 100644 ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/InstallIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/InstallIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/PullIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/PullIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/PushIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/PushIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/RecycleIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/RecycleIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/ResetSettingsIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/ResetSettingsIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/Icons/UninstallIcon.xaml create mode 100644 ADB Explorer _WpfUi/Controls/Icons/UninstallIcon.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/MaskedTextBox.xaml create mode 100644 ADB Explorer _WpfUi/Controls/MaskedTextBox.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/MasonryPanel.cs create mode 100644 ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml create mode 100644 ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml.cs create mode 100644 ADB Explorer _WpfUi/Controls/SimpleStackPanel.cs create mode 100644 ADB Explorer _WpfUi/Converters/CellConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/CompletedStatsConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/ControlSize.cs create mode 100644 ADB Explorer _WpfUi/Converters/DoubleEquals.cs create mode 100644 ADB Explorer _WpfUi/Converters/EnumToBooleanConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/FileOpProgressConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/FileOpTreeStatusConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/MarginConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/MenuItemConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/SizeConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/StringFormatConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/TreeViewIndentConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/TrimmedTooltipConverter.cs create mode 100644 ADB Explorer _WpfUi/Converters/UnixTimeConverter.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AdbHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/AsyncHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/ByteHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/CommandHandler.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/DictionaryHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/ListHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/ObservableList.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/ObservableProperty.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/RandomString.cs create mode 100644 ADB Explorer _WpfUi/Helpers/AppInfra/SettingsHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/ContextMenuHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/ExpanderHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/MenuHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/SelectionHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/StyleHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/TextHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/Attachable/VisibilityHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/DeviceHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/DriveHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/EnumToBooleanConverter.cs create mode 100644 ADB Explorer _WpfUi/Helpers/ExplorerHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/File/FileHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/File/FileToIcon.cs create mode 100644 ADB Explorer _WpfUi/Helpers/File/FolderHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/File/TrashHelper.cs create mode 100644 ADB Explorer _WpfUi/Helpers/QrGenerator.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TabularDateFormatter.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/DeviceTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/DriveTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpFileNameTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpProgressTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpTreeTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOperationTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/MenuTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Helpers/TemplateSelectors/SettingsTemplateSelector.cs create mode 100644 ADB Explorer _WpfUi/Models/Battery.cs create mode 100644 ADB Explorer _WpfUi/Models/Device/Device.cs create mode 100644 ADB Explorer _WpfUi/Models/Device/HistoryDevice.cs create mode 100644 ADB Explorer _WpfUi/Models/Device/LogicalDevice.cs create mode 100644 ADB Explorer _WpfUi/Models/Device/NewDevice.cs create mode 100644 ADB Explorer _WpfUi/Models/Device/ServiceDevice.cs create mode 100644 ADB Explorer _WpfUi/Models/Device/WsaPkgDevice.cs create mode 100644 ADB Explorer _WpfUi/Models/DirectoryLister.cs create mode 100644 ADB Explorer _WpfUi/Models/Drive/Drive.cs create mode 100644 ADB Explorer _WpfUi/Models/Drive/LogicalDrive.cs create mode 100644 ADB Explorer _WpfUi/Models/Drive/VirtualDrive.cs create mode 100644 ADB Explorer _WpfUi/Models/File/FileClass.cs create mode 100644 ADB Explorer _WpfUi/Models/File/FilePath.cs create mode 100644 ADB Explorer _WpfUi/Models/File/FileStat.cs create mode 100644 ADB Explorer _WpfUi/Models/File/IBaseFile.cs create mode 100644 ADB Explorer _WpfUi/Models/File/Package.cs create mode 100644 ADB Explorer _WpfUi/Models/File/SyncFile.cs create mode 100644 ADB Explorer _WpfUi/Models/File/TrashIndexer.cs create mode 100644 ADB Explorer _WpfUi/Models/FileOpColumnConfig.cs create mode 100644 ADB Explorer _WpfUi/Models/FileOpFilter.cs create mode 100644 ADB Explorer _WpfUi/Models/Log.cs create mode 100644 ADB Explorer _WpfUi/Models/Static/AdbRegEx.cs create mode 100644 ADB Explorer _WpfUi/Models/Static/Const.cs create mode 100644 ADB Explorer _WpfUi/Models/Static/Data.cs create mode 100644 ADB Explorer _WpfUi/Models/Static/NavHistory.cs create mode 100644 ADB Explorer _WpfUi/Properties/AppGlobal.Designer.cs create mode 100644 ADB Explorer _WpfUi/Properties/AppGlobal.resx create mode 100644 ADB Explorer _WpfUi/Resources/Fonts/Nunito-VariableFont_wght.ttf create mode 100644 ADB Explorer _WpfUi/Resources/Links.cs create mode 100644 ADB Explorer _WpfUi/Services/ADB/ADBDevice.cs create mode 100644 ADB Explorer _WpfUi/Services/ADB/ADBService.cs create mode 100644 ADB Explorer _WpfUi/Services/ADB/FileOpProgressInfo.cs create mode 100644 ADB Explorer _WpfUi/Services/ADB/MDNS.cs create mode 100644 ADB Explorer _WpfUi/Services/ADB/ShellCommands.cs create mode 100644 ADB Explorer _WpfUi/Services/ADB/WiFiPairingService.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/AppRuntimeSettings.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/AppSettings.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/CopyPasteService.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/DebugLog.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/DevicePollingService.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/DialogService.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/FileAction/ActionMenu.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/FileAction/FileAction.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/FileAction/FileActionLogic.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/FileAction/FileActionsEnable.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/FileAction/ToggleMenu.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/FileAction/ToolBar.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/IpcService.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/DataFormats.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/DiskUsage.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/FileDescriptor.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/MonitorInfo.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/ProcessHandling.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/ThemeService.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/VirtualFileDataObject.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/LowLevel/WindowStyle.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/NativeMethods/DragDropNative.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/NativeMethods/InterceptClipboard.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/NativeMethods/InterceptMouse.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/NativeMethods/NativeMethods.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/NativeMethods/SysImageList.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/Network.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/Security.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/SettingsModel.cs create mode 100644 ADB Explorer _WpfUi/Services/AppInfra/Storage.cs create mode 100644 ADB Explorer _WpfUi/Services/ApplicationHostService.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileArchiveOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileChangeModifiedOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileDeleteOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileMoveOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileOperationQueue.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileRenameOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/FileSyncOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/PackageInstallOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/FileOperation/ShellFileOperation.cs create mode 100644 ADB Explorer _WpfUi/Services/SettingsService.cs create mode 100644 ADB Explorer _WpfUi/Strings/Resources.Designer.cs create mode 100644 ADB Explorer _WpfUi/Strings/Resources.ar.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.bn.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.cs.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.de.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.es.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.fa.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.fr.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.he-IL.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.id.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.it.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.ja.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.ko.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.nl.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.pl.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.pt-BR.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.ru.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.ta.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.tr.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.zh-CN.resx create mode 100644 ADB Explorer _WpfUi/Strings/Resources.zh-TW.resx create mode 100644 ADB Explorer _WpfUi/Styles/BasicStyles.xaml create mode 100644 ADB Explorer _WpfUi/Styles/Colors.xaml create mode 100644 ADB Explorer _WpfUi/Styles/CustomStyles.xaml create mode 100644 ADB Explorer _WpfUi/Styles/Device.xaml create mode 100644 ADB Explorer _WpfUi/Styles/FileAction.xaml create mode 100644 ADB Explorer _WpfUi/Themes/Dark.xaml create mode 100644 ADB Explorer _WpfUi/Themes/Light.xaml create mode 100644 ADB Explorer _WpfUi/Usings.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/DeviceAction.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/DeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/Devices.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/HistoryDeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/LogicalDeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/MdnsDeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/NewDeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/ServiceDeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Device/WsaPkgDeviceViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Drive/DriveViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Drive/LogicalDriveViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Drive/VirtualDriveViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/CanceledOpProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/CompletedShellProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/CompletedSyncProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/FailedOpProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/FileOpProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/InProgShellProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/InProgSyncProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/FileOp/WaitingOpProgressViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Pages/DevicesViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Pages/ExplorerViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Pages/SettingsViewModel.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/SavedLocation.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/ViewModelBase.cs create mode 100644 ADB Explorer _WpfUi/ViewModels/Windows/MainWindowViewModel.cs create mode 100644 ADB Explorer _WpfUi/Views/Battery/CompactBatteryControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Battery/CompactBatteryControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Battery/DetailedBatteryControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Battery/DetailedBatteryControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Device/HistoryDeviceControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Device/HistoryDeviceControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Device/LogicalDeviceControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Device/LogicalDeviceControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Device/MdnsDeviceControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Device/MdnsDeviceControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Device/NewDeviceControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Device/NewDeviceControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Device/ServiceDeviceControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Device/ServiceDeviceControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Device/WsaPkgDeviceControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Device/WsaPkgDeviceControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Drive/LogicalDriveControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Drive/LogicalDriveControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Drive/VirtualDriveControl.xaml create mode 100644 ADB Explorer _WpfUi/Views/Drive/VirtualDriveControl.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Pages/DevicesPage.xaml create mode 100644 ADB Explorer _WpfUi/Views/Pages/DevicesPage.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Pages/ExplorerPage.xaml create mode 100644 ADB Explorer _WpfUi/Views/Pages/ExplorerPage.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Pages/SettingsPage.xaml create mode 100644 ADB Explorer _WpfUi/Views/Pages/SettingsPage.xaml.cs create mode 100644 ADB Explorer _WpfUi/Views/Windows/MainWindow.xaml create mode 100644 ADB Explorer _WpfUi/Views/Windows/MainWindow.xaml.cs create mode 100644 ADB Explorer _WpfUi/app.manifest create mode 100644 ADB Explorer/ADB Explorer _ModernWPF.csproj create mode 100644 SYNC_SUMMARY.md diff --git a/ADB Explorer _WpfUi/ADB Explorer.csproj b/ADB Explorer _WpfUi/ADB Explorer.csproj new file mode 100644 index 00000000..642ae3f3 --- /dev/null +++ b/ADB Explorer _WpfUi/ADB Explorer.csproj @@ -0,0 +1,130 @@ + + + + WinExe + net10.0-windows10.0.26100.0 + Major + app.manifest + Assets\app_icon_2023_combined.ico + true + enable + enable + 10.0.18362.0 + true + Debug;Release;Deploy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + AppGlobal.resx + True + True + + + Resources.resx + True + True + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + + + + Designer + AppGlobal.Designer.cs + PublicResXFileCodeGenerator + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + Resources.Designer.cs + PublicResXFileCodeGenerator + + + Designer + + + Designer + + + Designer + + + + diff --git a/ADB Explorer _WpfUi/App.config b/ADB Explorer _WpfUi/App.config new file mode 100644 index 00000000..a40998ab --- /dev/null +++ b/ADB Explorer _WpfUi/App.config @@ -0,0 +1,21 @@ + + + + +
+ + + + + + 0.9.25111 + + + E:\Log\log.txt + + + ADB Explorer + + + + \ No newline at end of file diff --git a/ADB Explorer _WpfUi/App.xaml b/ADB Explorer _WpfUi/App.xaml new file mode 100644 index 00000000..cab24702 --- /dev/null +++ b/ADB Explorer _WpfUi/App.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + Segoe Fluent Icons, Segoe MDL2 Assets + pack://application:,,,/Resources/Fonts/#Nunito + + + + diff --git a/ADB Explorer _WpfUi/App.xaml.cs b/ADB Explorer _WpfUi/App.xaml.cs new file mode 100644 index 00000000..4e7f56f0 --- /dev/null +++ b/ADB Explorer _WpfUi/App.xaml.cs @@ -0,0 +1,241 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels.Pages; +using ADB_Explorer.ViewModels.Windows; +using ADB_Explorer.Views.Pages; +using ADB_Explorer.Views.Windows; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Wpf.Ui; +using Wpf.Ui.DependencyInjection; + +namespace ADB_Explorer; + +/// +/// Interaction logic for App.xaml +/// +public partial class App +{ + // The.NET Generic Host provides dependency injection, configuration, logging, and other services. + // https://docs.microsoft.com/dotnet/core/extensions/generic-host + // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection + // https://docs.microsoft.com/dotnet/core/extensions/configuration + // https://docs.microsoft.com/dotnet/core/extensions/logging + private static readonly IHost _host = Host + .CreateDefaultBuilder() + .ConfigureAppConfiguration(c => { c.SetBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)); }) + .ConfigureServices((context, services) => + { + services.AddNavigationViewPageProvider(); + + services.AddHostedService(); + + services.AddHostedService(); + + // Theme manipulation + services.AddSingleton(); + + // TaskBar manipulation + services.AddSingleton(); + + // Service containing navigation, same as INavigationWindow... but without window + services.AddSingleton(); + + services.AddSingleton(); + + // Main window with navigation + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }).Build(); + + //private static string SettingsFilePath; + //private static readonly JsonSerializerSettings JsonSettings = new() { TypeNameHandling = TypeNameHandling.Objects }; + + /// + /// Gets services. + /// + public static IServiceProvider Services + { + get { return _host.Services; } + } + + /// + /// Occurs when the application is loading. + /// + private async void OnStartup(object sender, StartupEventArgs e) + { + if (e.Args.Length > 0) + { + if (!Directory.Exists(e.Args[0])) + { + MessageBox.Show($"{Strings.Resources.S_PATH_INVALID}\n\n{e.Args[0]}", Strings.Resources.S_CUSTOM_DATA_PATH, MessageBoxButton.OK, MessageBoxImage.Error); + + Current.Shutdown(1); + return; + } + + Data.AppDataPath = e.Args[0]; + } + else + Data.AppDataPath = Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), AdbExplorerConst.APP_DATA_FOLDER); + + // Read to force it to be set to Windows' culture + _ = Data.Settings.OriginalCulture; + + //SettingsFilePath = FileHelper.ConcatPaths(Data.AppDataPath, AdbExplorerConst.APP_SETTINGS_FILE, '\\'); + + var settings = Services.GetRequiredService(); + settings.Load(); + + //var settingsVM = Services.GetRequiredService(); + //await Dispatcher.Invoke(settingsVM.OnNavigatedToAsync); + + try + { + // if settings file exists in local app data - try to read it from there, otherwise try to read it from the isolated storage (old method) + //if (File.Exists(SettingsFilePath)) + //{ + // using StreamReader appDataReader = new(SettingsFilePath); + // ReadSettingsFile(appDataReader); + //} + //else + //{ + // if (!Directory.Exists(Data.AppDataPath)) + // Directory.CreateDirectory(Data.AppDataPath); + + // using IsolatedStorageFileStream stream = new(AdbExplorerConst.APP_SETTINGS_FILE, + // FileMode.Open, + // IsolatedStorageFile.GetUserStoreForDomain()); + // using StreamReader reader = new(stream); + // ReadSettingsFile(reader); + //} + + if (!Data.Settings.UICulture.Equals(CultureInfo.InvariantCulture)) + { + Thread.CurrentThread.CurrentUICulture = + Thread.CurrentThread.CurrentCulture = Data.Settings.UICulture; + } + +#if !DEPLOY + if (!File.Exists(ADB_Explorer.Properties.AppGlobal.DragDropLogPath)) + { + File.WriteAllText(ADB_Explorer.Properties.AppGlobal.DragDropLogPath, ""); + } +#endif + + } + catch + { + // in any case of failing to read the settings, try to write them instead + // will happen on first ever launch, or after resetting app settings + + //WriteSettings(); + } + + //void ReadSettingsFile(StreamReader reader) + //{ + // while (!reader.EndOfStream) + // { + // string[] keyValue = reader.ReadLine().TrimEnd(';').Split(':', 2); + // try + // { + // var jObj = JsonConvert.DeserializeObject(keyValue[1], JsonSettings); + // if (jObj is JArray jArr) + // Properties[keyValue[0]] = jArr.Values().ToArray(); + // else + // Properties[keyValue[0]] = jObj; + // } + // catch (Exception) + // { + // Properties[keyValue[0]] = keyValue[1]; + // } + // } + //} + + ClearDrag(); + + await _host.StartAsync(); + } + + /// + /// Occurs when the application is closing. + /// + private async void OnExit(object sender, ExitEventArgs e) + { + Data.FileOpQ?.Stop(); + //WriteSettings(); + Services.GetService().Save(); + + if (Data.Settings.UnrootOnDisconnect is true) + ADBService.Unroot(Data.CurrentADBDevice); + + App.Current.Dispatcher.Invoke(ClearDrag); + + await _host.StopAsync(); + + _host.Dispose(); + } + + private static void ClearDrag() + { + try + { + Directory.GetDirectories(Data.AppDataPath).ForEach(dir => Directory.Delete(dir, true)); + } + catch + { } + } + + //private void WriteSettings() + //{ + // if (Data.RuntimeSettings.ResetAppSettings) + // { + // try + // { + // File.Delete(SettingsFilePath); + // } + // catch + // { } + + // return; + // } + + // try + // { + // using StreamWriter writer = new(SettingsFilePath); + + // foreach (string key in from string key in Properties.Keys + // orderby key + // select key) + // { + // writer.WriteLine($"{key}:{JsonConvert.SerializeObject(Properties[key], JsonSettings)};"); + // } + // } + // catch (Exception) + // { } + //} + + /// + /// Occurs when an exception is thrown by an application but not handled. + /// + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + // Handle error 0x800401D0 (CLIPBRD_E_CANT_OPEN) - global WPF issue + if (e.Exception is COMException comException && comException.ErrorCode == -2147221040) + e.Handled = true; + + // If application shutdown has started, do not throw exceptions + if (App.Current is null || App.Current.Dispatcher is null) + e.Handled = true; + } +} diff --git a/ADB Explorer _WpfUi/AssemblyInfo.cs b/ADB Explorer _WpfUi/AssemblyInfo.cs new file mode 100644 index 00000000..7ad982e7 --- /dev/null +++ b/ADB Explorer _WpfUi/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located +//(used if a resource is not found in the page, +// app, or any theme specific resource dictionaries) +)] diff --git a/ADB Explorer _WpfUi/Assets/APK_format_icon_2014-2019_.ico b/ADB Explorer _WpfUi/Assets/APK_format_icon_2014-2019_.ico new file mode 100644 index 0000000000000000000000000000000000000000..241e007fa846d84ba7e32126e5f219cdfcd25a47 GIT binary patch literal 175896 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%0AW|g4C#I* zDp1zfjAYg1>ghjYGz&~BsAfjGpHB(i<2jqx$wzl+pF3?R3+po4vY;tTnCtRAW(_1` zJF2zHXaafx91&vy!*Lb_jd@%`z?^AA$&E01b_#m01g!Wy^YxW7Jv^etu zg8HppGKt(ENTPmJOc@di#F^#ma^#lNnd8oRaRG1dUEG+; zGa%bo5k|;X;Ln&sehT6OYq>4mg8vE#>Rw%b6kts`l=U#ly6hY3SO|<<0SylCS$gT0 zeav^-HkT=<@G9yaCwjz!eclUXd2(b#jIC9|jX9UZAn?%P&)Sw3(ssM4b;kiei(-=P zx~Cs5O$j3=f^bUJWFncH_*mh%Jee0THRHu~Pd`K^YPgj`x;{JgO3EIzW{mq)7D<=o z>h8G8-PAol)+gaB&o)h)2xOU1s4zE1XdB^0a{vz|l~VARR;cTyL)w@jsKU7dblJhx z|I*>Kwhm3=|L!ensf6G;(}#1#o;kmS;iCI65(u)nZ+!G(T1d8pRjnTWEW@VY4{S3G zZY3!ptSHI5vWBaI|D35K3RrnG##u(*YJdhL^))YbrWAIUb()zSBcPZwRmuiQw6c;h zeXqxyOROX+D;C!Xe_0I+sz{47;<#so`;HzY>uS^S^JhHkvwW*Y9l4DJOyIv` z&6G>0!m{uTH__@!wTAgu@E6?9WW~y)z&FB{38WP7&Hv#8yPQ5AgaU>28mmKExS)Kv zAox-x*k`a{q(E_ojA;1a&SZihh)w{GzOj2LOWLD5t`hl;{YY9V^m@APabalD!DR;gz{3ydS|gXleZk9;p3Bt|F|mX4b<3%Ls5KS`rr(O)rDL zv$B+f=~~6X4TY->7iLjs;kF;zz+PO9->n- zxD~~F9{*@p>!$JvCK!CLiLLb)Z|ypp;42z-5wTX4qF6~(!C#1vJG0|#gHwKj-&aSr z!hZ@~RFmkoO>K{Vv{Tf?*MdLOYSjcRKFzS=QW78d(aY-{gs=k=UFIS%p&@Kxz-e5W zqzTHgK?W3s(9df7D{Q=Utuid9G1NqnZyJ$bH8`JPcx`NB^}#D^i;}!E!d)(<9MExt z>8ffc8NqeXxc-g?#hZ`1zcno)fY*`u&A3w zvSI8FO0NEEwBN`9!paiIk9rDN^x6uZ3e_dR=>5Zb-rKhwmaQ14D- zhxBn^@Z%fOZOh;V)15Kw`$HueI4*JmF;pRyZ#7A@$ zT%=c1I+Xs$zeeyxjhZ&n4*D(4J{kN=Owh7<68=xgo%mnN4e(2#?R5)AWkrVOMDIT6 zw;XB-Mbkf|;7X&~N#HY0^Kx8xAvrWZ(g{)IrPO`fS1NH0Xr@LDp3tWNn^#Y{aEh?+ zMg?VcRW@FiF#`FsThR%x$WM2)ZHwsNstE;9Vfw}m?)K9$|DqGVDix3{kF8nDP(gMPUZu zaq>IAo?Y9L6)+cofR5xZVA4hp?vfc7QV{8SHqI;R8@IMi1(?PWl%s2HiqJM{)yzKr zYuAz(p8{5FU%dvL@c-nF1-7rB!nY(Jy0@B9OdCHi^%y}0gR_o%K&h0}T10RNj#%h7 z*MRsG8x`l0B+t_Z&)JU*;&o^Rz_M1TwQa+rMag@WR1oozvS}#~QW~$f0+B|K6r^DSJ?QVw#fAFiIF5RItQ@t_kMmt%JlV)~SH0q9J75 zkRFmb5w@wTRG2YJnqb<5fuFs#`OUqn@98TXmoBW}pq5*i-0Y4&?OXNn;Tb z#AI|qpo2DvD`SWBC@}cJ@9C_F;C~J9H{yS7;SV&7O%Ul;5+Crl!>e002h&8L0SVVJ ztzC2LwTmD8@-3&Ij)fG)g_Nf3W|CESGVs?3-!I?Yvi#`>97I{q*bSdRhZRI^?O^@4 zLmST~Nl;g)mMm~CHXC&#L8$ZcS;#y-_>(u*H-qUU+OTjr^Z>VW)Z4J0*$fZJPJHAVOWz*YVrAf(QoTs4~}_EulKxth4fxx{-?L)bOo5B>}NsRR6V zgoKl4E_N<8;1&OcYUBjpg!5BUG@<4&8zTmFnfus?wNF2^?&(R$i0&QKnF$ED;Nk3q zangy8EVi)QXW_ZzJ=hUp%EhDrgT{RHan%pORdx#I=_Cf?;_{W^VhVN4tBdemt-Xe=R`ZR=rtru z3>s1NRIiV#68IwJj`T&oinLs&Aqjzt=Zql#CX`61f`T?h0#)U+9O+lpty=x4S9pon zWx$nu1pO5U+CtbK0eA8ru*861&e7DW2whP#%W3f!_*cp)s^04!J`7*5G# z;)7Jv#tsWa85@M`%MDGtqm{$1wNq2R_b5s9!H!5xu~FF&o!JN2ieE)B6m~E=es-es-njIoHfV~vfN4wQ4HqSf@f4vbx?(B zSJ8}X%^hsUkLiQ%_&9ui%IEN^kTN?+P7rhr0CWdB3`DTe4SL_*d_=|vD73OBZ zzZ`!{VP#5&Xo3=M-!SW6d2bLC|6IC%X|nJ6lNhMD78o#nL7SsYxuWVy=gUFZ9NN_$ zN4OU!@BQxMU5`FAK!hb$NMRs}FpN6fDW{%P6BtpRvU9|B1$%xJ*@uh@8DkW))~1ag z^uvcvB`HA}kywU^FoYWEGkr>8n!n!)_)|o54Fdno&rCg+v^)0)r#yqBtSlAol^cng zp5%@6k_ob~`sI<<(x;-`&OGjMKE*Hgd#~2*9UQE!TieMP%qG$+85%@q05=I;0f8ZeIY3zJ%T|Wv5b+eYeRs_J{Lt3N z9=W@9i^i&~swIp+K%C}K_CyNWrh9H?XDyVhTYGNx)RI%7vTj+N3oMyPButEfZXg5`dn56mcE?D*^rZI> zA8dp6+}*G9FHznliD5bMA%^eVf<5OhpZDO9ejVF3YvN$XkR=$dZ?r)Rd%Mmp>_^_! zamAeRVXli~zI8iua?knHz+VmC1Mj+%YJPXb@lw1WG8QVOQyw~Xh3ed7_$xB0NT2;^vV5QmoOyx4Q|wrTJ*JRKI;SMt6$)(GQ`phW2=C%V|LK!O-1mCenv&$eUkp20 z_oyV=_i|DwvXNiHJ>$Q2Ir_oY_g`B5vfsiNJm$XSxA23PR~`FcYx395na4dZiWgx> z$w}ADXs2HcyNc6%U)!^S+@?No@7aXl97CD*sto6K*|=Vjn+|`Sseot-{olHJ>gBYs zf@APG-n!Fr(a4hHpy2@({|^Eiwm|giWBj*Wg%QcZt$O? znl}Ut?cbd;eevF=(gdH}2!z{0qD|#NP^pp|m@LaqNOC2WU6(RTv1u|9ZXjE8JD2S7 z?K?Y|j13yl{fxmMwm(OdgMwp%TDswxs_9L}e^_IkhU)OIqkh4{O@o=x`P4AaZDK99 za`A-1)KHB5ka7T}8uV+`HKQj_0FX3nIPHG_fd}E_|T0|DEth0g93c zJ!$OcTkh;=7wWX2G%+OC;3(7s3q6zziU+8DYl;?C3@v=bP!=24DDMn|OJPdT9_RTz z+S(ucd}m>T%UOd9B3y_MgT6=+ZyEi$E%0Bjeol;6(hnG&h7=i?gTTOe!hm6YVH_xH zjr;F)wEP-@FW#-lLWohA@VC*@VdGyk3VuLEB|gnRx8 z*WySIOz1HHKxY9D3>#Bt_%lz}dyPnF-E!lcfHt#uoUG4_p4~Mg8p`8EA1R8GSPD61 z^P2y1_|pU!MoK6W89kLDx6DCAL~LrKS{{fInr?T;z@eize5Tkn(KVce!J}z(F*pwh zeG_)#za0LeCynxEELKx*#YqAg1-Dv)-S`;IhkjFx@X<1h>6dsk2;1qMW$?*~a}pm? zv?263=^D!MP>t{ZY76fKJvTA^TJ%DDxR~q1NL15M+ zs|NVj185d}r2+mH1ZF+5YJh(|fM&s08sKk1VAdn62Kd(lXcl~>0sa;QW<9dH#qh_! zzAo_%>QTYl{FQ6KU*J<8|94ZZtI8jv)T{Q-b&2m`NyWFRgtzvfD)?Nr58PDW_yeX& z)tvw4)l-f3=XEgi_>XA?iNMKP9_8b2h;=^6XrFJy|GL{lmcg?U{B@^7@dc(A2>erD z$PahTizL>!xaC%ru$=jiOIQI#c@#zX7KFQ;KNSX^E1IApb^!b`_-|c9{KGvgQ=%M~ zOg2Szz`#oI*ZnUDuOaa$Hm;a=0%8~C%l6p_dD<-IEW zs@1Y^MEZ|Ox))w13;bd0NVxb@p<7m|_zV&&Puzn{@Mn#Zi>Z6@6Oly#%ScL{4yu5^ z%Aa_`!2kKxj}@m8=-#DJ#WMlRI-auMF{JB-! z!tJZ4TuKA}PWASKt_*)ZKqs?TVPEFX7+#PNmLKj?6hZJw978ZdmNl@Q*sA-JXB+Vrm$nAhYZ1hh7Q(w5`NQAPh4N zK`gAJ@4FIzfM}nDM$945tNd0-f9Q3-sx5qOq*J!Ru`u2Dn~%45Y%X!`BzA#7MS_9o zVTlg-%q>k065zyGiv9@v8Q;ZmzmW{VqXu^Q_I>B+W^&5O4=2hXp#QKp~ zMXCv)MV(sO&7C%ac>bGLPLV~s^yvN7F6>qRHMurRo~Cud>Yq>bi;mSQPqiM|RALLy zpE{~Tb0YH*u>;OW0{#(dldwoSaULi#!Q;-sx6G32MQw5?|%p41J%-4H?;D34;V-2{?q*uispt`qU1F`7svhuAA{>I=|Hy`lg36{7H9~OLpd&JxCvh@==I6Y9n>iF+b^G?cx4WzQ-uLs$bNY1kId8pn zy1Tl%`jnT25Z%pOLJ}m)M`afs~?=cG^bwn4uhZC5QA_2KJF|Gx9iZ?=Q{MP8;9q$LbIOEooKhTla0aAEg`54|g__dw|_9^k)gST*nB|NYMm-ojy6JA3#2 zSoJ{G(T@1s8gH@hci;d0U)KI(*S+7qb0^XeVUZ~$6!Jcz$4i;qAUrsfw;^hmvIrS{ zr>%>TFzQDuaw{Tv*So}@w!nEV>hTXQs_0YX{y*f9hgMv=_uYFw*|K2I_S{(08@FR! zk1wz)mbQXeikiiHu#gkdkmg$!r=&iQ-$1#hL^=_yB(i+dQ@1v&^%BE+_xptZz`oa^ z^jZNA_1=mskeb%iyz<1HeOP)JD;s)Q0=z{h5o!Aqot06|BBC)xFF1qzs_n}ym+;Zs zcl7V+EhgeG0gz}5cqf?G2DkuuY2=Z=6C3?b>4ZFEIL$rsJ^pj)!Z6%j(nF=2oU5qN zWx`5c(0HpFVd0NmYgW!14IZ#0UM@J3Yyn(!JoDa-7uWup`n6~ftR?!lUH2j_SS}T$ zmkkowatTk0&U2zx(^)we|6Ud;+(Ev4YtdIb?*8EAANJ{dEO_y*$jSftS0LPZA@u3e z>MyS^-}^S6CHFJ$^M>HB)c8jQkypga9_cS~jpd)-yYKOJQyP2k7rehDTL6)t8(+vR zv=Z+~9(B{E6+m!1Dlu3ORkdpEefc)ylgXB@e?kX3u%uKO0w^pXwwOJK-_ z2#ry~FMkVq#_&MzjWir`$bwlTzTSDyH%M%lTPMzYv#Ic3+MuGdmz{%vdE*t{kOi8+ zTMNKj(r(G#_tq|&b!n|Pyp4OYx73Aqi^N(O7y`+S$>h52e&;>xUtGe}U+!9;JFjv* zPOPed{pfHlnmyu+_pw;yl5a7;=)m|%DFZ17RV{b>$esqz7ifZ=tDgArls=tWdDkFZ zi0~ht3$S(;o{o$9VVFh!a`OH=OYZ8{vs2yU>n87ee+|~u^U`wSNiY`-*&{91w#HfT z&9=q37wmrdhj-7ptbdPo&1&O(B0IFULEj$j@0>q+=W8qe{{G5++i=~(A`4gy$a|=) za`OG^??rcLEAsc=`Cq%%eEjx`O;6tXqZOAgo;q^&4HwRyc%k=ilpjh?@?N3hH*ewv z^Cu5mHf`vS?zrN`#}|FJW%YME*L?LKOZRNQ4b#T<<&qHYeGBWDkbm!9fRVji8e}|o zd%5xPbo`H9_xyABn(sfjA4|Ib!~3`3FIQ~$y6<9<@9vL2FnT-ai6xy!Jwq*v*^mAj5SVc5 z!OrpHjg%hfpG3M);3b7OlHd&{c)5SQA96hAR>y;>{Yr1N$Itz~zcZF0qLPA|dnmvQ zWeiYwPe9(xQg30JV%=d%B*HuQ!;)V9ltn)Sd2Xt3E(!4dYd+@B6uVw$@py{GZ@GN# zwPt)6{D$xwQS089@%}nKghdA!U6TPaKnBPF86X2>fDDiUGC&5%02v?yWPl8i0Wv@a z$N(9z$bdJMhks;%43GgbKnBP_burM@tFAYf6ccjm9nxxl<-MuT2mj^ffBLgf_%9hC z17v^fDDiUGC&5%02v?yWPl8i0Wv@a$N(82 z17v^fDDiUGC&5%02v?yWPl8i0Wv@a$N(82 z17v^!taX!3k?o4Getz+QPY0 z1|8nCaiQpry1!|Y+F26^ezJK{HL%AzI?lk%iRXkMeB!~Y+qG$KjO@DRhg~wH$Kq+{ zKX(84*MEDmquYaPzVYx5#~z6?A+EWL45YvSg41{k zAd;^g(@zgw+z6om!=sb%2pk38SKDv@{Vyis5aT1m#Ia|o02DS(T4 zzh3PF!GCmJ6Abv2+;2fY3U>tk38HuRW8d94Ct}qCkhtj}0~iKGsGN36`>9tAc=qQv zM7&N$41m~+34wA7Y&Q zLn$K+;EH$0tYOaHc5}eV?y!hCZPVJ6TUAL%f`o@#f z3R+MPp`0NGI<~F*cV1p`k~z2h!Wup^|Fr<^$k zaDm11Ufn$jPI*^8-sEvOX*{?K1Ufv|oo9g% z-td+->EQS6fAs2Y$9Fhdkl;b!)7Q;!houaKOf!HPlHncQbOH}v!f8zHE`Sjsbx<_l zsTYoxdo#oU-X*0LB@9Yfu+-jifc@q01zpH+_i$~zX94t1IwKAEY!I#yMU!tVl@BUDvvM5(Mg=@iqaqa4CA&N6_Gkb1!n z$e8rvEOJ}GFj-ny!iSTIzJRt>$LjI>!OIJzHFW_qMXQI3h&9DaXUv`zu412h7ZC=F z#l?U$rBR5>OC{Msw@0|bCj*sKwJNNOG^J6fLP82w88I(hA&E-C@+{yo1Jaa6!DZyO zuwinDaCJuS4z^&>Z!8Q*QyPU>TvV75dihnSbA>CU zm5Xu+%Q(w`G^J5+7PduT$fF`$afr-fL&V}*n$jr5;-WMo$ki#39GQ$-T19x2%M3`2 z83l{T)tLrJT4fOlT~{yz#;_Nq#fqzW9=1FkDgB5=v~-vj(m(`kqx>|hyjT)qmYo(C}x*VA0m*D$u`O%EJFs0Wk6!g zC=_cg#`5Y+%a%tqO()w)jQD9P6AT!`UX&KAfT_jJn$!vs&a|}H78UGT0?a7bt@uUf zb`w-LTyeTxF!WxL3`l?(1-n(oVqSs@>sgeX+uUv~=-p)oB*2VAs5`tdj%MqmmCVK$*`-XXvokAKX3IPB0G0f}`kLh2IZt|+HK zjP9~x3WIVdpV(TOLlv-4&I`Ys5~U(2hD_S(u_D7b7H*DWB(5^z+9<^3r5rnmdBKMH zmjC|vP32gNer&;0AiAl_DEZXG*Qwo6X%rEVFri@dq5wv-!CZN}-uvU6&K(aV0Y;D@8{Ub>wM5LT0n_b-V~m+0eL}Dc z1=R9597fc{de{p`aF-5;Di~c(2SY(G>sOCcvEd8!)N(V1oB{=5S5kpFlLtu?;JwU> z*%#nDAhZGk6>sVfZQMxqF1d9`zKUZxS}BHZEe^!tFalFBh^(rz7w+;IiB~TkjY7P> z%CV*8mWL{$oJm$_?E)M{kM+_RBGk?;j;IXliK`s z%aT9=Y(j}$QtXEdh6l>viCFf+UA`pqfuqXY6pu!sOye;gj<2qIbUo4cLBVB zzVhM&bMSP2VyvCxrm$kTT;b6QXo~<~jOTRe)WW9-YqVjR!axCRLMh9)p|Cz)1hevR z4R>&=8;M<`H>;|N-Ca2b|MdK9EKH!n8$P(ZPGdPPLT6iaUZ1&p!opeWhv`|8BM{-V3Oo z$xGZqz)MDaHHJF^z8yxF32$C7n+|zYFrni0-`=v}fe9F zPQJ4LI~!*^SV6mx82@(pkLVpeoJZ~*i?7Cvo%wuc0SXU2r|Uy^jn-f5MD$zn#TftD zYGS;)MU$b2k0dJndbP)RDb^~&0@lc>h>JX4q9M5u-tA)k$?tx4z42vzw(6TSTE>6U z4AZ{L3^4v(ZXIGvngKQb@ru^yj_-esv{0Ev2Jl)0a>nQGwLKfSOn9F1U ztAdWXsFyJwY$@@L@aPLq!KB18tEL|wUL(D{T}`&o+U2S=@3_ojhFdt#w)Udy75iNv zye_42nA}m>LYo{9$yru&E`e>#`o?EjO%AKU4=)*gp`3q*IUf}dw1g;yc5Ll?dN7;=Akc_{BKc4;2HqbNzwVfJ}8--Y(-SI(3adN zwhqL56M0I-w_4kOH^UYZ)h}Skhhz9I5+K#RWy;x#sBEDvvuV;l9IpbAUc)CD69`kw zvk_iXz>t(FfEVxk%4|hcw$O$*tAl$P|A7E=t&IZk0-LU(%^V)azvB@VfEU=zT)bHa z{O1?uzWCUU&##|^Pb4sV5M68c`#%5)r~nMu0FNq^Lt`L>0-NPnPU6@AyufA>9xK)6 zvp>H9V=1j#H1j`p#*ikG^DLb{RK6#2v;Plx012o7jMfKuQ~?!1bxLcgKnMjkOS75W zQ2}^?&E))6l4ZO?nmuVCk_H6W3v9X>{{fJ)iRz$ICA=LI3r4}Ub~l0)rE6&2#G~dD z7V2~%Tk-GXzvY^(>+NFs_d5{dniq?SxjC zVgIoEUKB*D(iAlrcx-ZBUCjh0LZipJURA1wWBb0dan+z zK5?^OBV-0xb=wH_Giqn!A0Y2pI0AruVEGBZjGOTfcyIlFrjGooOZ!UI@F}-mAfHB#G79;QvZXhI6s0Xx zS@DY=j&OxhL>)H9)jPGT^UJsy|9}UOI&xG;s)lOn1@dX+D5H??C|i0XNKx8Cl@-58 zxi4HH*`p3SyYGKd9jThWvVdMw(a2FoL5evd7(t5C5s=QpVEpS%Bf(=7^zwE!Mv$Vk zi_LxSjDNk2BzTO1Uf!<82vU@GvAOS^@vpa$1dmbB%iGl$L5k8YHut?V{`EGJ;4un% zdAk}TNKx9w=Dv5vzura?JVrq;Z&zakDN4K8-1pA-*V{;f$0+FK?P`o5MQInC``#J< zdK*db7zMq&U5yc>DD7f%-#g=9ZzBmFqo9|!t1*HUrCn_9duROXZ6v{C6!h|THAax4 zw2RGs?~H%FjU;%Cf?nRP#t2fBcCoqdo$;@?kpz!X(97G^7(t5CE;jeQGye59lHf54 zdU?AVBS=x&#pb?u#=qW15un^#V-)oAb~Q$j zqO^<6eeaBay^SPzjDlX?uEq#bly~m z3VM0F8Y4(i+QsI+cgDZoMiM+mK`(DtV+1KmyV%_K&ip5O8%gjO1--mojS-|M?P7D^ zdj;peLOL%A1LnVSKNPJXRP!GqZN9#|9rGW`N#qMwnkmF-0A64-S7W zt*__$7t+cU@-cu#f@V#u!1a$%DXxE%uhFRwFyOC$hqU?raxm!m_w0)G?}lVy{ktK6 z>OZOgJgUI;@BG_epozIzkb#i(Pa8I<-=k~m-rbMw)y?J)000AE{3gcotX%&zUnj6& zbF)A=18V;Bd?ee|BmMz0ycR|64X;j7rcx-JEwm{R60ZwfT2JWIBAjHqd%GUrvZ&>~ z3i184F;vC_1~9{~6(bo;1=5AGgIHN}Wj)+k6Z zM+75CQ91%L(=g`WG+6k=JL1=Vee-*Nob%4c*;amVjn`X%A&?eN3od?FCfj1!D3obD z_QQ=JMH#!hDm3`JpIxsm1pLUoW9^dgi+jdO`{G9M+#?g?#UsXzLcG4pw`K$>%JS{g z!Y7*-p^q(;4*9`}b~yn60n@!ptIyupzwB>ZsKmHYh|5b{c8nlJ8JCwTw=;LjAfdET z!v@d%^!j{+?0RYb8&6LA@Ri&06$saV@bUs24kP)BJ^hpG06@Sjm^wIL$&&SA+$fZ6 zHlYKJAVryw(`qxj_37z#&G)YY3~tXo^9192)pd7VikD>=na7aPh6g4T%MuRh-0^4} z4kIw79}cH*EsfivuBkp@RX?g&jo3E|#ac_cyb+`*llERc_NQJoKv;!v@4}IRx^LQW zW3wi;Ql`x@N4)*qtU#HNO%!h3@^CpErZ6`;_?!g9V19l#^r@}z`z5`MhUSHFbUz^jRzv|;k(DEdod3O14q$$ zLt%g3kr;tNIUJ#m89uTL!h_2%lfCf@sux|3*H`)0bcCv=eEYPZGB9rBsVcn#`gYp6 zX`Wx;2kcj%D1K?R5j(>rv;UP-7hatKdl1Q_`2$?-_P{RAW-qWPr5)uS%Q>#KZgIzm-bzI|FSGJrl0I{zxwkWW6b z^-oq_{`s3_y^zIc>_1<1Mc`={#^J800aYXHD9S*k0-I4Rq**2aFR+;=l4|pXtNnFj z`l(ZceDY5{x$T-I7pp@<5t+e+I^?6n;ekek3pk4L<`Rx4;61Pzer)=C1MmWy>Eo$3 zk3U;=MW=Rk0ot*7bX^n7OvW3ljq4}lVb=JG3NH27g$ZmJ>c3#%NjOBe1LJU!CaXFI zRg)%?D85u|x*Wv<%aq9gKAFU;Sot6^&gmUY(j}$I)Ih)hf51$Qc%iYoQZ3rprf^`F@hAOU2GNa9n;c| z?{KuRjW6Ykn)8#79~i-KkeG#wGH9_^T9XicrP4R#qpk3 z{m6a7s01gx1u#qS7==J#>zhW9qO=aLf&pOM9({PBdiCOq(XbPX->xX*wDk5O<9zY(M; zT|-Me?lU04V-#G&Zv-hy*U%D=`wU3%7zNkx8$pWFHMGRzJ_8awM!_}wMv$U(4K4Aw z&wvDvQE&~v5u_+xLrXmFGa$iZ6kNk^1Sv|_&=QaP3`p=81=sK!L5k8fw8Y~+0}?z& z!8QCwkfL-AE%CU|fCP_Ga1Fl^q$piOOFZr~Ai-l4T*Ge!DN5JS5|8@~Nbnd1*YF!b zin4@Ns9dWc@ubNBGI~ky=mJtWrU@pBFCC<+dFH1@EWsJY9m`ZU1HXD;f*L;o87maH zR{a3Nz$OyNxCWSbTxQ_HK?m|K24V@DeS5ZZ4ZIFApjk2d0@pATkDClEn>j2HJb}%L zW6y96c>tkt6L$gR9OkotYi;KU9;__()Wg>eAKc9tJgV}s`^P(iSzxH}D3gKY7{F3Z zspX)P@GdAMS6^Bt11baP9Tkkm^b4p6DU*SO7--j~`DbsGwXShOr$$2crCBl{3}CgU z^{Xwc#24k15s(zfKtc@6yYbv8p=hN{LRF?><|S9PhC&Z4l#`!e$}^2{C|f53c+! z2S3iEgz8SSRn7n&c-OBU7vqIGAyZY}OsszF475C|>C)*#KY!E3(H{LI3a>sEO`AJq z5SC!ZO5%q$ZWLaad#jUyh7Iat#cixRk7c+qPau{hT=hUrNE6;<2|113?5>yQA zU9k6>(WmX%x-@q0G?-lm!eb^Y^%^^}cX&mx_u4W2vQi!5OpSr?ut^O@IgYThXLv2J z7t0`Gy~T38@YsrEAUty1ukt6G7jOD1^mO;@LWJov5FR<{<0;nTl<{YV zR{?t;ST?Fyvn*dF3^Z!kAS8Y;wcBMLFg#Vie!UR<*fpqsXP2u$Y<3yI$W=)EU^vSz zyb$jnzH(cqb_IGqO%AKs@$8HcBivQx3}DzcB>r9g{&&QA-9wQ3yO#6g%^l0CJi^1P zfBogBhewf{A%r)K;jpo{PmgxldKVMkQrw#a1DK>yFp!-3{ulG9FsrH{W~7>V<=>_z z!5D2;5(5|&4}SzG7-?=6m^E==B{dW4k{kn3`Vwvic|KHKk8hck`B%v`D=kN40HM;p z&5`+HDRcVTnueP;+!(R$EJg;vz$5pL&0L)41H5wnaJDHKD3*aalLzICs0``xwOz3l zXL&LJ1~9HN=AvF13UwXo-~nAY$&JBWG6OgT@U@E`SDDaYo`ahiW=d`j9%zLDJPWjK z-8>U9FCZsh*`HT_E74MsPH`b!I(fDDiUGC&5%02v?yWPl8i z0Wv@a$N(8217v^Yexpc8TfR^^6&!O zO9m>Nflqg=sO$=|c4Q!&0p10|`QhG*XW(;ogsS^tu;L@UUfDDiUGC&5%02v?yWPl8i0Wv@a$N(8217v^fDDiUGC&5%02v?yWPl8i0Wv@a$N(8217v^APxrp4^Z(X>Hq)$ literal 0 HcmV?d00001 diff --git a/ADB Explorer _WpfUi/Assets/app_icon_black_2023_256px.png b/ADB Explorer _WpfUi/Assets/app_icon_black_2023_256px.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbcf29ed4c991a5b80157c4764a96e47d1307e5 GIT binary patch literal 5719 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 + + + + + + + + + + + + + + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/AdbContentDialog.xaml.cs b/ADB Explorer _WpfUi/Controls/AdbContentDialog.xaml.cs new file mode 100644 index 00000000..f1b77a64 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/AdbContentDialog.xaml.cs @@ -0,0 +1,67 @@ +using ADB_Explorer.Models; +using static ADB_Explorer.Services.DialogService; + +namespace ADB_Explorer.Controls; + +/// +/// Interaction logic for AdbContentDialog.xaml +/// +public partial class AdbContentDialog : UserControl +{ + private static string GetIcon(DialogIcon icon) => icon switch + { + DialogIcon.None => "", + DialogIcon.Critical => "\uEA39", + DialogIcon.Exclamation => "\uE783", + DialogIcon.Informational => "\uE946", + DialogIcon.Tip => "\uE82F", + DialogIcon.Delete => "\uE74D", + _ => throw new NotImplementedException(), + }; + + public AdbContentDialog() + { + InitializeComponent(); + } + + public static AdbContentDialog StringDialog(string content, DialogIcon icon = DialogIcon.None, bool censorContent = true, bool copyToClipboard = false, string checkBoxContent = "") + { + var dialog = new AdbContentDialog(); + + if (censorContent) + { + content = content.Replace(AdbExplorerConst.RECYCLE_PATH, Strings.Resources.S_DRIVE_TRASH); + } + + if (copyToClipboard) + Data.FileActions.MessageToCopy = content; + + dialog.Icon.Glyph = GetIcon(icon); + dialog.ContentPresenter.Visibility = Visibility.Collapsed; + dialog.DialogContent.Visibility = Visibility.Visible; + dialog.DialogContent.Text = content; + + dialog.DialogContentCheckbox.Content = checkBoxContent; + if (string.IsNullOrEmpty(checkBoxContent)) + { + dialog.DialogContentCheckbox.Visibility = Visibility.Collapsed; + } + + return dialog; + } + + public static AdbContentDialog CustomContentDialog(UIElement content, DialogIcon icon = DialogIcon.None) + { + var dialog = new AdbContentDialog(); + + dialog.Icon.Glyph = GetIcon(icon); + dialog.ContentPresenter.Visibility = Visibility.Visible; + dialog.DialogContent.Visibility = Visibility.Collapsed; + dialog.ContentPresenter.Content = content; + dialog.DialogContentCheckbox.Visibility = Visibility.Collapsed; + + return dialog; + } + + public bool IsChecked => DialogContentCheckbox.IsChecked is true; +} diff --git a/ADB Explorer _WpfUi/Controls/AdbContextMenu.cs b/ADB Explorer _WpfUi/Controls/AdbContextMenu.cs new file mode 100644 index 00000000..0f33c6fa --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/AdbContextMenu.cs @@ -0,0 +1,10 @@ +namespace ADB_Explorer.Controls; + +public class AdbContextMenu : ContextMenu +{ + protected override DependencyObject GetContainerForItemOverride() + => new Wpf.Ui.Controls.MenuItem(); + + protected override bool IsItemItsOwnContainerOverride(object item) + => item is Wpf.Ui.Controls.MenuItem; +} diff --git a/ADB Explorer _WpfUi/Controls/AdbMenu.cs b/ADB Explorer _WpfUi/Controls/AdbMenu.cs new file mode 100644 index 00000000..16915553 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/AdbMenu.cs @@ -0,0 +1,10 @@ +namespace ADB_Explorer.Controls; + +public class AdbMenu : Menu +{ + protected override DependencyObject GetContainerForItemOverride() + => new Wpf.Ui.Controls.MenuItem(); + + protected override bool IsItemItsOwnContainerOverride(object item) + => item is Wpf.Ui.Controls.MenuItem; +} diff --git a/ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml b/ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml new file mode 100644 index 00000000..aadca67b --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml.cs b/ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml.cs new file mode 100644 index 00000000..d7c96600 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/DevicesPageHeader.xaml.cs @@ -0,0 +1,64 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Models; +using ADB_Explorer.Services; + +namespace ADB_Explorer.Controls; + +/// +/// Interaction logic for DevicesPageHeader.xaml +/// +public partial class DevicesPageHeader : UserControl +{ + public DevicesPageHeader() + { + Thread.CurrentThread.CurrentCulture = + Thread.CurrentThread.CurrentUICulture = Data.Settings.UICulture; + + InitializeComponent(); + + Data.DevicesObject.UIList.CollectionChanged += UIList_CollectionChanged; + Data.DevicesObject.PropertyChanged += DevicesObject_PropertyChanged; + Data.Settings.PropertyChanged += Settings_PropertyChanged; + } + + private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(AppSettings.EnableMdns): + FilterDevices(); + + break; + default: + break; + } + } + + private void RefreshDevicesButton_Click(object sender, RoutedEventArgs e) + { + DevicePollingService.RefreshDevices(); + } + + private void UIList_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + FilterDevices(); + } + + private void DevicesObject_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ViewModels.Devices.UIList)) + FilterDevices(); + } + + private void FilterDevices() + { + App.Current.Dispatcher.Invoke(() => + { + Thread.CurrentThread.CurrentCulture = + Thread.CurrentThread.CurrentUICulture = Data.Settings.UICulture; + + DeviceHelper.FilterDevices(CollectionViewSource.GetDefaultView(LogicalDevicesList.ItemsSource)); + DeviceHelper.FilterDevices(CollectionViewSource.GetDefaultView(VirtualDevicesList.ItemsSource)); + }); + } +} diff --git a/ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml b/ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml new file mode 100644 index 00000000..4b2ad95f --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml @@ -0,0 +1,268 @@ + + + 13 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml.cs b/ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml.cs new file mode 100644 index 00000000..8300bbef --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/ExplorerPageHeader.xaml.cs @@ -0,0 +1,398 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using ADB_Explorer.Services.AppInfra; +using static ADB_Explorer.Helpers.VisibilityHelper; +using static ADB_Explorer.Models.AdbExplorerConst; +using static ADB_Explorer.Models.Data; + +namespace ADB_Explorer.Controls; + +/// +/// Interaction logic for ExplorerPageHeader.xaml +/// +public partial class ExplorerPageHeader : UserControl +{ + public ExplorerPageHeader() + { + Thread.CurrentThread.CurrentCulture = + Thread.CurrentThread.CurrentUICulture = Settings.UICulture; + + RuntimeSettings.PropertyChanged += RuntimeSettings_PropertyChanged; + + InitializeComponent(); + } + + private void RuntimeSettings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + App.Current.Dispatcher.Invoke(() => + { + switch (e.PropertyName) + { + case nameof(AppRuntimeSettings.BrowseDrive) when RuntimeSettings.BrowseDrive: + InitNavigation(RuntimeSettings.BrowseDrive.Path); + break; + + case nameof(AppRuntimeSettings.DriveViewNav): + DriveViewNav(); + break; + + case nameof(AppRuntimeSettings.InitLister): + InitLister(); + break; + + case nameof(AppRuntimeSettings.ExplorerSource): + ExplorerGrid.ItemsSource = RuntimeSettings.ExplorerSource; + FilterExplorerItems(); + break; + + default: + break; + } + }); + } + + private void FilterExplorerItems(bool refreshOnly = false) + { + if (!FileActions.IsExplorerVisible) + return; + + var collectionView = CollectionViewSource.GetDefaultView(ExplorerGrid.ItemsSource); + if (collectionView is null) + return; + + if (refreshOnly) + collectionView.Refresh(); + + if (FileActions.IsAppDrive) + { + collectionView.Filter = Settings.ShowSystemPackages + ? FileHelper.PkgFilter() + : pkg => ((Package)pkg).Type is Package.PackageType.User; + + if (collectionView.SortDescriptions.All(d => d.PropertyName != nameof(Package.Type))) + { + ExplorerGrid.Columns[8].SortDirection = ListSortDirection.Descending; + + collectionView.SortDescriptions.Add(new(nameof(Package.Type), ListSortDirection.Descending)); + } + } + else + { + collectionView.Filter = !Settings.ShowHiddenItems + ? FileHelper.HideFiles() + : file => !FileHelper.IsHiddenRecycleItem((FileClass)file); + + if (!collectionView.SortDescriptions.Any(d => d.PropertyName + is nameof(FileClass.IsTemp) + or nameof(FileClass.IsDirectory) + or nameof(FileClass.SortName))) + { + ExplorerGrid.Columns[1].SortDirection = ListSortDirection.Ascending; + + collectionView.SortDescriptions.Add(new(nameof(FileClass.IsTemp), ListSortDirection.Descending)); + collectionView.SortDescriptions.Add(new(nameof(FileClass.IsDirectory), ListSortDirection.Descending)); + collectionView.SortDescriptions.Add(new(nameof(FileClass.SortName), ListSortDirection.Ascending)); + } + } + } + + private void InitLister() + { + DirList = new(Dispatcher, CurrentADBDevice, FileHelper.ListerFileManipulator); + DirList.PropertyChanged += DirectoryLister_PropertyChanged; + } + + private void DirectoryLister_PropertyChanged(object sender, PropertyChangedEventArgs e) => Dispatcher.Invoke(() => + { + switch (e.PropertyName) + { + case nameof(DirectoryLister.IsProgressVisible): + //UnfinishedBlock.Visible(DirList.IsProgressVisible); + //NavigationBox.IsLoadingProgressVisible = DirList.IsProgressVisible; + break; + + case nameof(DirectoryLister.InProgress): + { + Task.Run(() => + { + if (!DirList.InProgress) + Task.Delay(EMPTY_FOLDER_NOTICE_DELAY); + + Dispatcher.Invoke(() => FileActions.ListingInProgress = DirList.InProgress); + }); + + if (DirList.InProgress) + return; + + if (FileActions.IsRecycleBin) + { + TrashHelper.EnableRecycleButtons(); + } + + break; + } + case nameof(DirectoryLister.IsLinkListingFinished) when ExplorerGrid.Items.Count < 1 || !DirList.IsLinkListingFinished: + return; + + //case nameof(DirectoryLister.IsLinkListingFinished) when bfNavigation + // && !string.IsNullOrEmpty(prevPath) && DirList.FileList.FirstOrDefault(item => item.FullPath == prevPath) is var prevItem and not null: + // FileActions.ItemToSelect = prevItem; + // break; + + case nameof(DirectoryLister.IsLinkListingFinished): + { + if (ExplorerGrid.Items.Count > 0) + ExplorerGrid.ScrollIntoView(ExplorerGrid.Items[0]); + break; + } + } + }); + + private bool InitNavigation(string path = "") + { + if (path is null) + return true; + + var realPath = FolderHelper.FolderExists(string.IsNullOrEmpty(path) ? DEFAULT_PATH : path); + if (realPath is null) + return false; + + FileActions.IsDriveViewVisible = false; + FileActions.IsExplorerVisible = true; + FileActions.HomeEnabled = true; + RuntimeSettings.BrowseDrive = null; + + UpdateFileOp(); + + Task.Delay(EXPLORER_NAV_DELAY).ContinueWith(_ => Dispatcher.Invoke(() => RuntimeSettings.IsExplorerLoaded = true)); + + return _navigateToPath(realPath); + } + + private bool _navigateToPath(string realPath) + { + //PasteGrid.Visibility = Visibility.Collapsed; + FileActions.ListingInProgress = true; + + FileActions.ExplorerFilter = ""; + NavHistory.Navigate(realPath); + + SelectionHelper.SetFirstSelectedIndex(ExplorerGrid, -1); + SelectionHelper.SetCurrentSelectedIndex(ExplorerGrid, -1); + + ExplorerGrid.Focus(); + CurrentPath = realPath; + + //NavigationBox.Path = realPath == RECYCLE_PATH ? AdbLocation.StringFromLocation(Navigation.SpecialLocation.RecycleBin) : realPath; + ParentPath = FileHelper.GetParentPath(CurrentPath); + CurrentDrive = DriveHelper.GetCurrentDrive(CurrentPath); + + FileActions.IsRecycleBin = CurrentPath == RECYCLE_PATH; + FileActions.IsAppDrive = CurrentPath == AdbLocation.StringFromLocation(Navigation.SpecialLocation.PackageDrive); + FileActions.IsTemp = CurrentPath == TEMP_PATH; + FileActions.ParentEnabled = CurrentPath != ParentPath && !FileActions.IsRecycleBin && !FileActions.IsAppDrive; + + FileActionLogic.IsPasteEnabled(); + + if (!RuntimeSettings.IsRootActive && DevicesObject.Current.Root is AbstractDevice.RootStatus.Enabled) + RuntimeSettings.IsRootActive = true; + + FileActions.PushPackageEnabled = Settings.EnableApk && DevicesObject?.Current?.Type is not AbstractDevice.DeviceType.Recovery; + FileActions.UninstallPackageEnabled = false; + + FileActions.ContextPushPackagesEnabled = + FileActions.IsUninstallVisible.Value = FileActions.IsAppDrive; + + FileActions.PushFilesFoldersEnabled = + FileActions.ContextNewEnabled = + FileActions.ContextPushEnabled = + FileActions.NewEnabled = !FileActions.IsRecycleBin && !FileActions.IsAppDrive; + + OriginalPath.Visibility = + OriginalDate.Visibility = Visible(FileActions.IsRecycleBin); + + PackageName.Visibility = + PackageType.Visibility = + PackageUid.Visibility = + PackageVersion.Visibility = Visible(FileActions.IsAppDrive); + + IconColumn.Visibility = + NameColumn.Visibility = + DateColumn.Visibility = + TypeColumn.Visibility = + SizeColumn.Visibility = Visible(!FileActions.IsAppDrive); + + FileActions.CopyPathDescription.Value = FileActions.IsAppDrive ? Strings.Resources.S_COPY_APK_NAME : Strings.Resources.S_COPY_PATH; + + if (FileActions.IsRecycleBin) + { + TrashHelper.ParseIndexersAsync().ContinueWith(_ => DirList.Navigate(realPath)); + + FileActions.DeleteDescription.Value = Strings.Resources.S_EMPTY_TRASH; + FileActions.RestoreDescription.Value = Strings.Resources.S_RESTORE_ALL; + } + else + { + if (FileActions.IsAppDrive) + { + FileActionLogic.UpdatePackages(true); + FileActionLogic.UpdateFileActions(); + return true; + } + + DirList.Navigate(realPath); + + FileActions.DeleteDescription.Value = Strings.Resources.S_DELETE_ACTION; + } + + RuntimeSettings.ExplorerSource = DirList.FileList; + FileActionLogic.UpdateFileActions(); + return true; + } + + private void NavigateToLocation(AdbLocation location) + { + SelectionHelper.SetIsMenuOpen(ExplorerGrid.ContextMenu, false); + + if (location.Location is Navigation.SpecialLocation.DriveView) + { + FileActions.IsRecycleBin = false; + RuntimeSettings.IsPathBoxFocused = false; + FileActionLogic.RefreshDrives(); + DriveViewNav(); + + FileActionLogic.UpdateFileActions(); + } + else + { + if (!FileActions.IsExplorerVisible) + InitNavigation(location.DisplayName); + else + NavigateToPath(location.DisplayName); + } + } + + public bool NavigateToPath(string path) + { + if (path is null) + return false; + + //if (!bfNavigation) + // prevPath = path; + + var realPath = FolderHelper.FolderExists(path); + return realPath is not null && _navigateToPath(realPath); + } + + private void DriveViewNav() + { + FileActionLogic.ClearExplorer(false); + FileActions.IsDriveViewVisible = true; + UpdateFileOp(); + + //NavigationBox.Mode = NavigationBox.ViewMode.Breadcrumbs; + //NavigationBox.Path = AdbLocation.StringFromLocation(Navigation.SpecialLocation.DriveView); + NavHistory.Navigate(Navigation.SpecialLocation.DriveView); + + DriveList.ItemsSource = DevicesObject.Current.Drives; + CurrentDrive = null; + + if (DriveList.SelectedIndex > -1) + SelectionHelper.GetListViewItemContainer(DriveList).Focus(); + + //HomeSavedLocationsList.ItemsSource = NavigationBox.SavedItems; + //if (NavigationBox.SavedItems.Count == 0) + // Settings.HomeLocationsExpanded = false; + } + + private void UpdateFileOp(bool onlyProgress = true) + { + if (!onlyProgress) + Task.Run(FileActionLogic.UpdateFileOpControls); + + //if (FileOpQ.AnyFailedOperations) + // TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Error; + //else if (FileOpQ.IsActive) + //{ + // if (FileOpQ.Progress == 0) + // TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate; + // else + // TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal; + //} + //else + // TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; + } + + private void DataGridCell_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) + { + } + + private void DataGridCell_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + } + + private void DataGridCell_MouseUp(object sender, MouseButtonEventArgs e) + { + } + + private void DataGridRow_MouseDown(object sender, MouseButtonEventArgs e) + { + } + + private void DataGridRow_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + } + + private void DataGridRow_KeyDown(object sender, KeyEventArgs e) + { + } + + private void DataGridRow_Drop(object sender, DragEventArgs e) + { + } + + private void ExplorerGrid_DragOver(object sender, DragEventArgs e) + { + } + + private void ExplorerGrid_ContextMenuClosing(object sender, ContextMenuEventArgs e) + { + } + + private void ExplorerGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e) + { + } + + private void ExplorerGrid_MouseDown(object sender, MouseButtonEventArgs e) + { + } + + private void ExplorerGrid_MouseMove(object sender, MouseEventArgs e) + { + } + + private void ExplorerGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + } + + private void ExplorerGrid_Sorting(object sender, DataGridSortingEventArgs e) + { + } + + private void NameColumnEdit_KeyDown(object sender, KeyEventArgs e) + { + } + + private void NameColumnEdit_Loaded(object sender, RoutedEventArgs e) + { + } + + private void NameColumnEdit_LostFocus(object sender, RoutedEventArgs e) + { + } + + private void NameColumnEdit_TextChanged(object sender, TextChangedEventArgs e) + { + } +} diff --git a/ADB Explorer _WpfUi/Controls/Icons/InstallIcon.xaml b/ADB Explorer _WpfUi/Controls/Icons/InstallIcon.xaml new file mode 100644 index 00000000..98a554c4 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/Icons/InstallIcon.xaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/Icons/ResetSettingsIcon.xaml.cs b/ADB Explorer _WpfUi/Controls/Icons/ResetSettingsIcon.xaml.cs new file mode 100644 index 00000000..8c27c98b --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/Icons/ResetSettingsIcon.xaml.cs @@ -0,0 +1,12 @@ +namespace ADB_Explorer.Controls; + +/// +/// Interaction logic for ResetSettingsIcon.xaml +/// +public partial class ResetSettingsIcon : UserControl +{ + public ResetSettingsIcon() + { + InitializeComponent(); + } +} diff --git a/ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml b/ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml new file mode 100644 index 00000000..c369ed6c --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml.cs b/ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml.cs new file mode 100644 index 00000000..b646744c --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/Icons/SettingsIcon.xaml.cs @@ -0,0 +1,12 @@ +namespace ADB_Explorer.Controls; + +/// +/// Interaction logic for SettingsIcon.xaml +/// +public partial class SettingsIcon : UserControl +{ + public SettingsIcon() + { + InitializeComponent(); + } +} diff --git a/ADB Explorer _WpfUi/Controls/Icons/UninstallIcon.xaml b/ADB Explorer _WpfUi/Controls/Icons/UninstallIcon.xaml new file mode 100644 index 00000000..6f5d8da7 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/Icons/UninstallIcon.xaml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/MaskedTextBox.xaml.cs b/ADB Explorer _WpfUi/Controls/MaskedTextBox.xaml.cs new file mode 100644 index 00000000..49e855e4 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/MaskedTextBox.xaml.cs @@ -0,0 +1,151 @@ +using ADB_Explorer.Helpers; +using static ADB_Explorer.Helpers.TextHelper; + +namespace ADB_Explorer.Controls; + +/// +/// Interaction logic for MaskedTextBox.xaml +/// +public partial class MaskedTextBox : UserControl +{ + public MaskedTextBox() + { + InitializeComponent(); + } + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register("Text", typeof(string), + typeof(MaskedTextBox), new PropertyMetadata("")); + + public ValidationType ValidationType + { + get => (ValidationType)GetValue(ValidationTypeProperty); + set => SetValue(ValidationTypeProperty, value); + } + + public static readonly DependencyProperty ValidationTypeProperty = + DependencyProperty.Register("ValidationType", typeof(ValidationType), + typeof(MaskedTextBox), new PropertyMetadata(default(ValidationType))); + + public char Separator + { + get => (char)GetValue(SeparatorProperty); + set => SetValue(SeparatorProperty, value); + } + + public static readonly DependencyProperty SeparatorProperty = + DependencyProperty.Register("Separator", typeof(char), + typeof(MaskedTextBox), new PropertyMetadata(default(char))); + + public int MaxChars + { + get => (int)GetValue(MaxCharsProperty); + set => SetValue(MaxCharsProperty, value); + } + + public static readonly DependencyProperty MaxCharsProperty = + DependencyProperty.Register("MaxChars", typeof(int), + typeof(MaskedTextBox), new PropertyMetadata(default(int))); + + public bool IsNumeric + { + get => (bool)GetValue(IsNumericProperty); + set => SetValue(IsNumericProperty, value); + } + + public static readonly DependencyProperty IsNumericProperty = + DependencyProperty.Register("IsNumeric", typeof(bool), + typeof(MaskedTextBox), new PropertyMetadata(default(bool))); + + public ulong MaxNumber + { + get => (ulong)GetValue(MaxNumberProperty); + set => SetValue(MaxNumberProperty, value); + } + + public static readonly DependencyProperty MaxNumberProperty = + DependencyProperty.Register("MaxNumber", typeof(ulong), + typeof(MaskedTextBox), new PropertyMetadata(default(ulong))); + + public int MaxSeparators + { + get => (int)GetValue(MaxSeparatorsProperty); + set => SetValue(MaxSeparatorsProperty, value); + } + + public static readonly DependencyProperty MaxSeparatorsProperty = + DependencyProperty.Register("MaxSeparators", typeof(int), + typeof(MaskedTextBox), new PropertyMetadata(default(int))); + + public char[] SpecialChars + { + get => (char[])GetValue(SpecialCharsProperty); + set => SetValue(SpecialCharsProperty, value); + } + + public static readonly DependencyProperty SpecialCharsProperty = + DependencyProperty.Register("SpecialChars", typeof(char[]), + typeof(MaskedTextBox), new PropertyMetadata(default(char[]))); + + public Style ControlStyle + { + get => (Style)GetValue(ControlStyleProperty); + set => SetValue(ControlStyleProperty, value); + } + + public static readonly DependencyProperty ControlStyleProperty = + DependencyProperty.Register("ControlStyle", typeof(Style), + typeof(MaskedTextBox), new PropertyMetadata(default(Style))); + + public ICommand EnterCommand + { + get => (ICommand)GetValue(EnterCommandProperty); + set => SetValue(EnterCommandProperty, value); + } + + public static readonly DependencyProperty EnterCommandProperty = + DependencyProperty.Register("EnterCommand", typeof(ICommand), + typeof(MaskedTextBox), new PropertyMetadata(default(ICommand))); + + private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + { + Wpf.Ui.Controls.TextBox textbox = sender as Wpf.Ui.Controls.TextBox; + if (!textbox.IsFocused) + return; + + switch (ValidationType) + { + case ValidationType.SeparateDigits: + TextHelper.SeparateDigits(textbox, Separator); + break; + case ValidationType.SeparateAndLimitDigits: + TextHelper.SeparateAndLimitDigits(textbox, Separator, MaxChars); + break; + case ValidationType.LimitDigits: + TextHelper.LimitDigits(textbox, MaxChars); + break; + case ValidationType.LimitNumber: + TextHelper.LimitNumber(textbox, MaxNumber); + break; + case ValidationType.LimitDigitsAndNumber: + TextHelper.LimitDigitsAndNumber(textbox, MaxChars, MaxNumber); + break; + case ValidationType.FilterString: + TextHelper.FilterString(textbox, SpecialChars); + break; + case ValidationType.SeparateFormat: + TextHelper.SeparateFormat(textbox, Separator, MaxNumber, MaxSeparators); + break; + default: // including None + break; + } + + Text = textbox.Text; + } +} diff --git a/ADB Explorer _WpfUi/Controls/MasonryPanel.cs b/ADB Explorer _WpfUi/Controls/MasonryPanel.cs new file mode 100644 index 00000000..178dea3a --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/MasonryPanel.cs @@ -0,0 +1,86 @@ +namespace ADB_Explorer.Controls; + +public class MasonryPanel : Panel +{ + public int Columns + { + get => (int)GetValue(ColumnsProperty); + set => SetValue(ColumnsProperty, value); + } + + public static readonly DependencyProperty ColumnsProperty = + DependencyProperty.Register( + nameof(Columns), + typeof(int), + typeof(MasonryPanel), + new FrameworkPropertyMetadata(2, FrameworkPropertyMetadataOptions.AffectsMeasure)); + + protected override Size MeasureOverride(Size availableSize) + { + if (Children.Count == 0 || Columns <= 0) + return new Size(0, 0); + + double availableWidth = availableSize.Width; + + double columnWidth = availableWidth / Columns; + var columnHeights = new double[Columns]; + + foreach (UIElement child in Children) + { + // Children get finite width, unbounded height + child.Measure(new Size(columnWidth, double.PositiveInfinity)); + + int column = GetShortestColumn(columnHeights); + columnHeights[column] += child.DesiredSize.Height; + availableWidth = child.DesiredSize.Width; + } + + double desiredHeight = columnHeights.Max(); + + return new Size(availableWidth * Columns, desiredHeight); + } + + private static int GetShortestColumn(double[] heights) + { + int index = 0; + double min = heights[0]; + + for (int i = 1; i < heights.Length; i++) + { + if (heights[i] < min) + { + min = heights[i]; + index = i; + } + } + + return index; + } + + + protected override Size ArrangeOverride(Size finalSize) + { + if (Children.Count == 0 || Columns <= 0) + return finalSize; + + double columnWidth = finalSize.Width / Columns; + var columnHeights = new double[Columns]; + + foreach (UIElement child in Children) + { + int column = GetShortestColumn(columnHeights); + + double height = child.DesiredSize.Height; + + child.Arrange(new Rect( + x: column * columnWidth, + y: columnHeights[column], + width: columnWidth, + height: height)); + + columnHeights[column] += height; + } + + return finalSize; + } +} diff --git a/ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml b/ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml new file mode 100644 index 00000000..78f46f4c --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml @@ -0,0 +1,620 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml.cs b/ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml.cs new file mode 100644 index 00000000..8d6b4426 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/SettingsPageHeader.xaml.cs @@ -0,0 +1,41 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Models; +using ADB_Explorer.Services; + +namespace ADB_Explorer.Controls +{ + /// + /// Interaction logic for SettingsPageHeader.xaml + /// + public partial class SettingsPageHeader : UserControl + { + public SettingsPageHeader() + { + InitializeComponent(); + + Data.RuntimeSettings.PropertyChanged += RuntimeSettings_PropertyChanged; + Data.Settings.PropertyChanged += Settings_PropertyChanged; + } + + private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(AppSettings.EnableMdns): + AdbHelper.EnableMdns(); + break; + default: + break; + } + } + + private void RuntimeSettings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + default: + break; + } + } + } +} diff --git a/ADB Explorer _WpfUi/Controls/SimpleStackPanel.cs b/ADB Explorer _WpfUi/Controls/SimpleStackPanel.cs new file mode 100644 index 00000000..6953ce11 --- /dev/null +++ b/ADB Explorer _WpfUi/Controls/SimpleStackPanel.cs @@ -0,0 +1,188 @@ +// Copied (and possibly modified) from ModernWPF under MIT License + +namespace ADB_Explorer.Controls; + +/// +/// Arranges child elements into a single line that can be oriented horizontally +/// or vertically. +/// +public class SimpleStackPanel : Panel +{ + /// + /// Initializes a new instance of the SimpleStackPanel class. + /// + public SimpleStackPanel() + { + } + + /// + /// Gets or sets a value that indicates the dimension by which child elements are + /// stacked. + /// + /// The Orientation of child content. + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Identifies the Orientation dependency property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(SimpleStackPanel), + new FrameworkPropertyMetadata( + Orientation.Vertical, + FrameworkPropertyMetadataOptions.AffectsMeasure)); + + /// + /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied + /// in the direction of the SimpleStackPanel's Orientation. + /// + /// The uniform distance (in pixels) between stacked items. + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + /// + /// Identifies the Spacing dependency property. + /// + public static readonly DependencyProperty SpacingProperty = + DependencyProperty.Register( + nameof(Spacing), + typeof(double), + typeof(SimpleStackPanel), + new FrameworkPropertyMetadata( + 0.0, + FrameworkPropertyMetadataOptions.AffectsMeasure)); + + /// + /// Gets a value that indicates if this SimpleStackPanel has vertical + /// or horizontal orientation. + /// + /// This property always returns true. + protected override bool HasLogicalOrientation => true; + + /// + /// Gets a value that represents the Orientation of the SimpleStackPanel. + /// + /// An Orientation value. + protected override Orientation LogicalOrientation => Orientation; + + /// + /// Measures the child elements of a SimpleStackPanel in anticipation + /// of arranging them during the SimpleStackPanel.ArrangeOverride(System.Windows.Size) + /// pass. + /// + /// An upper limit System.Windows.Size that should not be exceeded. + /// The System.Windows.Size that represents the desired size of the element. + protected override Size MeasureOverride(Size constraint) + { + Size stackDesiredSize = new(); + UIElementCollection children = InternalChildren; + Size layoutSlotSize = constraint; + bool fHorizontal = Orientation == Orientation.Horizontal; + double spacing = Spacing; + bool hasVisibleChild = false; + + if (fHorizontal) + { + layoutSlotSize.Width = double.PositiveInfinity; + } + else + { + layoutSlotSize.Height = double.PositiveInfinity; + } + + for (int i = 0, count = children.Count; i < count; ++i) + { + UIElement child = children[i]; + + if (child == null) { continue; } + + bool isVisible = child.Visibility != Visibility.Collapsed; + + if (isVisible && !hasVisibleChild) + { + hasVisibleChild = true; + } + + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; + + if (fHorizontal) + { + stackDesiredSize.Width += (isVisible ? spacing : 0) + childDesiredSize.Width; + stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height); + } + else + { + stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width); + stackDesiredSize.Height += (isVisible ? spacing : 0) + childDesiredSize.Height; + } + } + + if (fHorizontal) + { + stackDesiredSize.Width -= hasVisibleChild ? spacing : 0; + } + else + { + stackDesiredSize.Height -= hasVisibleChild ? spacing : 0; + } + + return stackDesiredSize; + } + + /// + /// Arranges the content of a SimpleStackPanel element. + /// + /// The System.Windows.Size that this element should use to arrange its child elements. + /// + /// The System.Windows.Size that represents the arranged size of this SimpleStackPanel + /// element and its child elements. + /// + protected override Size ArrangeOverride(Size arrangeSize) + { + UIElementCollection children = InternalChildren; + bool fHorizontal = Orientation == Orientation.Horizontal; + Rect rcChild = new(arrangeSize); + double previousChildSize = 0.0; + double spacing = Spacing; + + for (int i = 0, count = children.Count; i < count; ++i) + { + UIElement child = children[i]; + + if (child == null) { continue; } + + if (fHorizontal) + { + rcChild.X += previousChildSize; + previousChildSize = child.DesiredSize.Width; + rcChild.Width = previousChildSize; + rcChild.Height = Math.Max(arrangeSize.Height, child.DesiredSize.Height); + } + else + { + rcChild.Y += previousChildSize; + previousChildSize = child.DesiredSize.Height; + rcChild.Height = previousChildSize; + rcChild.Width = Math.Max(arrangeSize.Width, child.DesiredSize.Width); + } + + if (child.Visibility != Visibility.Collapsed) + { + previousChildSize += spacing; + } + + child.Arrange(rcChild); + } + return arrangeSize; + } +} diff --git a/ADB Explorer _WpfUi/Converters/CellConverter.cs b/ADB Explorer _WpfUi/Converters/CellConverter.cs new file mode 100644 index 00000000..c6f60243 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/CellConverter.cs @@ -0,0 +1,13 @@ +namespace ADB_Explorer.Converters; + +public class CellConverter +{ + public static DataGridCell GetDataGridCell(DataGridCellInfo cellInfo) + { + var cellContent = cellInfo.Column?.GetCellContent(cellInfo.Item); + if (cellContent != null) + return (DataGridCell)cellContent.Parent; + + return null; + } +} diff --git a/ADB Explorer _WpfUi/Converters/CompletedStatsConverter.cs b/ADB Explorer _WpfUi/Converters/CompletedStatsConverter.cs new file mode 100644 index 00000000..d56f9a96 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/CompletedStatsConverter.cs @@ -0,0 +1,27 @@ +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Converters; + +internal class CompletedStatsConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not CompletedSyncProgressViewModel info || string.IsNullOrEmpty(info.TotalSize)) + return ""; + + string format = Strings.Resources.S_FILE_PROGRESS_TRANSFER; + + if (string.IsNullOrEmpty(info.TotalTime)) + return info.TotalSize; + + if (string.IsNullOrEmpty(info.AverageRateString)) + return string.Format(format[..(format.IndexOf("{1}") + 3)].Trim(), info.TotalSize, info.TotalTime); + + return string.Format(format, info.TotalSize, info.TotalTime, info.AverageRateString); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/ADB Explorer _WpfUi/Converters/ControlSize.cs b/ADB Explorer _WpfUi/Converters/ControlSize.cs new file mode 100644 index 00000000..deac6934 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/ControlSize.cs @@ -0,0 +1,22 @@ +namespace ADB_Explorer.Converters; + +public static class ControlSize +{ + /// + /// Returns the length of a path button. + /// + /// + /// + public static double GetWidth(UIElement item) + { + item.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + return item.DesiredSize.Width; + } + + public static double GetWidth(DataTemplate template, object dataContext) + { + var content = (FrameworkElement)template.LoadContent(); + content.DataContext = dataContext; + return GetWidth(content); + } +} diff --git a/ADB Explorer _WpfUi/Converters/DoubleEquals.cs b/ADB Explorer _WpfUi/Converters/DoubleEquals.cs new file mode 100644 index 00000000..f9e6b56a --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/DoubleEquals.cs @@ -0,0 +1,24 @@ +namespace ADB_Explorer.Converters; + +public class DoubleEquals : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is string paramString) + { + if (double.TryParse(paramString, out double result)) + { + var val = double.Parse($"{value}", CultureInfo.InvariantCulture); + + return val == result; + } + } + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } +} diff --git a/ADB Explorer _WpfUi/Converters/EnumToBooleanConverter.cs b/ADB Explorer _WpfUi/Converters/EnumToBooleanConverter.cs new file mode 100644 index 00000000..94853d83 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,31 @@ +namespace ADB_Explorer.Converters; + +public class EnumToBooleanConverter : IValueConverter +{ + public Type EnumType { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is string enumString) + { + if (Enum.IsDefined(EnumType, value)) + { + var enumValue = Enum.Parse(EnumType, enumString); + + return enumValue.Equals(value); + } + } + + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is string enumString) + { + return Enum.Parse(EnumType, enumString); + } + + return null; + } +} diff --git a/ADB Explorer _WpfUi/Converters/FileOpProgressConverter.cs b/ADB Explorer _WpfUi/Converters/FileOpProgressConverter.cs new file mode 100644 index 00000000..d86622e9 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/FileOpProgressConverter.cs @@ -0,0 +1,22 @@ +namespace ADB_Explorer.Converters; + +public class FileOpProgressConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value switch + { + double val when val + is double.NaN + or double.PositiveInfinity + or double.NegativeInfinity => 0.0, + int or long or double => value, + _ => 0.0 + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } +} diff --git a/ADB Explorer _WpfUi/Converters/FileOpTreeStatusConverter.cs b/ADB Explorer _WpfUi/Converters/FileOpTreeStatusConverter.cs new file mode 100644 index 00000000..90f9dc54 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/FileOpTreeStatusConverter.cs @@ -0,0 +1,90 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using static ADB_Explorer.Converters.FileOpStatusConverter; + +namespace ADB_Explorer.Converters; + +internal class FileOpTreeStatusConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + string result = ""; + if (value is ObservableList children) + { + var (failed, type) = CountFails(children); + + result = StatusString(type, children.Count - failed, failed); + } + else if (value is ObservableList updates) + { + result = StatusString(updates.First().GetType(), message: updates.OfType().LastOrDefault()?.Message); + } + + if (parameter is "Completed") + { + return result == Strings.Resources.S_FILEOP_COMPLETED; + } + else if (parameter is "Validated") + { + return result == Strings.Resources.S_FILEOP_VALIDATED; + } + else + return result; + + throw new NotSupportedException(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +public static class FileOpStatusConverter +{ + public static string StatusString(Type type, int completed = 0, int failed = -1, string message = "", bool total = false) + { + if (!string.IsNullOrEmpty(message)) + { + return message.StartsWith(Strings.Resources.S_REDIRECTION) + ? message + : string.Format(Strings.Resources.S_FILEOP_ERROR, message); + } + + var completedString = (type == typeof(HashFailInfo) || type == typeof(HashSuccessInfo)) + ? Strings.Resources.S_FILEOP_VALIDATED + : Strings.Resources.S_FILEOP_COMPLETED; + + if (failed == -1) + return completedString; + + if (type == typeof(ShellErrorInfo)) + return $"({string.Format(Strings.Resources.S_FILEOP_SUBITEM_FAILED, failed)})"; + + var failedString = failed > 0 ? $"{string.Format(Strings.Resources.S_FILEOP_SUBITEM_FAILED, failed)}, " : ""; + + return $"({(total ? $"{Strings.Resources.S_FILEOP_TOTAL} " : "")}{failedString}{completed} {completedString})"; + } + + public static (int, Type) CountFails(ObservableList children) + { + int total = 0; + + foreach (var item in children.Where(c => c.Children.Count > 0)) + { + if (CountFails(item.Children).Item1 > 0) + total++; + } + + total += children.Count(c => c.Children.Count == 0 && c.ProgressUpdates.OfType().Any()); + + var type = typeof(SyncErrorInfo); + if (children.Any(c => c.ProgressUpdates.Any(u => u is ShellErrorInfo))) + type = typeof(ShellErrorInfo); + else if (children.Any(c => c.ProgressUpdates.Any(u => u is HashFailInfo or HashSuccessInfo))) + type = typeof(HashFailInfo); + + return (total, type); + } +} \ No newline at end of file diff --git a/ADB Explorer _WpfUi/Converters/MarginConverter.cs b/ADB Explorer _WpfUi/Converters/MarginConverter.cs new file mode 100644 index 00000000..4ac69c71 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/MarginConverter.cs @@ -0,0 +1,45 @@ +namespace ADB_Explorer.Converters; + +public class MarginConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + // arg 0 - ExpansionProgress + // arg 1 - ExpandDirection + // arg 2 - ContentHeight + // arg 3 - ContentWidth + + if (values.Length < 4) + throw new ArgumentException("This function requires 4 arguments."); + else if (values[0] is not double || values[1] is not ExpandDirection || values[2] is not double || values[3] is not double) + throw new ArgumentException("Provided arguments are not of correct format."); + + var expansionProgress = (double)values[0]; + var expandDirection = (ExpandDirection)values[1]; + var contentHeight = (double)values[2]; + var contentWidth = (double)values[3]; + + var size = expandDirection switch + { + ExpandDirection.Down or ExpandDirection.Up => contentHeight, + ExpandDirection.Left or ExpandDirection.Right => contentWidth, + _ => throw new NotSupportedException(), + }; + + var result = -size * (1 - expansionProgress); + + return expandDirection switch + { + ExpandDirection.Down => new Thickness(0, result, 0, 0), + ExpandDirection.Up => new Thickness(0, 0, 0, result), + ExpandDirection.Left => new Thickness(0, 0, result, 0), + ExpandDirection.Right => new Thickness(result, 0, 0, 0), + _ => throw new NotSupportedException(), + }; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/ADB Explorer _WpfUi/Converters/MenuItemConverter.cs b/ADB Explorer _WpfUi/Converters/MenuItemConverter.cs new file mode 100644 index 00000000..7d50f697 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/MenuItemConverter.cs @@ -0,0 +1,27 @@ +namespace ADB_Explorer.Converters; + +class MenuItemConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + DependencyObject parent = value as UIElement; + while (parent is not null and not Menu) + parent = VisualTreeHelper.GetParent(parent); + + if (parent is null) + return null; + + return parameter switch + { + "Padding" => Helpers.MenuHelper.GetItemPadding(parent as UIElement), + "Margin" => Helpers.MenuHelper.GetItemMargin(parent as UIElement), + "Style" => Helpers.MenuHelper.GetIsButtonMenu(parent as UIElement), + _ => throw new NotSupportedException(), + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/ADB Explorer _WpfUi/Converters/SizeConverter.cs b/ADB Explorer _WpfUi/Converters/SizeConverter.cs new file mode 100644 index 00000000..588916d2 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/SizeConverter.cs @@ -0,0 +1,108 @@ +namespace ADB_Explorer.Converters +{ + public static class UnitConverter + { + private static readonly Dictionary byteScaleTable = new() + { + { 0, Strings.Resources.BYTES }, + { 1, Strings.Resources.KILO }, + { 2, Strings.Resources.MEGA }, + { 3, Strings.Resources.GIGA }, + { 4, Strings.Resources.TERA }, + { 5, Strings.Resources.PETA }, + { 6, Strings.Resources.EXA } + }; + + /// + /// Converts a byte value to a human-readable size string with appropriate units. + /// + /// The method uses a logarithmic scale to determine the appropriate unit (e.g., KB, MB, + /// GB) based on the input size. The result is formatted with the specified rounding rules and optional spacing + /// between the value and the unit. + /// The size in bytes to be converted. Must be a non-negative value. + /// A boolean value indicating whether to include a space between the numeric value and the unit. to include a space; otherwise, . + /// The number of decimal places to round to when the numeric value is less than 100. + /// The number of decimal places to round to when the numeric value is 100 or greater. + /// A string representing the size in a human-readable format, including the appropriate unit (e.g., KB, MB, + /// GB). + public static string BytesToSize(this long bytes, bool scaleSpace = false, int bigRound = 1, int smallRound = 0) + { + if (bytes < 0) + return ""; + + int scale = (bytes == 0) ? 0 : Convert.ToInt32(Math.Floor(Math.Round(Math.Log(bytes, 1024), 2))); // 0 <= scale <= 6 + double value = bytes / Math.Pow(1024, scale); + var format = scaleSpace + ? byteScaleTable[scale] + : byteScaleTable[scale].Replace(" ", ""); + + return string.Format(format, Math.Round(value, value < 100 ? bigRound : smallRound)); + } + + private static readonly Dictionary ampScaleTable = new() + { + { -3, Strings.Resources.NANO }, + { -2, Strings.Resources.MICRO }, + { -1, Strings.Resources.MILLI }, + { 0, "" }, + }; + + public static string AmpsToSize(this double source, bool scaleSpace = false, int bigRound = 1, int smallRound = 0) + { + int scale = (source == 0) ? 0 : Convert.ToInt32(Math.Floor(Math.Round(Math.Log(Math.Abs(source), 1000), 2))); + double value = source / Math.Pow(1000, scale); + + // Currently, this is designed to handle values of nano Ampere up to Ampere. + // If your device draws more than that and requires a dedicated GFCI - you probably have bigger problems. + return $"{(Math.Round(value, value < 100 ? bigRound : smallRound))}{(scaleSpace ? " " : "")}{ampScaleTable[scale]}"; + } + + public static string ToTime(this double? seconds, bool scaleSpace = false, bool useMilli = true, int digits = 2) + { + if (seconds is null) + return ""; + + TimeSpan span = TimeSpan.FromSeconds(seconds.Value); + string resolution; + string value; + + if (span.Days == 0) + { + if (span.Hours == 0) + { + if (span.Minutes == 0) + { + if (useMilli && span.Seconds == 0) + { + value = $"{Math.Round(span.TotalMilliseconds, span.TotalMilliseconds < 100 ? digits : 1)}"; + resolution = Strings.Resources.S_MILLISECONDS_SHORT; + } + else + { + value = $"{Math.Round(span.TotalSeconds, digits)}"; + resolution = Strings.Resources.S_SECONDS_SHORT; + } + } + else + { + value = $"{span.Minutes}:{span.Seconds:00}"; + resolution = Strings.Resources.S_MINUTES_SHORT; + } + } + else + { + value = $"{span.Hours}:{span.Minutes:00}:{span.Seconds:00}"; + resolution = Strings.Resources.S_HOURS_SHORT; + } + } + else + { + value = $"{Math.Round(span.TotalHours)}"; + resolution = Strings.Resources.S_HOURS_SHORT; + } + + return string.Format(resolution, $"{value}{(scaleSpace ? " " : "")}"); + } + } +} diff --git a/ADB Explorer _WpfUi/Converters/StringFormatConverter.cs b/ADB Explorer _WpfUi/Converters/StringFormatConverter.cs new file mode 100644 index 00000000..12991211 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/StringFormatConverter.cs @@ -0,0 +1,24 @@ +namespace ADB_Explorer.Converters; + +public class StringFormatConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not string format) + return string.Empty; + + try + { + return string.Format(format, values); + } + catch + { + return format; + } + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/ADB Explorer _WpfUi/Converters/TreeViewIndentConverter.cs b/ADB Explorer _WpfUi/Converters/TreeViewIndentConverter.cs new file mode 100644 index 00000000..fe9289eb --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/TreeViewIndentConverter.cs @@ -0,0 +1,17 @@ +namespace ADB_Explorer.Converters; + +internal class TreeViewIndentConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Thickness margin) + return new Thickness(-1 * margin.Left, margin.Top, margin.Right, margin.Bottom); + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/ADB Explorer _WpfUi/Converters/TrimmedTooltipConverter.cs b/ADB Explorer _WpfUi/Converters/TrimmedTooltipConverter.cs new file mode 100644 index 00000000..1a0ded8a --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/TrimmedTooltipConverter.cs @@ -0,0 +1,37 @@ +namespace ADB_Explorer.Converters; + +public class TrimmedTooltipConverter : IValueConverter +{ + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is null) + return Visibility.Collapsed; + + var text = value switch + { + TextBlock textBlock => textBlock.Text, + TextBox textBox => textBox.Text, + _ => "", + }; + + return text.Length > 0 + ? Visibility.Visible + : Visibility.Collapsed; + + //// FrameworkElement to include both TextBlock and TextBox + //var textBlock = value as FrameworkElement; + + //textBlock.Measure(new(double.PositiveInfinity, double.PositiveInfinity)); + //var margin = textBlock.Margin.Left + textBlock.Margin.Right; + + //return textBlock.ActualWidth + margin < textBlock.DesiredSize.Width + // ? Visibility.Visible + // : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/ADB Explorer _WpfUi/Converters/UnixTimeConverter.cs b/ADB Explorer _WpfUi/Converters/UnixTimeConverter.cs new file mode 100644 index 00000000..5b2d0601 --- /dev/null +++ b/ADB Explorer _WpfUi/Converters/UnixTimeConverter.cs @@ -0,0 +1,28 @@ +namespace ADB_Explorer.Converters; + +public static class UnixTimeConverter +{ + /// + /// Converts a DateTime (UTC or local) to Unix time (seconds since 1970-01-01 UTC). + /// + public static double? ToUnixTime(this DateTime? dateTime) + { + if (dateTime is null) + return null; + + // Convert to UTC to ensure correct epoch alignment + return (dateTime.Value.ToUniversalTime() - DateTime.UnixEpoch).TotalSeconds; + } + + /// + /// Converts Unix time (seconds since 1970-01-01 UTC) to DateTime (local or UTC). + /// + public static DateTime? FromUnixTime(this double? unixTimeSeconds, bool asLocal = true) + { + if (unixTimeSeconds is null) + return null; + + var dt = DateTime.UnixEpoch.AddSeconds(unixTimeSeconds.Value); + return asLocal ? dt.ToLocalTime() : dt; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AdbHelper.cs b/ADB Explorer _WpfUi/Helpers/AdbHelper.cs new file mode 100644 index 00000000..6b2ed2fd --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AdbHelper.cs @@ -0,0 +1,93 @@ +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using AdvancedSharpAdbClient; + +namespace ADB_Explorer.Helpers; + +internal static class AdbHelper +{ + public static Task CheckAdbVersion() => Task.Run(() => + { + string adbPath = string.IsNullOrEmpty(Data.Settings.ManualAdbPath) + ? AdbExplorerConst.ADB_PROCESS + : $"\"{Data.Settings.ManualAdbPath}\""; + + ADBService.VerifyAdbVersion(adbPath); + + return Data.RuntimeSettings.AdbVersion >= AdbExplorerConst.MIN_ADB_VERSION; + }); + + private static void UpdateQrClass() => Data.RuntimeSettings.RefreshQrImage = true; + + public static void MdnsCheck() + { + Task.Run(() => Data.MdnsService.State = ADBService.CheckMDNS() ? MDNS.MdnsState.Running : MDNS.MdnsState.NotRunning); + Task.Run(async () => + { + while (Data.MdnsService.State is MDNS.MdnsState.InProgress) + { + App.Current.Dispatcher.Invoke(() => Data.MdnsService.UpdateProgress()); + + await Task.Delay(AdbExplorerConst.MDNS_STATUS_UPDATE_INTERVAL); + } + }); + } + + public static void EnableMdns() => App.Current.Dispatcher.Invoke(async () => + { + ADBService.IsMdnsEnabled = Data.Settings.EnableMdns; + if (Data.Settings.EnableMdns) + { + if (Data.MdnsService?.State is MDNS.MdnsState.Disabled) + { + Data.MdnsService.State = MDNS.MdnsState.InProgress; + + Data.MdnsService.QrClass = new(); + MdnsCheck(); + UpdateQrClass(); + } + } + else + { + if (Data.MdnsService?.State is MDNS.MdnsState.Running) + { + var result = await DialogService.ShowConfirmation(Strings.Resources.S_DISABLE_MDNS, + Strings.Resources.S_DISABLE_MDNS_TITLE, + Strings.Resources.S_RESTART_ADB_NOW, + cancelText: Strings.Resources.S_RESTART_LATER, + icon: DialogService.DialogIcon.Informational); + + if (result.Item1 is Wpf.Ui.Controls.ContentDialogResult.Primary) + ADBService.KillAdbServer(); + + Data.MdnsService.QrClass = null; + Data.MdnsService.State = MDNS.MdnsState.Disabled; + UpdateQrClass(); + } + } + }); + + public static string ReadFile(ADBService.AdbDevice device, string path) + { + using MemoryStream stream = new(); + using SyncService service = new(device.Device.DeviceData); + + service.Pull(path, stream); + + stream.Position = 0; + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static void WriteFile(ADBService.AdbDevice device, string path, string content) + { + using MemoryStream stream = new(); + using StreamWriter writer = new(stream); + writer.Write(content); + writer.Flush(); + stream.Position = 0; + + using SyncService service = new(device.Device.DeviceData); + service.Push(stream, path, (UnixFileMode)0x1ED, DateTime.Now); // 0x1ED = 0777 in octal + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/AsyncHelper.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/AsyncHelper.cs new file mode 100644 index 00000000..88c4e3e3 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/AsyncHelper.cs @@ -0,0 +1,14 @@ +namespace ADB_Explorer.Helpers; + +public static class AsyncHelper +{ + public static async Task WaitUntil(Func condition, TimeSpan timeout, TimeSpan assertDelay, CancellationToken cancellationToken) + { + var waitTask = Task.Run(async () => + { + while (!condition()) await Task.Delay(assertDelay, cancellationToken); + }, cancellationToken); + + await Task.WhenAny(waitTask, Task.Delay(timeout, cancellationToken)); + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/ByteHelper.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/ByteHelper.cs new file mode 100644 index 00000000..8e813f09 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/ByteHelper.cs @@ -0,0 +1,47 @@ +using System.Numerics; + +namespace ADB_Explorer.Helpers; + +public static class ByteHelper +{ + public static int PatternAt(Span source, ReadOnlySpan pattern, int startIndex = 0, bool evenAlign = false) + { + int length = source.Length; + int patLength = pattern.Length; + + if (patLength == 0) + return -1; + + // Preserve original empty-pattern behavior: + // return the first index (respecting evenAlign) in [startIndex, source.Length) + int limitExclusive = length - patLength + 1; + + for (int i = startIndex; i < limitExclusive; i++) + { + if (evenAlign && !int.IsEvenInteger(i)) + continue; + + int srcIndex = i < 0 ? 0 : i; + + // Avoid out-of-range slicing when pattern is longer than remaining source + if (srcIndex <= length - patLength && + source.Slice(srcIndex, patLength).SequenceEqual(pattern)) + { + return i; + } + } + + return -1; + } + + public static int Sum(this Span source) + { + int sum = 0; + foreach (byte value in source) + { + sum += value; + } + + return sum; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/CommandHandler.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/CommandHandler.cs new file mode 100644 index 00000000..16686d2b --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/CommandHandler.cs @@ -0,0 +1,64 @@ +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Helpers; + +public class CommandHandler : ICommand +{ + private readonly Action _action; + private readonly Func _canExecute; + + /// + /// Raises an event when the command is executed. + /// + public ObservableProperty OnExecute { get; set; } = new(); + + public void Execute(object parameter) + { + _action(); + + OnExecute.Value ^= true; + } + + public bool CanExecute(object parameter) + { + return _canExecute.Invoke(); + } + + public CommandHandler(Action action, Func canExecute) + { + _action = action; + _canExecute = canExecute; + } + + public event EventHandler CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + +} + +public class BaseAction : ViewModelBase +{ + private readonly Func canExecute; + public bool IsEnabled => canExecute(); + + private readonly Action action; + + private ICommand command; + public ICommand Command => command ??= new CommandHandler(action, canExecute); + + public BaseAction(Func canExecute, Action action) + { + this.canExecute = canExecute ??= () => true; + this.action = action; + } + + public BaseAction() + { + canExecute = () => true; + action = () => { }; + } + + public void Execute() => Command.Execute(null); +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/DictionaryHelper.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/DictionaryHelper.cs new file mode 100644 index 00000000..419ce70c --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/DictionaryHelper.cs @@ -0,0 +1,44 @@ +namespace ADB_Explorer.Helpers; + +public static class DictionaryHelper +{ + public static Dictionary TryToDictionary(this IEnumerable source, Func keySelector, Func elementSelector) where TKey : notnull => + TryToDictionary(source, keySelector, elementSelector, null); + + public static Dictionary TryToDictionary(this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer? comparer) where TKey : notnull + { + ArgumentNullException.ThrowIfNull(source); + + ArgumentNullException.ThrowIfNull(keySelector); + + ArgumentNullException.ThrowIfNull(elementSelector); + + int capacity = 0; + if (source is ICollection collection) + { + capacity = collection.Count; + if (capacity == 0) + { + return new Dictionary(comparer); + } + + if (collection is TSource[] array) + { + return TryToDictionary(array, keySelector, elementSelector, comparer); + } + + if (collection is List list) + { + return TryToDictionary(list, keySelector, elementSelector, comparer); + } + } + + Dictionary d = new Dictionary(capacity, comparer); + foreach (TSource element in source) + { + d.TryAdd(keySelector(element), elementSelector(element)); + } + + return d; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/ListHelper.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/ListHelper.cs new file mode 100644 index 00000000..ce0b8fc4 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/ListHelper.cs @@ -0,0 +1,46 @@ +namespace ADB_Explorer.Helpers; + +internal static class ListHelper +{ + public static ListSortDirection Invert(ListSortDirection? value) + { + return value is ListSortDirection and ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; + } + + /// + /// Appends an IEnumerable to the end of another IEnumerable.
+ /// *** ENUMERATES BOTH OF THE ENUMERABLES *** + ///
+ public static IEnumerable AppendRange(this IEnumerable self, IEnumerable other) + { + foreach (var item in self) + { + yield return item; + } + + foreach (var item in other) + { + yield return item; + } + } + + /// + /// Determines whether all elements of a sequence satisfy a condition + /// + /// if the source sequence contains any elements and every element passes the test in the specified predicate; otherwise, + public static bool AnyAll(this IEnumerable source, Func predicate) + { + return source.Any() && source.All(predicate); + } + + /// + /// Performs the specified on each element of the collection + /// + public static void ForEach(this IEnumerable self, Action action) + { + foreach (var item in self) + { + action(item); + } + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/ObservableList.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/ObservableList.cs new file mode 100644 index 00000000..33d71098 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/ObservableList.cs @@ -0,0 +1,144 @@ +using System; + +namespace ADB_Explorer.Helpers; + +public class ObservableList : ObservableCollection where T : INotifyPropertyChanged +{ + private bool suppressOnCollectionChanged = false; + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (!suppressOnCollectionChanged) + { + base.OnCollectionChanged(e); + } + } + + /// + /// Adds a collection of items to the end of the list. + /// + /// + public void AddRange(IEnumerable items) + { + var itemsList = items.ToArray(); + switch (itemsList.Length) + { + case < 1: + return; + case < 2: + // When adding one item, we can skip the notification suppression mechanism + Add(itemsList[0]); + return; + } + + // When adding more than one item, we suppress the notification mechanism while items are being added + suppressOnCollectionChanged = true; + + foreach (T item in itemsList) + { + Add(item); + } + + suppressOnCollectionChanged = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public void RemoveAll() + { + suppressOnCollectionChanged = true; + + while (Count > 0) + { + RemoveAt(0); + } + + suppressOnCollectionChanged = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public T Find(Func predicate) + { + if (Count == 0 || predicate is null) + return default; + + var resultList = this.Where(predicate).ToArray(); + return resultList.Length > 0 + ? resultList[0] + : default; + } + + /// + /// Removes all items that match the predicate. + /// + /// + /// if at least one item was removed, otherwise + public bool RemoveAll(Func predicate) + { + var resultList = this.Where(predicate).ToArray(); + switch (resultList.Length) + { + case < 1: + return false; + case 1: + // When removing one item, we can skip the notification suppression mechanism + Remove(resultList[0]); + return true; + } + + // When removing more than one item, we suppress the notification mechanism while items are being removed + suppressOnCollectionChanged = true; + + foreach (T item in resultList) + { + Remove(item); + } + + suppressOnCollectionChanged = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + + return true; + + } + + public void RemoveAll(IEnumerable items) + { + var resultList = items.ToArray(); + switch (resultList.Length) + { + case < 1: + return; + case 1: + // When removing one item, we can skip the notification suppression mechanism + Remove(resultList[0]); + return; + } + + // When removing more than one item, we suppress the notification mechanism while items are being removed + suppressOnCollectionChanged = true; + + foreach (var item in resultList) + { + Remove(item); + } + + suppressOnCollectionChanged = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public void ForEach(Action action) + { + suppressOnCollectionChanged = true; + + foreach (var item in this) + { + action(item); + } + + suppressOnCollectionChanged = false; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/ObservableProperty.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/ObservableProperty.cs new file mode 100644 index 00000000..0fcb3c9a --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/ObservableProperty.cs @@ -0,0 +1,34 @@ +namespace ADB_Explorer.Helpers; + +public class PropertyChangedEventArgs : EventArgs +{ + public T OldValue { get; set; } + public T NewValue { get; set; } +} + +public class ObservableProperty +{ + public event EventHandler> PropertyChanged; + + private T _value; + public T Value + { + get => _value; + set + { + if (Equals(_value, value)) + return; + + PropertyChangedEventArgs args = new() + { + OldValue = _value, + NewValue = value + }; + + _value = value; + PropertyChanged?.Invoke(this, args); + } + } + + public static implicit operator T(ObservableProperty p) => p.Value; +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/RandomString.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/RandomString.cs new file mode 100644 index 00000000..834f5b9c --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/RandomString.cs @@ -0,0 +1,28 @@ +using static ADB_Explorer.Models.AdbExplorerConst; + +namespace ADB_Explorer.Helpers; + +public class RandomString +{ + public static string GetUniqueKey(int size, char[] chars = null) + { + if (chars is null) + { + chars = WIFI_PAIRING_ALPHABET; + } + + byte[] data = new byte[4 * size]; + RandomNumberGenerator.Create().GetBytes(data); + + StringBuilder result = new(); + for (int i = 0; i < size; i++) + { + var rnd = BitConverter.ToUInt32(data, i * 4); + var idx = rnd % chars.Length; + + result.Append(chars[idx]); + } + + return result.ToString(); + } +} diff --git a/ADB Explorer _WpfUi/Helpers/AppInfra/SettingsHelper.cs b/ADB Explorer _WpfUi/Helpers/AppInfra/SettingsHelper.cs new file mode 100644 index 00000000..f40ce14a --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/AppInfra/SettingsHelper.cs @@ -0,0 +1,203 @@ +using ADB_Explorer.Models; +using ADB_Explorer.Resources; +using ADB_Explorer.Services; + +namespace ADB_Explorer.Helpers; + +public static class SettingsHelper +{ + public static void DisableAnimationTipAction() => + DialogService.ShowMessage(Strings.Resources.S_DISABLE_ANIMATION, Strings.Resources.S_ANIMATION_TITLE, DialogService.DialogIcon.Tip); + + public static void ResetAppAction() + { + Process.Start(Environment.ProcessPath); + Application.Current.Shutdown(); + } + + public static void ChangeDefaultPathAction() + { + var dialog = new CommonOpenFileDialog() + { + IsFolderPicker = true, + Multiselect = false + }; + if (Data.Settings.DefaultFolder != "") + dialog.DefaultDirectory = Data.Settings.DefaultFolder; + + if (dialog.ShowDialog() == CommonFileDialogResult.Ok) + { + Data.Settings.DefaultFolder = dialog.FileName; + } + } + + public static void ChangeAdbPathAction() + { + var dialog = new OpenFileDialog() + { + Multiselect = false, + Title = Strings.Resources.S_OVERRIDE_ADB_BROWSE, + Filter = $"{Strings.Resources.S_ADB_EXECUTABLE}|adb.exe", + }; + + if (!string.IsNullOrEmpty(Data.Settings.ManualAdbPath)) + { + try + { + var dir = Directory.GetParent(Data.Settings.ManualAdbPath); + + if (dir.Exists) + dialog.InitialDirectory = dir.FullName; + } + catch (Exception) { } + } + + if (dialog.ShowDialog() == true) + { + string message = ""; + ADBService.VerifyAdbVersion(dialog.FileName); + if (Data.RuntimeSettings.AdbVersion is null) + { + message = Strings.Resources.S_MISSING_ADB_OVERRIDE; + } + else if (Data.RuntimeSettings.AdbVersion < AdbExplorerConst.MIN_ADB_VERSION) + { + message = Strings.Resources.S_ADB_VERSION_LOW_OVERRIDE; + } + + if (message != "") + { + DialogService.ShowMessage(message, Strings.Resources.S_FAIL_OVERRIDE_TITLE, DialogService.DialogIcon.Exclamation, copyToClipboard: true); + return; + } + + Data.Settings.ManualAdbPath = dialog.FileName; + } + } + + public static void SetSymbolFont() + { + Application.Current.Resources["SymbolThemeFontFamily"] = App.Current.FindResource("FluentSymbolThemeFontFamily"); + } + + public static async void SplashScreenTask() + { + var startTime = DateTime.Now; + var versionValid = await AdbHelper.CheckAdbVersion(); + var delay = AdbExplorerConst.SPLASH_DISPLAY_TIME - (DateTime.Now - startTime); + + if (!versionValid) // || !Data.Settings.AdvancedDragSet + return; + + await Task.Delay(Data.Settings.EnableSplash && delay > TimeSpan.Zero ? delay : TimeSpan.Zero); + + Data.RuntimeSettings.FinalizeSplash = true; + } + + public static async void InitNotifications() + { + if (Data.Settings.OriginalCulture is null || Data.Settings.OriginalCulture.Name != "en-US") + { + UISettings.Notifications.Add(new(async () => + { + var res = await DialogService.ShowConfirmation(Strings.Resources.S_LANG_NOTIFICATION, + Strings.Resources.S_LANG_NOTIFICATION_TITLE, + Strings.Resources.S_GOTO_WEBLATE, + cancelText: Strings.Resources.S_BUTTON_CLOSE, + icon: DialogService.DialogIcon.Informational); + + if (res.Item1 is Wpf.Ui.Controls.ContentDialogResult.Primary) + Process.Start(Data.RuntimeSettings.DefaultBrowserPath, $"\"{Links.WEBLATE}\""); + + Data.Settings.ShowLanguageNotification = false; + }, Strings.Resources.S_LANG_NOTIFICATION_TITLE)); + } + + if (new Version(Properties.AppGlobal.AppVersion) > new Version(Data.Settings.LastVersion)) + { + UISettings.Notifications.Add(new(async () => + { + var res = await DialogService.ShowConfirmation( + Strings.Resources.S_NEW_VERSION_MSG, + Strings.Resources.S_NEW_VERSION_TITLE, + Strings.Resources.S_GO_TO_RELEASE_NOTES, + cancelText: Strings.Resources.S_BUTTON_CLOSE); + + if (res.Item1 is Wpf.Ui.Controls.ContentDialogResult.Primary) + Process.Start(Data.RuntimeSettings.DefaultBrowserPath, $"\"https://github.com/Alex4SSB/ADB-Explorer/releases/tag/v{Properties.AppGlobal.AppVersion}\""); + + Data.Settings.LastVersion = Properties.AppGlobal.AppVersion; + }, Strings.Resources.S_NEW_VERSION_TITLE)); + } + + if (!Data.RuntimeSettings.IsAppDeployed && Data.Settings.CheckForUpdates) + { + var latestVersion = await Network.LatestAppReleaseAsync(); + if (latestVersion is null || latestVersion <= Data.AppVersion) + return; + + UISettings.Notifications.Add(new(async () => + { + var res = await DialogService.ShowConfirmation(string.Format(Strings.Resources.S_NEW_VERSION, Properties.AppGlobal.AppDisplayName, latestVersion), + Strings.Resources.S_NEW_VERSION_TITLE, + Strings.Resources.S_GO_TO_VERSION_PAGE, + cancelText: Strings.Resources.S_BUTTON_CLOSE, + icon: DialogService.DialogIcon.Informational); + + if (res.Item1 is Wpf.Ui.Controls.ContentDialogResult.Primary) + Process.Start(Data.RuntimeSettings.DefaultBrowserPath, $"\"https://github.com/Alex4SSB/ADB-Explorer/releases/tag/v{latestVersion}\""); + }, Strings.Resources.S_NEW_VERSION_TITLE)); + } + } + + public static IEnumerable GetAvailableLanguages() + { + string assemblyName = Assembly.GetEntryAssembly()?.GetName().Name; + yield return CultureInfo.InvariantCulture; + yield return new CultureInfo("en-US"); + + foreach (var dir in Directory.GetDirectories(AppDomain.CurrentDomain.BaseDirectory)) + { + string folderName = Path.GetFileName(dir); + CultureInfo culture = null; + string resourceAssembly = ""; + + try + { + // Attempt to create a CultureInfo from folder name + culture = new(folderName); + + // Check if satellite assembly exists for this culture + resourceAssembly = Path.Combine(dir, $"{assemblyName}.resources.dll"); + + } + catch (CultureNotFoundException) + { + // Folder name is not a valid culture + continue; + } + + if (File.Exists(resourceAssembly)) + { + yield return culture; + } + } + } + + public static double GetCurrentPercentageTranslated(CultureInfo currentCulture) + { + var neutralCulture = CultureInfo.InvariantCulture; + + var resourceManager = Strings.Resources.ResourceManager; + var resourceType = typeof(Strings.Resources); + var propertyInfos = resourceType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + var stringProps = propertyInfos.Where(p => p.PropertyType == typeof(string)); + var neutralValues = stringProps.Select(p => resourceManager.GetString(p.Name, neutralCulture)).Where(s => !s.All(c => char.IsAsciiLetterUpper(c))); + var currentValues = stringProps.Select(p => resourceManager.GetString(p.Name, currentCulture)); + double translated = neutralValues.Except(currentValues).Count(); + double total = neutralValues.Count(); + + return translated / total; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/Attachable/ContextMenuHelper.cs b/ADB Explorer _WpfUi/Helpers/Attachable/ContextMenuHelper.cs new file mode 100644 index 00000000..a69d95e8 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/Attachable/ContextMenuHelper.cs @@ -0,0 +1,77 @@ +namespace ADB_Explorer.Helpers; + +public static class ContextMenuHelper +{ + public static readonly DependencyProperty EnableAutoCloseProperty = + DependencyProperty.RegisterAttached( + "EnableAutoClose", + typeof(bool), + typeof(ContextMenuHelper), + new PropertyMetadata(false, OnEnableAutoCloseChanged)); + + public static void SetEnableAutoClose(DependencyObject element, bool value) + => element.SetValue(EnableAutoCloseProperty, value); + + public static bool GetEnableAutoClose(DependencyObject element) + => (bool)element.GetValue(EnableAutoCloseProperty); + + private static void OnEnableAutoCloseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is FrameworkElement element) + { + if ((bool)e.NewValue) + element.ContextMenuOpening += OnContextMenuOpening; + else + element.ContextMenuOpening -= OnContextMenuOpening; + } + } + + private static void OnContextMenuOpening(object sender, ContextMenuEventArgs e) + { + if (sender is FrameworkElement element && element.ContextMenu is ContextMenu menu) + { + // Determine the target for CanExecute (focused element or owner) + IInputElement target = menu.PlacementTarget ?? element; + + bool anyVisibleAndEnabled = false; + + foreach (var item in menu.Items.OfType()) + { + if (item.Command is not null) + { + var command = item.Command; + var parameter = item.CommandParameter; + + if (command is RoutedCommand routed) + { + var commandTarget = item.CommandTarget ?? target; + if (routed.CanExecute(parameter, commandTarget)) + { + anyVisibleAndEnabled = true; + break; + } + } + else + { + if (command.CanExecute(parameter)) + { + anyVisibleAndEnabled = true; + break; + } + } + } + else if (item.IsEnabled) + { + anyVisibleAndEnabled = true; + break; + } + } + + + if (!anyVisibleAndEnabled) + { + e.Handled = true; + } + } + } +} diff --git a/ADB Explorer _WpfUi/Helpers/Attachable/ExpanderHelper.cs b/ADB Explorer _WpfUi/Helpers/Attachable/ExpanderHelper.cs new file mode 100644 index 00000000..5e726d50 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/Attachable/ExpanderHelper.cs @@ -0,0 +1,167 @@ +namespace ADB_Explorer.Helpers; + +public static class ExpanderHelper +{ + public enum ExpandArrow + { + None, + CW, + CCW + } + + public static ExpandArrow GetExpanderArrow(Control control) => + (ExpandArrow)control.GetValue(ExpanderArrowProperty); + + public static void SetExpanderArrow(Control control, ExpandArrow value) => + control.SetValue(ExpanderArrowProperty, value); + + public static readonly DependencyProperty ExpanderArrowProperty = + DependencyProperty.RegisterAttached( + "ExpanderArrow", + typeof(ExpandArrow), + typeof(ExpanderHelper), + null); + + public static bool GetIsListItem(Control control) => + (bool)control.GetValue(IsListItemProperty); + + public static void SetIsListItem(Control control, bool value) => + control.SetValue(IsListItemProperty, value); + + public static readonly DependencyProperty IsListItemProperty = + DependencyProperty.RegisterAttached( + "IsListItem", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static bool GetIsHeaderVisible(Control control) => + (bool)control.GetValue(IsHeaderVisibleProperty); + + public static void SetIsHeaderVisible(Control control, bool value) => + control.SetValue(IsHeaderVisibleProperty, value); + + public static readonly DependencyProperty IsHeaderVisibleProperty = + DependencyProperty.RegisterAttached( + "IsHeaderVisible", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static bool GetIsContentCollapsed(Control control) => + (bool)control.GetValue(IsContentCollapsedProperty); + + public static void SetIsContentCollapsed(Control control, bool value) => + control.SetValue(IsContentCollapsedProperty, value); + + public static readonly DependencyProperty IsContentCollapsedProperty = + DependencyProperty.RegisterAttached( + "IsContentCollapsed", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static bool GetIsAnimationActive(UIElement control) => + (bool)control.GetValue(IsAnimationActiveProperty); + + public static void SetIsAnimationActive(UIElement control, bool value) => + control.SetValue(IsAnimationActiveProperty, value); + + public static readonly DependencyProperty IsAnimationActiveProperty = + DependencyProperty.RegisterAttached( + "IsAnimationActive", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static double GetExpansionProgress(UIElement control) => + (double)control.GetValue(ExpansionProgressProperty); + + public static void SetExpansionProgress(UIElement control, double value) => + control.SetValue(ExpansionProgressProperty, value); + + public static readonly DependencyProperty ExpansionProgressProperty = + DependencyProperty.RegisterAttached( + "ExpansionProgress", + typeof(double), + typeof(ExpanderHelper), + null); + + public static bool GetIsExpandEnabled(UIElement control) => + (bool)control.GetValue(IsExpandEnabledProperty); + + public static void SetIsExpandEnabled(UIElement control, bool value) => + control.SetValue(IsExpandEnabledProperty, value); + + public static readonly DependencyProperty IsExpandEnabledProperty = + DependencyProperty.RegisterAttached( + "IsExpandEnabled", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static object GetHeaderBottomContent(UIElement control) => + control.GetValue(HeaderBottomContentProperty); + + public static void SetHeaderBottomContent(UIElement control, object value) => + control.SetValue(HeaderBottomContentProperty, value); + + public static readonly DependencyProperty HeaderBottomContentProperty = + DependencyProperty.RegisterAttached( + "HeaderBottomContent", + typeof(object), + typeof(ExpanderHelper), + null); + + public static bool GetIsHeaderToggleExternal(UIElement control) => + (bool)control.GetValue(IsHeaderToggleExternalProperty); + + public static void SetIsHeaderToggleExternal(UIElement control, bool value) => + control.SetValue(IsHeaderToggleExternalProperty, value); + + public static readonly DependencyProperty IsHeaderToggleExternalProperty = + DependencyProperty.RegisterAttached( + "IsHeaderToggleExternal", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static bool GetIsHeaderPressed(UIElement control) => + (bool)control.GetValue(IsHeaderPressedProperty); + + public static void SetIsHeaderPressed(UIElement control, bool value) => + control.SetValue(IsHeaderPressedProperty, value); + + public static readonly DependencyProperty IsHeaderPressedProperty = + DependencyProperty.RegisterAttached( + "IsHeaderPressed", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static bool GetIsHeaderMouseOver(UIElement control) => + (bool)control.GetValue(IsHeaderMouseOverProperty); + + public static void SetIsHeaderMouseOver(UIElement control, bool value) => + control.SetValue(IsHeaderMouseOverProperty, value); + + public static readonly DependencyProperty IsHeaderMouseOverProperty = + DependencyProperty.RegisterAttached( + "IsHeaderMouseOver", + typeof(bool), + typeof(ExpanderHelper), + null); + + public static bool GetIsTransparent(Control control) => + (bool)control.GetValue(IsTransparentProperty); + + public static void SetIsTransparent(Control control, bool value) => + control.SetValue(IsTransparentProperty, value); + + public static readonly DependencyProperty IsTransparentProperty = + DependencyProperty.RegisterAttached( + "IsTransparent", + typeof(bool), + typeof(ExpanderHelper), + null); +} diff --git a/ADB Explorer _WpfUi/Helpers/Attachable/MenuHelper.cs b/ADB Explorer _WpfUi/Helpers/Attachable/MenuHelper.cs new file mode 100644 index 00000000..24d80fa7 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/Attachable/MenuHelper.cs @@ -0,0 +1,84 @@ +using Windows.UI.Popups; + +namespace ADB_Explorer.Helpers; + +public static class MenuHelper +{ + public static bool GetIsMouseSelectionVisible(UIElement control) => + (bool)control.GetValue(IsMouseSelectionVisibleProperty); + + public static void SetIsMouseSelectionVisible(UIElement control, bool value) => + control.SetValue(IsMouseSelectionVisibleProperty, value); + + public static readonly DependencyProperty IsMouseSelectionVisibleProperty = + DependencyProperty.RegisterAttached( + "IsMouseSelectionVisible", + typeof(bool), + typeof(MenuHelper), + null); + + public static Brush GetCheckBackground(UIElement control) => + (Brush)control.GetValue(CheckBackgroundProperty); + + public static void SetCheckBackground(UIElement control, Brush value) => + control.SetValue(CheckBackgroundProperty, value); + + public static readonly DependencyProperty CheckBackgroundProperty = + DependencyProperty.RegisterAttached( + "CheckBackground", + typeof(Brush), + typeof(MenuHelper), + null); + + public static Thickness GetItemPadding(UIElement control) => + (Thickness)control.GetValue(ItemPaddingProperty); + + public static void SetItemPadding(UIElement control, Thickness value) => + control.SetValue(ItemPaddingProperty, value); + + public static readonly DependencyProperty ItemPaddingProperty = + DependencyProperty.RegisterAttached( + "ItemPadding", + typeof(Thickness), + typeof(MenuHelper), + null); + + public static Thickness GetItemMargin(UIElement control) => + (Thickness)control.GetValue(ItemMarginProperty); + + public static void SetItemMargin(UIElement control, Thickness value) => + control.SetValue(ItemMarginProperty, value); + + public static readonly DependencyProperty ItemMarginProperty = + DependencyProperty.RegisterAttached( + "ItemMargin", + typeof(Thickness), + typeof(MenuHelper), + null); + + public static bool? GetIsButtonMenu(UIElement control) => + (bool?)control.GetValue(IsButtonMenuProperty); + + public static void SetIsButtonMenu(UIElement control, bool? value) => + control.SetValue(IsButtonMenuProperty, value); + + public static readonly DependencyProperty IsButtonMenuProperty = + DependencyProperty.RegisterAttached( + "IsButtonMenu", + typeof(bool?), + typeof(MenuHelper), + null); + + public static PlacementMode GetDropDownPlacement(UIElement control) => + (PlacementMode)control.GetValue(DropDownPlacementProperty); + + public static void SetDropDownPlacement(UIElement control, PlacementMode value) => + control.SetValue(DropDownPlacementProperty, value); + + public static readonly DependencyProperty DropDownPlacementProperty = + DependencyProperty.RegisterAttached( + "DropDownPlacement", + typeof(PlacementMode), + typeof(MenuHelper), + null); +} diff --git a/ADB Explorer _WpfUi/Helpers/Attachable/SelectionHelper.cs b/ADB Explorer _WpfUi/Helpers/Attachable/SelectionHelper.cs new file mode 100644 index 00000000..e4a8e558 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/Attachable/SelectionHelper.cs @@ -0,0 +1,181 @@ +namespace ADB_Explorer.Helpers; + +public static class SelectionHelper +{ + public enum MenuType + { + Context, + Submenu, + Menubar, + } + + public static MenuType GetMenuType(UIElement control) => + (MenuType)control.GetValue(MenuTypeProperty); + + public static void SetMenuType(UIElement control, MenuType value) => + control.SetValue(MenuTypeProperty, value); + + public static readonly DependencyProperty MenuTypeProperty = + DependencyProperty.RegisterAttached( + "MenuType", + typeof(MenuType), + typeof(SelectionHelper), + null); + + public static bool GetIsMenuOpen(UIElement control) => + (bool)control.GetValue(IsMenuOpenProperty); + + public static void SetIsMenuOpen(UIElement control, bool value) => + control.SetValue(IsMenuOpenProperty, value); + + public static readonly DependencyProperty IsMenuOpenProperty = + DependencyProperty.RegisterAttached( + "IsMenuOpen", + typeof(bool), + typeof(SelectionHelper), + null); + + public static int GetFirstSelectedIndex(UIElement control) => + (int)control.GetValue(FirstSelectedIndexProperty); + + public static void SetFirstSelectedIndex(UIElement control, int value) => + control.SetValue(FirstSelectedIndexProperty, value); + + public static readonly DependencyProperty FirstSelectedIndexProperty = + DependencyProperty.RegisterAttached( + "FirstSelectedIndex", + typeof(int), + typeof(SelectionHelper), + null); + + public static int GetCurrentSelectedIndex(UIElement control) => + (int)control.GetValue(CurrentSelectedIndexProperty); + + public static void SetCurrentSelectedIndex(UIElement control, int value) => + control.SetValue(CurrentSelectedIndexProperty, value); + + public static readonly DependencyProperty CurrentSelectedIndexProperty = + DependencyProperty.RegisterAttached( + "CurrentSelectedIndex", + typeof(int), + typeof(SelectionHelper), + null); + + public static int GetNextSelectedIndex(UIElement control) => + (int)control.GetValue(NextSelectedIndexProperty); + + public static void SetNextSelectedIndex(UIElement control, int value) => + control.SetValue(NextSelectedIndexProperty, value); + + public static readonly DependencyProperty NextSelectedIndexProperty = + DependencyProperty.RegisterAttached( + "NextSelectedIndex", + typeof(int), + typeof(SelectionHelper), + null); + + /// + /// Sets index to First, Current, and Next + /// + /// + /// + public static void SetIndexSingle(UIElement control, int value) + { + SetFirstSelectedIndex(control, value); + SetCurrentSelectedIndex(control, value); + SetNextSelectedIndex(control, value); + } + + public static bool GetSelectionInProgress(UIElement control) => + (bool)control.GetValue(SelectionInProgressProperty); + + public static void SetSelectionInProgress(UIElement control, bool value) => + control.SetValue(SelectionInProgressProperty, value); + + public static readonly DependencyProperty SelectionInProgressProperty = + DependencyProperty.RegisterAttached( + "SelectionInProgress", + typeof(bool), + typeof(SelectionHelper), + null); + + public static void MultiSelect(this DataGrid dataGrid, Key key) + { + SetSelectionInProgress(dataGrid, true); + + var firstIndex = GetFirstSelectedIndex(dataGrid); + var currentIndex = GetCurrentSelectedIndex(dataGrid); + + if (key == Key.Up) + currentIndex--; + else if (key == Key.Down) + currentIndex++; + else if (key == Key.Home) + currentIndex = 0; + else if (key == Key.End) + currentIndex = dataGrid.Items.Count - 1; + + dataGrid.UnselectAll(); + + var index1 = firstIndex < currentIndex ? firstIndex : currentIndex; + var index2 = firstIndex < currentIndex ? currentIndex : firstIndex; + + for (int i = index1; i <= index2; i++) + { + if (i < 0 || i >= dataGrid.Items.Count) + continue; + + dataGrid.SelectedItems.Add(dataGrid.Items[i]); + } + + if (currentIndex >= 0 && currentIndex < dataGrid.Items.Count) + dataGrid.ScrollIntoView(dataGrid.Items[currentIndex]); + + if (currentIndex >= 0 && currentIndex < dataGrid.Items.Count) + SetCurrentSelectedIndex(dataGrid, currentIndex); + + SetSelectionInProgress(dataGrid, false); + } + + public static void SingleSelect(this DataGrid dataGrid, Key key) + { + if (dataGrid.Items.Count == 1 && dataGrid.SelectedIndex == -1) + { + dataGrid.SelectedIndex = 0; + return; + } + + dataGrid.SelectedIndex = GetCurrentSelectedIndex(dataGrid); + + if (key == Key.Up) + { + if (dataGrid.SelectedIndex > -1) + dataGrid.SelectedIndex--; + else + dataGrid.SelectedIndex = dataGrid.Items.Count - 1; + } + else if (key == Key.Down) + { + if (dataGrid.SelectedIndex < 0 || dataGrid.Items.IndexOf(dataGrid.SelectedItems[^1]) < dataGrid.Items.Count - 1) + dataGrid.SelectedIndex++; + else + dataGrid.SelectedIndex = -1; + } + else if (key == Key.Home) + { + dataGrid.SelectedIndex = 0; + } + else if (key == Key.End) + { + dataGrid.SelectedIndex = dataGrid.Items.Count - 1; + } + + SetCurrentSelectedIndex(dataGrid, dataGrid.SelectedIndex); + SetFirstSelectedIndex(dataGrid, dataGrid.SelectedIndex); + if (dataGrid.SelectedIndex > -1) + dataGrid.ScrollIntoView(dataGrid.SelectedItem); + } + + public static System.Windows.Controls.ListViewItem GetListViewItemContainer(System.Windows.Controls.ListView listView, int index = -1) => + listView.ItemContainerGenerator.ContainerFromIndex(index < 0 ? listView.SelectedIndex : index) as System.Windows.Controls.ListViewItem; +} diff --git a/ADB Explorer _WpfUi/Helpers/Attachable/StyleHelper.cs b/ADB Explorer _WpfUi/Helpers/Attachable/StyleHelper.cs new file mode 100644 index 00000000..a8d2857b --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/Attachable/StyleHelper.cs @@ -0,0 +1,221 @@ +namespace ADB_Explorer.Helpers; + +public static class StyleHelper +{ + public enum ContentAnimation + { + None, + Bounce, + RotateCW, + RotateCCW, + LeftMarquee, + RightMarquee, + UpMarquee, + DownMarquee, + Pulsate, + } + + public static ContentAnimation GetContentAnimation(Control control) => + (ContentAnimation)control.GetValue(ContentAnimationProperty); + + public static void SetContentAnimation(Control control, ContentAnimation value) => + control.SetValue(ContentAnimationProperty, value); + + public static readonly DependencyProperty ContentAnimationProperty = + DependencyProperty.RegisterAttached( + "ContentAnimation", + typeof(ContentAnimation), + typeof(StyleHelper), + null); + + public static bool GetActivateAnimation(Control control) => + (bool)control.GetValue(ActivateAnimationProperty); + + public static void SetActivateAnimation(Control control, bool value) => + control.SetValue(ActivateAnimationProperty, value); + + public static readonly DependencyProperty ActivateAnimationProperty = + DependencyProperty.RegisterAttached( + "ActivateAnimation", + typeof(bool), + typeof(StyleHelper), + null); + + public static bool GetUseFluentStyles(Control control) => + (bool)control.GetValue(UseFluentStylesProperty); + + public static void SetUseFluentStyles(Control control, bool value) => + control.SetValue(UseFluentStylesProperty, value); + + public static readonly DependencyProperty UseFluentStylesProperty = + DependencyProperty.RegisterAttached( + "UseFluentStyles", + typeof(bool), + typeof(StyleHelper), + null); + + public static bool GetAnimateOnClick(Control control) => + (bool)control.GetValue(AnimateOnClickProperty); + + public static void SetAnimateOnClick(Control control, bool value) => + control.SetValue(AnimateOnClickProperty, value); + + public static readonly DependencyProperty AnimateOnClickProperty = + DependencyProperty.RegisterAttached( + "AnimateOnClick", + typeof(bool), + typeof(StyleHelper), + null); + + public static bool GetIsUnchecked(ToggleButton control) => + (bool)control.GetValue(IsUncheckedProperty); + + public static void SetIsUnchecked(ToggleButton control, bool value) => + control.SetValue(IsUncheckedProperty, value); + + public static readonly DependencyProperty IsUncheckedProperty = + DependencyProperty.RegisterAttached( + "IsUnchecked", + typeof(bool), + typeof(StyleHelper), + null); + + public static bool GetBeginAnimation(Control control) => + (bool)control.GetValue(BeginAnimationProperty); + + public static void SetBeginAnimation(Control control, bool value) => + control.SetValue(BeginAnimationProperty, value); + + public static readonly DependencyProperty BeginAnimationProperty = + DependencyProperty.RegisterAttached( + "BeginAnimation", + typeof(bool), + typeof(StyleHelper), + null); + + public static Brush GetPressedForeground(Control control) => + (Brush)control.GetValue(PressedForegroundProperty); + + public static void SetPressedForeground(Control control, Brush value) => + control.SetValue(PressedForegroundProperty, value); + + public static readonly DependencyProperty PressedForegroundProperty = + DependencyProperty.RegisterAttached( + "PressedForeground", + typeof(Brush), + typeof(StyleHelper), + null); + + public static Brush GetAltBorderBrush(UIElement control) => + (Brush)control.GetValue(AltBorderBrushProperty); + + public static void SetAltBorderBrush(UIElement control, Brush value) => + control.SetValue(AltBorderBrushProperty, value); + + public static readonly DependencyProperty AltBorderBrushProperty = + DependencyProperty.RegisterAttached( + "AltBorderBrush", + typeof(Brush), + typeof(StyleHelper), + null); + + public static string GetThreeStateGlyph(CheckBox control) => + (string)control.GetValue(ThreeStateGlyphProperty); + + public static void SetThreeStateGlyph(CheckBox control, string value) => + control.SetValue(ThreeStateGlyphProperty, value); + + public static readonly DependencyProperty ThreeStateGlyphProperty = + DependencyProperty.RegisterAttached( + "ThreeStateGlyph", + typeof(string), + typeof(StyleHelper), + null); + + public static UIElement GetPopupPlacementTarget(DependencyObject control) => + (UIElement)control.GetValue(PopupPlacementTargetProperty); + + public static void SetPopupPlacementTarget(DependencyObject control, UIElement value) => + control.SetValue(PopupPlacementTargetProperty, value); + + public static readonly DependencyProperty PopupPlacementTargetProperty = + DependencyProperty.RegisterAttached( + "PopupPlacementTarget", + typeof(UIElement), + typeof(StyleHelper), + null); + + public static PlacementMode GetPopupPlacement(DependencyObject control) => + (PlacementMode)control.GetValue(PopupPlacementProperty); + + public static void SetPopupPlacement(DependencyObject control, PlacementMode value) => + control.SetValue(PopupPlacementProperty, value); + + public static readonly DependencyProperty PopupPlacementProperty = + DependencyProperty.RegisterAttached( + "PopupPlacement", + typeof(PlacementMode), + typeof(StyleHelper), + null); + + + private static readonly Dictionary ChildrenProperties = []; + + public static T FindDescendant(DependencyObject control, bool includeSelf = false) where T : DependencyObject + { + if (control is null) + return null; + + if (includeSelf && control is T tControl) + return tControl; + + var key = $"Child{typeof(T).Name}"; + if (!ChildrenProperties.TryGetValue(key, out var requestedChild)) + { + requestedChild = DependencyProperty.RegisterAttached( + key, + typeof(T), + typeof(StyleHelper), + null); + + ChildrenProperties.Add(key, requestedChild); + } + + var saved = control.GetValue(requestedChild) as T; + if (saved is not null) + return saved; + + var descendant = _findDescendant(control); + control.SetValue(requestedChild, descendant); + + return descendant; + } + + private static T _findDescendant(DependencyObject control) where T : DependencyObject + { + if (control is null) + return null; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(control); i++) + { + var child = VisualTreeHelper.GetChild(control, i); + if (child is T tChild) + return tChild; + + var descendant = _findDescendant(child); + if (descendant is not null) + return descendant; + } + + return null; + } + + public static void VerifyIcon(string icon, [CallerMemberName] string propertyName = null) + { + if (!IsFontIcon(icon)) + throw new ArgumentException("An icon must be one char in range E000-F8FF", propertyName); + } + + public static bool IsFontIcon(string icon) => + icon is null || (icon.Length == 1 && char.GetUnicodeCategory(icon, 0) is UnicodeCategory.PrivateUse); +} diff --git a/ADB Explorer _WpfUi/Helpers/Attachable/TextHelper.cs b/ADB Explorer _WpfUi/Helpers/Attachable/TextHelper.cs new file mode 100644 index 00000000..d8c07ad1 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/Attachable/TextHelper.cs @@ -0,0 +1,286 @@ +using System.Windows.Documents; +using System.Windows.Markup; +using System.Xml; + +namespace ADB_Explorer.Helpers; + +public static class TextHelper +{ + public static string GetAltText(UIElement control) => + (string)control.GetValue(AltTextProperty); + + public static void SetAltText(UIElement control, string value) => + control.SetValue(AltTextProperty, value); + + public static readonly DependencyProperty AltTextProperty = + DependencyProperty.RegisterAttached( + "AltText", + typeof(string), + typeof(TextHelper), + null); + + public static bool GetIsValidating(UIElement control) => + (bool)control.GetValue(IsValidatingProperty); + + public static void SetIsValidating(UIElement control, bool value) => + control.SetValue(IsValidatingProperty, value); + + public static readonly DependencyProperty IsValidatingProperty = + DependencyProperty.RegisterAttached( + "IsValidating", + typeof(bool), + typeof(TextHelper), + null); + + public enum ValidationType + { + None, + SeparateDigits, + SeparateAndLimitDigits, + LimitDigits, + LimitNumber, + LimitDigitsAndNumber, + FilterString, + SeparateFormat, + } + + public static void SeparateDigits(this TextBox textbox, char separator) => TextBoxValidation(textbox, separator); + + public static void SeparateAndLimitDigits(this TextBox textbox, char separator, int maxChars) => TextBoxValidation(textbox, separator, maxChars); + + public static void LimitDigits(this TextBox textbox, int maxLength) => TextBoxValidation(textbox, maxChars: maxLength); + + public static void LimitNumber(this TextBox textbox, ulong maxNumber) => TextBoxValidation(textbox, maxChars: $"{maxNumber}".Length, maxNumber: maxNumber); + + public static void LimitDigitsAndNumber(this TextBox textbox, int maxLength, ulong maxNumber) => TextBoxValidation(textbox, maxChars: maxLength, maxNumber: maxNumber); + + public static void FilterString(this TextBox textbox, params char[] invalidChars) => TextBoxValidation(textbox, specialChars: invalidChars, numeric: false); + + public static void SeparateFormat(this TextBox textbox, char separator, ulong maxNumber, int maxSeparators) => TextBoxValidation(textbox, specialChars: separator, maxNumber: maxNumber, maxSeparators: maxSeparators); + + public static void TextBoxValidation(TextBox textBox, + char? separator = null, + int maxChars = -1, + bool numeric = true, + ulong maxNumber = 9, + int maxSeparators = -1, + params char[] specialChars) + { + if (GetIsValidating(textBox)) + return; + else + SetIsValidating(textBox, true); + + var caretIndex = textBox.CaretIndex; + var text = textBox.Text; + var altText = GetAltText(textBox); + var maxLength = textBox.MaxLength; + + TextBoxValidation(ref caretIndex, ref text, ref altText, ref maxLength, separator, maxChars, numeric, maxNumber, maxSeparators, specialChars); + + textBox.Text = text; + textBox.CaretIndex = caretIndex; + SetAltText(textBox, altText); + textBox.MaxLength = maxLength; + + SetIsValidating(textBox, false); + } + + /// + /// Provides validation and separation of text in a . + /// + /// The textbox to be validated + /// Text separator. Default is null - no separator + /// Maximum allowed characters in the text. Default is -1 - no length validation + /// Enable numeric validation. Default is + /// When numeric is enabled - allowed non-numeric chars. Otherwise - forbidden chars + public static void TextBoxValidation(ref int caretIndex, + ref string text, + ref string altText, + ref int maxLength, + char? separator = null, + int maxChars = -1, + bool numeric = true, + ulong maxNumber = 9, + int maxSeparators = -1, + params char[] specialChars) + { + var output = ""; + var numbers = ""; + var deletedChars = 0; + if (altText is null) + altText = ""; + + if (text.Length < 1) + return; + + if (numeric && altText.Length - 1 == text.Length && specialChars.Contains(altText[^1])) + text = text[..^1]; + + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + bool validChar = true; + + if (numeric) + { + if (char.IsDigit(c)) + { + validChar = ulong.Parse($"{c}") <= maxNumber; + if (maxNumber > 9) + { + var index = specialChars.Length == 0 || !text.Contains(specialChars[0]) ? 0 : text[..i].LastIndexOf(specialChars[0]) + 1; + if (index < i) + { + // Valid if parse fails + if (ulong.TryParse(text[index..(i + 1)], out ulong res)) + validChar = res <= maxNumber; + } + } + } + else + { + validChar = specialChars.Contains(c) + && i > 0 + && c != text[i - 1] + && !(maxSeparators > 0 && text.Count(c => c == specialChars[0]) > maxSeparators + && i == text.LastIndexOf(specialChars[0])); + } + } + else + { + validChar = !specialChars.Contains(c); + } + + if (validChar) + numbers += c; + else if (c != separator) + deletedChars++; + } + + if (separator is null) + { + output = numbers; + } + else + { + for (int i = 0; i < numbers.Length; i++) + { + output += $"{(i > 0 ? separator : "")}{numbers[i]}"; + } + } + + if (maxNumber > 9 && specialChars.Length > 0 && output.Length > 0) + { + if (output[^1] != specialChars[0]) + { + var index = !text.Contains(specialChars[0]) ? 0 : output.LastIndexOf(specialChars[0]) + 1; + if (output.Length - index == $"{maxNumber}".Length) + { + if (maxSeparators < 0 || output.Count(c => c == specialChars[0]) < maxSeparators) + { + output += specialChars[0]; + caretIndex++; + } + } + } + + var items = output.Split(specialChars[0]); + for (int i = 0; i < items.Length; i++) + { + if (items[i].Length > $"{maxNumber}".Length) + { + var newItem = items[i][^$"{maxNumber}".Length..]; + deletedChars += (items[i].Length - newItem.Length); + items[i] = newItem; + } + } + output = string.Join(specialChars[0], items); + } + + if (deletedChars > 0 && (altText.Length > output.Length || deletedChars == text.Length - altText.Length)) + { + text = altText; + caretIndex -= deletedChars; + + if (caretIndex < 0) + caretIndex = 0; + + return; + } + + if (maxChars > -1) + maxLength = separator is null ? maxChars : (maxChars * 2) - 1; + + caretIndex -= deletedChars; + if (separator is not null) + caretIndex += output.Count(c => c == separator) - text.Count(c => c == separator); + + text = output; + + if (caretIndex < 0) + caretIndex = 0; + + altText = output; + } + + /// + /// Checks if the character is a right-to-left character, except for the RTL mark 200F + /// + /// + /// + public static bool IsRtlCharacter(char c) + { + // http://www.unicode.org/Public/6.0.0/ucd/UnicodeData.txt + + int[][] ranges = + [ + [0x0590, 0x07B1], // Hebrew, Arabic, Syriac, & Thaana + [0xFB1D, 0xFDFD], // Hebrew & Arabic ligatures + [0xFE70, 0xFEFC] // More Arabic + ]; + + foreach (var range in ranges) + { + if (c >= range[0] && c <= range[1]) + return true; + } + return false; + } + + /// + /// Checks if the text contains at least one right-to-left character, except for the RTL mark 200F + /// + /// + /// + public static bool ContainsRtl(string text) + { + foreach (char c in text) + { + if (IsRtlCharacter(c)) + return true; + } + return false; + } + + public const char LTR_MARK = '\u200E'; + public const char RTL_MARK = '\u200F'; + + public static void BuildLocalizedInlines(object sender, RoutedEventArgs e) + { + var textBlock = sender as TextBlock; + var altText = GetAltText(textBlock); + + string xamlString = $" value is bool and true ? Visibility.Visible : Visibility.Collapsed; + + public static void Visible(this FrameworkElement control, bool value) => control.Visibility = Visible(value); + + public static bool Visible(this FrameworkElement control) => control.Visibility == Visibility.Visible; + + public static void ToggleVisibility(this FrameworkElement control) => control.Visible(!control.Visible()); +} diff --git a/ADB Explorer _WpfUi/Helpers/DeviceHelper.cs b/ADB Explorer _WpfUi/Helpers/DeviceHelper.cs new file mode 100644 index 00000000..e9232be1 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/DeviceHelper.cs @@ -0,0 +1,810 @@ +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using ADB_Explorer.Services.AppInfra; +using ADB_Explorer.ViewModels; +using ADB_Explorer.Views.Pages; +using Windows.Management.Deployment; +using Wpf.Ui; +using static ADB_Explorer.Models.AbstractDevice; + +namespace ADB_Explorer.Helpers; + +public static class DeviceHelper +{ + public static DeviceStatus GetStatus(string status) => status switch + { + "device" or "recovery" or "sideload" => DeviceStatus.Ok, + "offline" => DeviceStatus.Offline, + "unauthorized" or "authorizing" => DeviceStatus.Unauthorized, + _ => throw new NotImplementedException(), + }; + + public static DeviceType GetType(string id, string status) + { + if (status == "recovery") + return DeviceType.Recovery; + + if (status == "sideload") + return DeviceType.Sideload; + + if (id.Contains("._adb-tls-")) + return DeviceType.Service; + if (id.Contains(':')) + { + return AdbExplorerConst.LOOPBACK_ADDRESSES.Contains(id.Split(':')[0]) + ? DeviceType.WSA + : DeviceType.Remote; + } + + return id.Contains("emulator") + ? DeviceType.Emulator + : DeviceType.Local; + } + + public static LogicalDrive GetMmcDrive(IEnumerable drives, string deviceID) + { + if (drives is null) + return null; + + // Try to find the MMC in the props + if (Data.CurrentADBDevice.MmcProp is string mmcId) + { + return drives.FirstOrDefault(d => d.ID == mmcId); + } + // If OTG exists, but no MMC ID - there is no MMC + else if (Data.CurrentADBDevice.OtgProp is not null) + return null; + + var externalDrives = drives.Where(d => d.Type is AbstractDrive.DriveType.Unknown); + + switch (externalDrives.Count()) + { + // MMC ID has to be acquired if more than one extension drive exists + case > 1: + var mmc = ADBService.GetMmcId(deviceID); + return drives.FirstOrDefault(d => d.ID == mmc); + + // Only check whether MMC exists if there's only one drive + case 1: + return ADBService.MmcExists(deviceID) ? externalDrives.First() : null; + default: + return null; + } + } + + /// + /// Sets type of all drives with unknown type as external. Changes the object itself. + /// + /// The collection of s to change + public static void SetExternalDrives(IEnumerable drives) + { + if (drives is null) + return; + + foreach (var item in drives.Where(d => d.Type == AbstractDrive.DriveType.Unknown)) + { + item.Type = AbstractDrive.DriveType.External; + } + } + + public static string ParseDeviceName(string model, string device) + { + var name = device; + if (device == device.ToLower()) + name = model; + + return name.Replace('_', ' '); + } + + public static void BrowseDeviceAction(LogicalDeviceViewModel device) + { + Data.RuntimeSettings.DeviceToOpen = device; + } + + public static void SideloadDeviceAction(LogicalDeviceViewModel device) + { + OpenFileDialog dialog = new() + { + Title = Strings.Resources.S_SIDELOAD_ROM_TITLE, + Filter = $"{Strings.Resources.S_ROM_FILE}|*.zip", + Multiselect = false, + }; + + if (dialog.ShowDialog() is not true) + return; + + var res = ADBService.ExecuteDeviceAdbCommand(device.ID, "sideload", out string stdout, out string stderr, CancellationToken.None, ADBService.EscapeAdbString(dialog.FileName)); + DialogService.ShowMessage(string.Join('\n', stdout, stderr), + Strings.Resources.S_REBOOT_SIDELOAD, + res == 0 ? DialogService.DialogIcon.Informational + : DialogService.DialogIcon.Critical); + } + + private static async void RemoveDeviceAction(DeviceViewModel device) + { + var message = device.Type is DeviceType.Emulator + ? Strings.Resources.S_KILL_EMULATOR + : Strings.Resources.S_REM_DEVICE; + + var name = device switch + { + HistoryDeviceViewModel dev when string.IsNullOrEmpty(dev.DeviceName) => dev.IpAddress, + HistoryDeviceViewModel dev => dev.DeviceName, + LogicalDeviceViewModel dev => dev.Name, + _ => throw new NotImplementedException(), + }; + + var title = device.Type is DeviceType.Emulator + ? Strings.Resources.S_KILL_EMULATOR_TITLE + : Strings.Resources.S_REM_DEVICE_TITLE; + + var dialogTask = await DialogService.ShowConfirmation(message, string.Format(title, name)); + if (dialogTask.Item1 is not Wpf.Ui.Controls.ContentDialogResult.Primary) + return; + + if (device.Type is DeviceType.Emulator) + { + try + { + ADBService.KillEmulator(device.ID); + } + catch (Exception ex) + { + DialogService.ShowMessage(ex.Message, Strings.Resources.S_DISCONN_FAILED_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true); + return; + } + } + else if (device.Type is DeviceType.Remote) + { + try + { + ADBService.DisconnectNetworkDevice(device.ID); + } + catch (Exception ex) + { + DialogService.ShowMessage(ex.Message, Strings.Resources.S_DISCONN_FAILED_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true); + return; + } + } + else if (device.Type is DeviceType.History) + { } // No additional action is required + else + { + throw new NotImplementedException(); + } + + RemoveDevice(device); + } + + public static DeviceAction RemoveDeviceCommand(DeviceViewModel device) => new( + () => device.Type is DeviceType.History + || !Data.RuntimeSettings.IsManualPairingInProgress + && device.Type is DeviceType.Remote or DeviceType.Emulator, + () => RemoveDeviceAction(device), + device.Type switch + { + DeviceType.Remote => Strings.Resources.S_REM_DEV, + DeviceType.Emulator => Strings.Resources.S_REM_EMU, + DeviceType.History => Strings.Resources.S_REM_HIST_DEV, + _ => "", + }); + + public static DeviceAction ToggleRootDeviceCommand(LogicalDeviceViewModel device) => new( + () => device.Root is not RootStatus.Forbidden + && device.Status is DeviceStatus.Ok + && device.Type is not DeviceType.Sideload and not DeviceType.Recovery, + () => ToggleRootAction(device)); + + private static async void ToggleRootAction(LogicalDeviceViewModel device) + { + bool rootEnabled = device.Root is RootStatus.Enabled; + + await Task.Run(() => device.EnableRoot(!rootEnabled)); + + if (device.Root is RootStatus.Forbidden) + { + App.Current.Dispatcher.Invoke(() => DialogService.ShowMessage(Strings.Resources.S_ROOT_FORBID, Strings.Resources.S_ROOT_FORBID_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true)); + } + } + + public static DeviceAction ConnectDeviceCommand(NewDeviceViewModel device) => new( + () => { + if (!device.IsConnectPortValid) + return false; + + if (!device.IsIpAddressValid && !device.IsHostNameValid) + return false; + + return !device.IsPairingEnabled + || (device.IsPairingCodeValid && device.IsPairingPortValid); + }, + () => Data.RuntimeSettings.ConnectNewDevice = device); + + public static DeviceAction LaunchWsa(WsaPkgDeviceViewModel device) => new( + () => device.Status is DeviceStatus.Ok, + async () => + { + if (Data.Settings.ShowLaunchWsaMessage) + { + var result = await DialogService.ShowConfirmation(Strings.Resources.S_WSA_LAUNCH, + Strings.Resources.S_WSA_DIALOG_TITLE, + primaryText: Strings.Resources.S_BUTTON_LAUNCH, + checkBoxText: Strings.Resources.S_DONT_SHOW_AGAIN, + icon: DialogService.DialogIcon.Exclamation, + censorContent: false); + + Data.Settings.ShowLaunchWsaMessage = !result.Item2; + + if (result.Item1 is not Wpf.Ui.Controls.ContentDialogResult.Primary) + return; + } + + device.SetLastLaunch(); + device.SetStatus(DeviceStatus.Unauthorized); + Process.Start($"{AdbExplorerConst.WSA_PROCESS_NAME}.exe"); + }); + + public static readonly Predicate DevicePredicate = device => + { + // The mDNS device cannot hide itself when in a listview + if (device is MdnsDeviceViewModel) + return Data.Settings.EnableMdns; + + // current device cannot be hidden + if (device is LogicalDeviceViewModel { IsOpen: true }) + return true; + + if (device is LogicalDeviceViewModel logDev && device.Type is DeviceType.Service) + { + if (device.Status is DeviceStatus.Offline) + { + // if a logical service is offline, and we have one of its services - hide the logical service + return Data.DevicesObject.ServiceDeviceViewModels.All(s => s.IpAddress != device.IpAddress); + } + + // if there's a logical service and a remote device with the same IP - hide the logical service + var res = !Data.DevicesObject.LogicalDeviceViewModels.Any(l => l.IpAddress == device.IpAddress + && l.Type is DeviceType.Remote or DeviceType.Local + && l.Status is DeviceStatus.Ok); + + if (res) + logDev.UseIdForName = false; + + return res; + } + + if (device is LogicalDeviceViewModel && device.Type is DeviceType.Remote) + { + // if a remote device is also connected by USB and both are authorized - hide the remote device + return !Data.DevicesObject.LogicalDeviceViewModels.Any(usb => usb.Type is DeviceType.Local + && usb.Status is DeviceStatus.Ok + && (device.ID.Contains(usb.ID) + || usb.IpAddress == device.IpAddress)); + } + + if (device is HistoryDeviceViewModel hist) + { + // if there's any device with the IP of a history device - hide the history device + return Data.Settings.SaveDevices && !Data.DevicesObject.LogicalDeviceViewModels.Any(logical => logical.IpAddress == hist.IpAddress || logical.IpAddress == hist.HostName) + && !Data.DevicesObject.ServiceDeviceViewModels.Any(service => service.IpAddress == hist.IpAddress || service.IpAddress == hist.HostName); + } + + if (device is ServiceDeviceViewModel service) + { + // connect services are always hidden + if (service is ConnectServiceViewModel) + return false; + + // if there's any online logical device with the IP of a pairing service - hide the pairing service + if (Data.DevicesObject.LogicalDeviceViewModels.Any(logical => logical.Status is not DeviceStatus.Offline && logical.IpAddress == service.IpAddress)) + return false; + + // if there's any QR service with the IP of a code pairing service - hide the code pairing service + if (service.MdnsType is ServiceDevice.ServiceType.PairingCode + && Data.DevicesObject.ServiceDeviceViewModels.Any(qr => qr.MdnsType is ServiceDevice.ServiceType.QrCode + && qr.IpAddress == service.IpAddress)) + return false; + } + + if (device is WsaPkgDeviceViewModel wsaPkg) + { + // if WSA is not installed - hide it + if (wsaPkg.Status is DeviceStatus.Offline) + return false; + + // if an online logical WSA device exists, the WSA package is hidden + if (Data.DevicesObject.LogicalDeviceViewModels.Any(logical => logical.Type is DeviceType.WSA && logical.Status is not DeviceStatus.Offline)) + return false; + } + + // if there's an offline WSA device - hide it + if (device is LogicalDeviceViewModel { Type: DeviceType.WSA, Status: DeviceStatus.Offline }) + return false; + + if (device is LogicalDeviceViewModel logicalDev && logicalDev.Type is not DeviceType.Emulator) + { + // if there are multiple logical devices of the same model, display their ID instead + logicalDev.UseIdForName = Data.DevicesObject.LogicalDeviceViewModels.Count(dev => dev.Name.Equals(logicalDev.Name) && dev.IpAddress != logicalDev.IpAddress) > 1; + } + + return true; + }; + + public static readonly Predicate DevicesFilter = d => DevicePredicate((DeviceViewModel)d); + + public static void FilterDevices(ICollectionView collectionView) + { + if (collectionView is null) + return; + + if (collectionView.Filter is not null) + { + collectionView.Refresh(); + return; + } + + collectionView.Filter = new(DevicesFilter); + collectionView.SortDescriptions.Clear(); + collectionView.SortDescriptions.Add(new SortDescription(nameof(DeviceViewModel.Type), ListSortDirection.Ascending)); + } + + public static void UpdateDevicesBatInfo() + { + Data.DevicesObject.Current?.UpdateBattery(); + + if (DateTime.Now - Data.DevicesObject.LastUpdate <= AdbExplorerConst.BATTERY_UPDATE_INTERVAL && !Data.RuntimeSettings.IsDevicesView) + return; + + var items = Data.DevicesObject.LogicalDeviceViewModels.Where(device => !device.IsOpen); + foreach (var item in items) + { + item.UpdateBattery(); + } + + Data.DevicesObject.LastUpdate = DateTime.Now; + } + + public static async void ListServices(IEnumerable services) + { + if (services is null) + return; + + var viewModels = services.Select(ServiceDeviceViewModel.New); + + if (!Data.DevicesObject.ServicesChanged(viewModels)) + return; + + Data.DevicesObject.UpdateServices(viewModels); + + var qrClass = Data.MdnsService?.QrClass; + if (qrClass is null) + return; + + var qrServices = Data.DevicesObject.ServiceDeviceViewModels.Where(service => + service.MdnsType == ServiceDevice.ServiceType.QrCode + && service.ID == qrClass.ServiceName); + + if (qrServices.Any()) + { + await PairService(qrServices.First()); + } + } + + public static async Task PairService(ServiceDeviceViewModel service) + { + var code = service.MdnsType == ServiceDevice.ServiceType.QrCode + ? Data.MdnsService?.QrClass?.Password + : service.PairingCode; + + if (string.IsNullOrEmpty(code)) + return false; + + return await Task.Run(() => + { + try + { + ADBService.PairNetworkDevice(service.ID, code); + } + catch (Exception ex) + { + App.Current.Dispatcher.Invoke(() => DialogService.ShowMessage(ex.Message, Strings.Resources.S_PAIR_ERR_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true)); + return false; + } + + return true; + }); + } + + public static void UpdateDevicesRootAccess() + { + var devices = Data.DevicesObject.LogicalDeviceViewModels.Where(d => d.Root is RootStatus.Unchecked).ToList(); + foreach (var device in devices) + { + bool root = ADBService.WhoAmI(device.ID); + bool rootDisabled = Data.DevicesObject.RootDevices.Contains(device.ID); + App.Current?.Dispatcher?.Invoke(() => + { + return device.SetRootStatus(root ? RootStatus.Enabled + : rootDisabled ? RootStatus.Disabled + : RootStatus.Unchecked); + }); + } + } + + public static async void PairNewDevice() + { + var dev = (NewDeviceViewModel)Data.RuntimeSettings.ConnectNewDevice; + await Task.Run(() => + { + try + { + ADBService.PairNetworkDevice(dev.PairingAddress, dev.PairingCode); + return true; + } + catch (Exception ex) + { + App.Current.Dispatcher.Invoke(() => DialogService.ShowMessage(ex.Message, Strings.Resources.S_PAIR_ERR_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true)); + return false; + } + }).ContinueWith(t => + { + if (t.IsCanceled) + return; + + App.Current.Dispatcher.Invoke(() => + { + if (t.Result) + ConnectNewDevice(); + + Data.RuntimeSettings.ConnectNewDevice = null; + Data.RuntimeSettings.IsManualPairingInProgress = false; + }); + }); + } + + public static async void ConnectNewDevice() + { + var dev = (NewDeviceViewModel)Data.RuntimeSettings.ConnectNewDevice; + await Task.Run(() => + { + try + { + ADBService.ConnectNetworkDevice(dev.ConnectAddress); + return true; + } + catch (Exception ex) + { + if (AdbExplorerConst.LOOPBACK_ADDRESSES.Contains(dev.IpAddress)) + return true; + + if (ex.Message.Contains(Strings.Resources.S_FAILED_CONN + dev.ConnectAddress) + && !((NewDeviceViewModel)Data.RuntimeSettings.ConnectNewDevice).IsPairingEnabled) + { + Data.DevicesObject.CurrentNewDevice.EnablePairing(); + } + else + App.Current.Dispatcher.Invoke(() => DialogService.ShowMessage(ex.Message, Strings.Resources.S_FAILED_CONN_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true)); + + return false; + } + }).ContinueWith(t => + { + if (t.IsCanceled) + return; + + App.Current.Dispatcher.Invoke(() => + { + if (t.Result) + { + string newDeviceAddress = ""; + var newDevice = Data.RuntimeSettings.ConnectNewDevice is null ? Data.DevicesObject.CurrentNewDevice : Data.RuntimeSettings.ConnectNewDevice; + + if (newDevice.Type is DeviceType.New && !AdbExplorerConst.LOOPBACK_ADDRESSES.Contains(newDevice.IpAddress)) + { + if (Data.Settings.SaveDevices) + Data.DevicesObject.AddHistoryDevice(HistoryDeviceViewModel.New(dev)); + + newDeviceAddress = dev.ConnectAddress; + ((NewDeviceViewModel)newDevice).ClearDevice(); + } + else if (newDevice.Type is DeviceType.History) + { + newDeviceAddress = ((HistoryDeviceViewModel)newDevice).ConnectAddress; + + // In case user has changed the port of the history device + if (Data.Settings.SaveDevices) + Data.DevicesObject.StoreHistoryDevices(); + } + + DeviceListSetup(newDeviceAddress); + } + + Data.RuntimeSettings.ConnectNewDevice = null; + Data.RuntimeSettings.IsManualPairingInProgress = false; + }); + }); + } + + public static IEnumerable ReconnectFileOpDevice(IEnumerable devices) + { + var pastOps = Data.FileOpQ.Operations.Where(op => op.IsPastOp); + + // get the newly acquired devices with similar IDs to devices of the past file ops [the objects of] which also do not exist in the devices UI list + var exceptDevices = devices.Where(d => pastOps.Any(op => op.Device.ID == d.ID && !Data.DevicesObject.UIList.Contains(op.Device.Device))); + + // get the corresponding file op devices + var fileOpDevices = pastOps.Select(op => op.Device.Device).Where(d => exceptDevices.Any(e => e.ID == d.ID)); + + return devices.Except(exceptDevices, new LogicalDeviceViewModelEqualityComparer()).AppendRange(fileOpDevices.Distinct()); + } + + public static void DeviceListSetup(string selectedAddress = "") + { + Task.Run(ADBService.GetDevices).ContinueWith((t) => App.Current.Dispatcher.Invoke(() => DeviceListSetup(t.Result.Select(l => new LogicalDeviceViewModel(l)), selectedAddress))); + } + + public static void DeviceListSetup(IEnumerable devices, string selectedAddress = "") + { + devices = ReconnectFileOpDevice(devices); + Data.DevicesObject.UpdateDevices(devices); + Data.RuntimeSettings.FilterDevices = true; + + if (Data.DevicesObject.Current is null || Data.DevicesObject.Current.IsOpen && Data.DevicesObject.Current.Status is not DeviceStatus.Ok) + { + DriveHelper.ClearDrives(); + Devices.SetOpenDevice(null); + } + + if (Data.DevicesObject.DevicesAvailable(true)) + return; + + Devices.SetOpenDevice(null); + + App.Current.Dispatcher.Invoke(Data.CopyPaste.GetClipboardPasteItems); + + FileActionLogic.ClearExplorer(); + Data.FileActions.IsExplorerVisible = false; + + NavHistory.Reset(); + DriveHelper.ClearDrives(); + + if (string.IsNullOrEmpty(selectedAddress)) + { + if (!Data.Settings.AutoOpen) + return; + } + else + { + if (!Data.DevicesObject.SetOpenDevice(selectedAddress)) + return; + } + + if (!devices.Any() && Data.DevicesObject.Current is null) + return; + + var startTime = DateTime.Now; + LogicalDeviceViewModel device; + + if (Data.DevicesObject.Current is null) + { + if (string.IsNullOrEmpty(Data.Settings.LastDevice)) + device = devices.First(); + else + device = devices.FirstOrDefault(d => d.Name == Data.Settings.LastDevice); + } + else + device = Data.DevicesObject.Current; + + Task.Run(() => + { + if (device is null) + return false; + + while (device.Status is not DeviceStatus.Ok) + { + if (DateTime.Now - startTime > TimeSpan.FromSeconds(6)) + return false; + + Thread.Sleep(500); + } + return true; + }).ContinueWith(t => App.Current.Dispatcher.Invoke(() => + { + if (!t.Result) + return; + + Devices.SetOpenDevice(device); + Data.CurrentADBDevice = new(Data.DevicesObject.Current); + Data.RuntimeSettings.InitLister = true; + })); + } + + public static void InitDevice() + { + SetAndroidVersion(); + FileActionLogic.RefreshDrives(true); + + FolderHelper.CombineDisplayNames(); + Data.RuntimeSettings.DriveViewNav = true; + NavHistory.Navigate(Navigation.SpecialLocation.DriveView); + + Data.CopyPaste.GetClipboardPasteItems(); + Data.RuntimeSettings.FilterDrives = true; + + Data.RuntimeSettings.CurrentDevice = Data.DevicesObject.Current; + Data.FileActions.PushPackageEnabled = Data.Settings.EnableApk && Data.DevicesObject?.Current?.Type is not DeviceType.Recovery; + + Data.FileOpQ.MoveOperationsToPast(); + FileActionLogic.UpdateFileActions(); + } + + public static void TestDevices() + { + //ConnectTimer.IsEnabled = false; + + //DevicesObject.UpdateServices(new List() { new PairingService("sdfsdfdsf_adb-tls-pairing._tcp.", "192.168.1.20", "5555") { MdnsType = ServiceDevice.ServiceType.PairingCode } }); + //DevicesObject.UpdateDevices(new List() { LogicalDevice.New("Test", "test.ID", "device") }); + } + + public static void SetAndroidVersion() + { + var versionTask = Task.Run(Data.CurrentADBDevice.GetAndroidVersion); + versionTask.ContinueWith(t => + { + if (t.IsCanceled) + return; + + App.Current.Dispatcher.Invoke(() => + { + Data.DevicesObject.Current.SetAndroidVersion(t.Result); + }); + }); + } + + public static void ConnectDevice(DeviceViewModel device) + { + Data.RuntimeSettings.IsManualPairingInProgress = true; + Data.DevicesObject.CurrentNewDevice = (NewDeviceViewModel)device; + + if (device is NewDeviceViewModel newDevice && newDevice.IsPairingEnabled) + PairNewDevice(); + else + ConnectNewDevice(); + } + + public static void RemoveDevice(DeviceViewModel device) + { + switch (device) + { + case LogicalDeviceViewModel logical: + if (logical.IsOpen) + { + DriveHelper.ClearDrives(); + FileActionLogic.ClearExplorer(); + NavHistory.Reset(); + Data.FileActions.IsExplorerVisible = false; + Data.CurrentADBDevice = null; + Data.DirList = null; + Data.RuntimeSettings.DeviceToOpen = null; + } + + Data.DevicesObject.UIList.Remove(device); + Data.RuntimeSettings.FilterDevices = true; + DeviceListSetup(); + break; + case HistoryDeviceViewModel hist: + Data.DevicesObject.RemoveHistoryDevice(hist); + break; + default: + throw new NotSupportedException(); + } + } + + public static void OpenDevice(LogicalDeviceViewModel device) + { + Data.CurrentADBDevice = new(device); + Devices.SetOpenDevice(device); + + App.Services.GetService()?.Navigate(typeof(ExplorerPage)); + Data.RuntimeSettings.InitLister = true; + + FileActionLogic.ClearExplorer(); + NavHistory.Reset(); + InitDevice(); + + Data.RuntimeSettings.IsDevicesView = false; + } + + public static void ConnectWsaDevice() + { + if (Data.DevicesObject.UIList.OfType().Any(wsa => wsa.Status is not DeviceStatus.Unauthorized)) + return; + + if (Data.DevicesObject.LogicalDeviceViewModels.Any(dev => dev.Type is DeviceType.WSA && dev.Status is not DeviceStatus.Offline)) + return; + + var wsaPid = GetWsaPid(); + if (wsaPid is null) + return; + + var wsaIp = Network.GetWsaIp(); + if (wsaIp is null) + return; + + var retCode = ADBService.ExecuteCommand("cmd.exe", + "/C", + out string stdout, + out _, + Encoding.UTF8, + CancellationToken.None, "\"netstat", "-nao", "|", "findstr", $"{wsaPid.Value}\""); + + if (retCode != 0) + return; + + var match = AdbRegEx.RE_NETSTAT_TCP_SOCK().Match(stdout); + if (match.Groups?.Count < 2) + return; + + var netstatIp = match.Groups["IP"].Value; + Data.DevicesObject.WsaPort = match.Groups["Port"].Value; + if (!AdbExplorerConst.LOOPBACK_ADDRESSES.Contains(netstatIp)) + return; + + Data.DevicesObject.CurrentNewDevice = new(new()) + { + IpAddress = AdbExplorerConst.WIN_LOOPBACK_ADDRESS, + ConnectPort = Data.DevicesObject.WsaPort, + }; + Data.DevicesObject.CurrentNewDevice.ConnectCommand.Execute(); + } + + public static int? GetWsaPid() => + Process.GetProcessesByName(AdbExplorerConst.WSA_PROCESS_NAME).FirstOrDefault()?.Id; + + private static bool IsWsaInstalled() => + new PackageManager().FindPackagesForUser("")?.Any(pkg => pkg.DisplayName.Contains(AdbExplorerConst.WSA_PACKAGE_NAME)) + is true; + + public static void UpdateWsaPkgStatus() + { + var wsa = Data.DevicesObject.UIList.OfType().FirstOrDefault(); + if (wsa is null) + return; + + if (Data.DevicesObject.LogicalDeviceViewModels.Any(dev => dev.Type is DeviceType.WSA && dev.Status is not DeviceStatus.Offline)) + return; + + if (wsa.LastLaunch == DateTime.MaxValue || DateTime.Now - wsa.LastLaunch < AdbExplorerConst.WSA_LAUNCH_DELAY) + return; + + DeviceStatus newStatus; + var oldStatus = wsa.Status; + + if (oldStatus is DeviceStatus.Unauthorized && DateTime.Now - wsa.LastLaunch > AdbExplorerConst.WSA_CONNECT_TIMEOUT) + { + if (wsa.LastLaunch == DateTime.MinValue) + { + wsa.SetLastLaunch(); + return; + } + + newStatus = DeviceStatus.Ok; + wsa.SetLastLaunch(DateTime.MaxValue); + } + else + { + if (GetWsaPid() is not null) + newStatus = DeviceStatus.Unauthorized; + else if (IsWsaInstalled()) + newStatus = DeviceStatus.Ok; + else + newStatus = DeviceStatus.Offline; + } + + if (newStatus != oldStatus) + { + App.Current.Dispatcher.Invoke(() => wsa.SetStatus(newStatus)); + Data.RuntimeSettings.FilterDevices = true; + } + } +} diff --git a/ADB Explorer _WpfUi/Helpers/DriveHelper.cs b/ADB Explorer _WpfUi/Helpers/DriveHelper.cs new file mode 100644 index 00000000..60cf45d5 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/DriveHelper.cs @@ -0,0 +1,31 @@ +using ADB_Explorer.Models; +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Helpers; + +internal class DriveHelper +{ + public static void ClearSelectedDrives() + { + Data.RuntimeSettings.CollapseDrives = true; + Data.RuntimeSettings.CollapseDrives = false; + } + + public static void ClearDrives() + { + Data.DevicesObject.Current?.Drives.Clear(); + Data.FileActions.IsDriveViewVisible = false; + } + + public static DriveViewModel GetCurrentDrive(string path) + { + if (string.IsNullOrEmpty(path)) return null; + + // First search for a non-root drive that matches the path + var nonRoot = Data.DevicesObject.Current?.Drives.FirstOrDefault(d => d.Type is not AbstractDrive.DriveType.Root && path.StartsWith(d.Path)); + if (nonRoot is null) + return Data.DevicesObject.Current?.Drives.FirstOrDefault(d => d.Type is AbstractDrive.DriveType.Root); + + return nonRoot; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/EnumToBooleanConverter.cs b/ADB Explorer _WpfUi/Helpers/EnumToBooleanConverter.cs new file mode 100644 index 00000000..5208736e --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/EnumToBooleanConverter.cs @@ -0,0 +1,37 @@ +using ADB_Explorer.Services; +using System.Globalization; +using System.Windows.Data; +using Wpf.Ui.Appearance; + +namespace ADB_Explorer.Helpers +{ + internal class EnumToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not String enumString) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } + + if (!Enum.IsDefined(typeof(AppSettings.AppTheme), value)) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterValueMustBeAnEnum"); + } + + var enumValue = Enum.Parse(typeof(AppSettings.AppTheme), enumString); + + return enumValue.Equals(value); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not String enumString) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } + + return Enum.Parse(typeof(AppSettings.AppTheme), enumString); + } + } +} diff --git a/ADB Explorer _WpfUi/Helpers/ExplorerHelper.cs b/ADB Explorer _WpfUi/Helpers/ExplorerHelper.cs new file mode 100644 index 00000000..c3673e06 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/ExplorerHelper.cs @@ -0,0 +1,26 @@ +using static Vanara.PInvoke.Shell32; + +namespace ADB_Explorer.Helpers; + +public class ExplorerHelper +{ + public static bool NotifyFileCreated(string path) + { + var hPath = (nuint)Marshal.StringToHGlobalUni(path); + bool result = false; + + try + { + SHChangeNotify(SHCNE.SHCNE_CREATE, SHCNF.SHCNF_PATHW, hPath); + result = true; + } + catch + { } + finally + { + Marshal.FreeHGlobal((nint)hPath); + } + + return result; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/File/FileHelper.cs b/ADB Explorer _WpfUi/Helpers/File/FileHelper.cs new file mode 100644 index 00000000..690e1cb2 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/File/FileHelper.cs @@ -0,0 +1,384 @@ +using ADB_Explorer.Converters; +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using Vanara.Windows.Shell; +using static ADB_Explorer.Models.AbstractFile; + +namespace ADB_Explorer.Helpers; + +public static class FileHelper +{ + public static FileClass ListerFileManipulator(FileClass item) + { + if (Data.CopyPaste.Files.Length > 0 + && Data.CopyPaste.IsSelfClipboard + && Data.CopyPaste.ParentFolder == Data.DirList.CurrentPath + && Data.CopyPaste.Files.FirstOrDefault(f => f == item.FullPath) is not null) + { + item.CutState = Data.CopyPaste.PasteState; + } + + if (Data.FileActions.IsRecycleBin) + { + var indexer = Data.RecycleIndex.FirstOrDefault(index => index.RecycleName == item.FullName); + if (indexer is not null) + { + item.TrashIndex = indexer; + item.UpdateType(); + } + } + + return item; + } + + public static Predicate HideFiles() => file => + { + if (file is not FileClass fileClass) + return false; + + if (fileClass.IsHidden) + return false; + + return !IsHiddenRecycleItem(fileClass); + }; + + public static Predicate PkgFilter() => pkg => + { + if (pkg is not Package) + return false; + + return string.IsNullOrEmpty(Data.FileActions.ExplorerFilter) + || pkg.ToString().Contains(Data.FileActions.ExplorerFilter, StringComparison.OrdinalIgnoreCase); + }; + + public static bool IsHiddenRecycleItem(FileClass file) + { + if (AdbExplorerConst.POSSIBLE_RECYCLE_PATHS.Contains(file.FullPath) || file.Extension == AdbExplorerConst.RECYCLE_INDEX_SUFFIX) + return true; + + if (!string.IsNullOrEmpty(Data.FileActions.ExplorerFilter) && !file.ToString().Contains(Data.FileActions.ExplorerFilter, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + public static void RenameFile(FileClass file, string newName) + { + var newPath = ConcatPaths(file.ParentPath, newName); + if (!Data.Settings.ShowExtensions) + newPath += file.Extension; + + ShellFileOperation.Rename(file, newPath, Data.CurrentADBDevice); + } + + public static string DisplayName(TextBox textBox) => DisplayName(textBox.DataContext as FilePath); + + public static string DisplayName(FilePath file) => Data.Settings.ShowExtensions ? file?.FullName : file?.NoExtName; + + public static FileClass GetFromCell(DataGridCellInfo cell) => CellConverter.GetDataGridCell(cell).DataContext as FileClass; + + public static string ConcatPaths(FilePath path1, string path2) => + ConcatPaths(path1.FullPath, path2, path1.PathType is FilePathType.Android ? '/' : '\\'); + + public static string ConcatPaths(ShellItem path1, string path2) => + ConcatPaths(path1.FileSystemPath, path2, '\\'); + + public static string ConcatPaths(string path1, string path2, char separator = '/') + { + string result = $"{path1.TrimEnd('/', '\\')}{separator}{path2.TrimStart('/', '\\')}"; + + return result.Replace(separator is '/' ? '\\' : '/', separator); + } + + public static string ExtractRelativePath(string fullPath, string parent, bool includeSelf = true) + { + if (fullPath == parent) + { + return includeSelf + ? GetFullName(fullPath) + : $"{GetSeparator(fullPath)}"; + } + + var index = fullPath.IndexOf(parent); + + var result = index < 0 + ? fullPath + : fullPath[parent.Length..]; + + return result.TrimStart('/', '\\'); + } + + public static string GetParentPath(string fullPath) + { + var index = LastSeparatorIndex(fullPath); + if (index.Value == 0) + index = 1; + + return fullPath[..index]; + } + + public static string GetShortFileName(string fullName, int length = -1) + { + if (string.IsNullOrEmpty(fullName)) + return fullName; + + var name = GetFullName(fullName); + if (length < 0) + return name; + + return name.Length > length ? name[..length] + "…" : name; + } + + public static string GetFullName(string fullPath) + { + if (string.IsNullOrEmpty(fullPath)) + return fullPath; + + var separator = GetSeparator(fullPath); + if (fullPath.IndexOf(separator) == fullPath.Length - 1) + return fullPath; + + fullPath = fullPath.TrimEnd(separator); + var index = LastSeparatorIndex(fullPath); + + return index.IsFromEnd + ? fullPath + : fullPath[(index.Value + 1)..]; + } + + public static char GetSeparator(string path) + { + if (path.Contains('/')) + return '/'; + + return path.Contains('\\') + ? '\\' + : '\0'; + } + + public static Index LastSeparatorIndex(string path) + => IndexAdjust(path.LastIndexOf(GetSeparator(path))); + + public static Index NextSeparatorIndex(string parentPath, string childPath) + => IndexAdjust(childPath.IndexOf(GetSeparator(childPath), parentPath.Length + 1)); + + public static Index IndexAdjust(int originalIndex) => originalIndex switch + { + < 0 => ^0, + _ => originalIndex, + }; + + public static string DirectChildPath(string parentPath, string childPath) + { + if (childPath is null || !childPath.Contains(parentPath) || childPath.Length - parentPath.Length < 2) + return null; + + var index = NextSeparatorIndex(parentPath, childPath); + return childPath[..index]; + } + + public static long TotalSize(IEnumerable files) + { + if (files.Any(f => f.Type is not FileType.File || f.IsLink)) + return 0; + + return files.Select(f => f.Size.GetValueOrDefault(0)).Sum(); + } + + /// + /// Returns the extension (including the period ".").
+ /// Returns an empty string if file has no extension. + ///
+ public static string GetExtension(string fullName) + { + var lastDot = fullName.LastIndexOf('.'); + if (lastDot < 1) + return ""; + + var secondLast = fullName[..lastDot].LastIndexOf('.'); + + if (secondLast > 0 && fullName[(secondLast + 1)..lastDot] == "tar") + return fullName[secondLast..]; + + return fullName[lastDot..]; + } + + public static string DuplicateFile(ObservableList fileList, string fullName, DragDropEffects cutType = DragDropEffects.None) + => DuplicateFile(fileList.Select(f => f.FullName), fullName, cutType); + + public static string DuplicateFile(IEnumerable fileList, string fullName, DragDropEffects cutType = DragDropEffects.None) + { + // None - new file + var copySuffix = cutType is DragDropEffects.None ? "" : Strings.Resources.S_ITEM_COPY.Replace("{0}", ""); + var extension = GetExtension(fullName); + var noExtName = fullName[..^extension.Length]; + + // Has the exact same name before the suffix, optionally has the suffix which is optionally iterated, and has the exact same extension + Regex re = new($"^{noExtName}(?:{copySuffix}(?: (?\\d+))?)?{extension.Replace(".", "\\.")}$"); + + var items = from i in fileList + let match = re.Match(i) + where match.Success + select match.Groups["iter"].Value; + + return $"{noExtName}{ExistingIndexes(items, copySuffix)}{extension}"; + } + + public static string ExistingIndexes(IEnumerable suffixes, string copySuffix = "") + { + var indexes = (from i in suffixes + where int.TryParse(i, out _) + select int.Parse(i)).ToList(); + if (suffixes.Any(s => s == "")) + indexes.Add(0); + + indexes.Sort(); + if (indexes.Count == 0 || indexes[0] != 0) + return ""; + + var result = ""; + for (int i = 0; i < indexes.Count; i++) + { + if (indexes[i] <= i) + continue; + + result = $"{copySuffix} {i}"; + break; + } + if (result == "") + result = $"{copySuffix} {indexes.Count}"; + + return result; + } + + public enum RenameTarget + { + Unix, + FUSE, + Windows, + WinRoot, + } + + private static readonly Func FileNamePredicateWindows = (name) => + !AdbExplorerConst.INVALID_WINDOWS_FILENAMES.Contains(name) + && !name.Any(c => AdbExplorerConst.INVALID_NTFS_CHARS.Any(chr => chr == c)) + && name.Length > 0 + && name[^1] is not ' ' and not '.' + && name[0] is not ' '; + + private static readonly Func FileNamePredicateWinRoot = (name) => + !AdbExplorerConst.INVALID_WINDOWS_ROOT_PATHS.Contains(name) + && !AdbExplorerConst.INVALID_WINDOWS_FILENAMES.Contains(name) + && !name.Any(c => AdbExplorerConst.INVALID_NTFS_CHARS.Any(chr => chr == c)) + && name.Length > 0 + && name[^1] is not ' ' and not '.' + && name[0] is not ' '; + + private static readonly Func FileNamePredicateFuse = (name) => + !name.Any(c => AdbExplorerConst.INVALID_NTFS_CHARS.Contains(c)) + && name.Length > 0 + && name is not "." and not ".."; + + private static readonly Func FileNamePredicateUnix = (name) => + name.Length > 0 + && name is not "." and not ".."; + + public static bool FileNameLegal(string fileName, RenameTarget target) + => FileNameLegal([fileName], target); + + public static bool FileNameLegal(IEnumerable files, RenameTarget target) + => FileNameLegal(files.Select(f => f.FullName), target); + + public static bool FileNameLegal(IEnumerable names, RenameTarget target) + { + var predicate = target switch + { + RenameTarget.Unix => FileNamePredicateUnix, + RenameTarget.FUSE => FileNamePredicateFuse, + RenameTarget.Windows => FileNamePredicateWindows, + RenameTarget.WinRoot => FileNamePredicateWinRoot, + _ => throw new NotSupportedException(), + }; + + return names.AnyAll(predicate); + } + + public static string[] ApkExtensions => [.. AdbExplorerConst.APK_NAMES.Select(n => n[1..])]; + + public static bool AllFilesAreApks(string[] items) => + items.AnyAll(i => i.Contains('.') && ApkExtensions.Any(n => n == i.Split('.').Last().ToUpper())); + + /// + /// Returns the relation of to .
+ /// Example: RelationFrom(File, File.Parent) = Ancestor
+ /// + /// + /// + public static RelationType RelationFrom(string self, string other) + { + var separator = GetSeparator(self); + + if (other == self) + return RelationType.Self; + + if (other.StartsWith(self) && other[self.Length..(self.Length + 1)][0] == separator) + return RelationType.Descendant; + + if (self.StartsWith(other)) + return RelationType.Ancestor; + + return RelationType.Unrelated; + } + + public static IEnumerable GetFilesFromTree((string, long?, double?)[] tree) => + tree.Select(t => new FileClass(GetFullName(t.Item1), t.Item1, t.Item2 is null ? FileType.Folder : FileType.File, size: t.Item2) + { ModifiedTime = t.Item3.FromUnixTime() }); + + public static (string, long?, double?)[] GetFolderTree(IEnumerable paths, bool isFolder = true) + { + string stdout = ""; + var files = string.Join(" ", paths.Select(p => ADBService.EscapeAdbShellString(p))); + var depth = isFolder ? "-mindepth 1" : ""; + + if (ShellCommands.FindPrintf) + { + // get absolute paths, sizes and dates of all files, directries are marked with 'd' + string[] args = + [ + files, + depth, + """\( -type d -printf '/// %p /// d /// d ///\n' \)""", + "-o", + $"""\( -type f -printf '/// %p /// %s /// {(Data.Settings.KeepDateModified ? "%T@" : "d")} ///\n' \)""", + "2>&1" + ]; + + ADBService.ExecuteDeviceAdbShellCommand(Data.CurrentADBDevice.ID, "find", out stdout, out _, CancellationToken.None, args); + } + else // when find does not support -printf + { + // gives the same result, but much slower since it executes stat on each file *after* performing find + string[] args = + [ + files, + depth, + """2>/dev/null | while IFS= read -r f;""", + """do if [ -d \"$f\" ]; then""", + """echo /// $f /// d /// d ///;""", + $"""else echo /// $f /// $(stat -c '%s /// %Y' {(Data.Settings.KeepDateModified ? "\\\"$f\\\")" : ") d")} ///;""", + """fi; done;""" + ]; + + ADBService.ExecuteDeviceAdbShellCommand(Data.CurrentADBDevice.ID, "find", out stdout, out _, CancellationToken.None, args); + } + var matches = AdbRegEx.RE_FIND_TREE().Matches(stdout); + + return [.. matches.Where(m => m.Success) + .Select(m => + ( + m.Groups["Name"].Value, + m.Groups["Size"].Value == "d" ? (long?)null : long.Parse(m.Groups["Size"].Value, CultureInfo.InvariantCulture), + m.Groups["Date"].Value == "d" ? (double?)null : double.Parse(m.Groups["Date"].Value, CultureInfo.InvariantCulture) + ))]; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/File/FileToIcon.cs b/ADB Explorer _WpfUi/Helpers/File/FileToIcon.cs new file mode 100644 index 00000000..88d66ca0 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/File/FileToIcon.cs @@ -0,0 +1,333 @@ +// Part of FileToIcon from Code Project article by Leung Yat Chun +// https://www.codeproject.com/Articles/32059/WPF-Filename-To-Icon-Converter +// Used and modified under the LGPLv3 license + +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using System.Drawing; +using static Services.NativeMethods; + +namespace ADB_Explorer.Helpers; + +public class FileToIconConverter +{ + public enum IconSize : uint + { + Large, + Small, + ExtraLarge, + Jumbo, + Thumbnail, + } + + private const int FOLDER_ICON_INDEX = 3; + private const int LINK_OVERLAY_INDEX = 29; + private const int UNKNOWN_ICON_INDEX = 175; + private const int BROKEN_LINK_ICON_INDEX = 271; + + private static readonly Dictionary iconDic = []; + private static readonly SysImageList _imgList = new(SysImageListSize.SHIL_JUMBO); + + // + /// Return large file icon of the specified file. + /// + private static Icon GetFileIcon(string fileName, IconSize size) + { + var flags = NativeMethods.FileInfoFlags.SHGFI_SYSICONINDEX; + + if (!fileName.Contains(':')) + flags = flags | NativeMethods.FileInfoFlags.SHGFI_USEFILEATTRIBUTES; + + if (size == IconSize.Small) + flags = flags | NativeMethods.FileInfoFlags.SHGFI_SMALLICON; + + return NativeMethods.GetIcon(fileName, flags); + } + private static Icon GetIconFromIndex(int index, IconSize size) + => NativeMethods.ExtractIconByIndex("Shell32.dll", index, size); + + private static Bitmap ResizeImage(Bitmap imgToResize, System.Drawing.Size size, int spacing, bool addBorder = true) + { + int destWidth = imgToResize.Width; + int destHeight = imgToResize.Height; + + int leftOffset = (size.Width - destWidth) / 2; + int topOffset = (size.Height - destHeight) / 2; + + Bitmap b = new Bitmap(size.Width, size.Height); + Graphics g = Graphics.FromImage(b); + g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High; + + var Gray222 = System.Drawing.Color.FromArgb(222, 222, 222); + var Gray225 = System.Drawing.Color.FromArgb(225, 225, 225); + var Gray232 = System.Drawing.Color.FromArgb(232, 232, 232); + var Gray244 = System.Drawing.Color.FromArgb(244, 244, 244); + + if (addBorder) + { + g.DrawRectangle(new System.Drawing.Pen(Gray232), + spacing + 1, + spacing + 1, + size.Width - (spacing + 1) * 2 - 1, + size.Height - (spacing + 1) * 2 - 1); + + g.DrawRectangle(new System.Drawing.Pen(Gray222), + spacing, + spacing, + size.Width - spacing * 2 - 1, + size.Height - spacing * 2 - 1); + } + + g.DrawImage(imgToResize, leftOffset, topOffset, destWidth, destHeight); + g.Dispose(); + + if (addBorder) + { + b.SetPixel(spacing, spacing, Gray244); + b.SetPixel(spacing, size.Height - 1, Gray244); + b.SetPixel(size.Width - 1, spacing, Gray244); + b.SetPixel(size.Width - 1, size.Height - 1, Gray244); + + b.SetPixel(spacing + 1, spacing + 1, Gray225); + b.SetPixel(spacing + 1, size.Height - 2, Gray225); + b.SetPixel(size.Width - 2, spacing + 1, Gray225); + b.SetPixel(size.Width - 2, size.Height - 2, Gray225); + } + + return b; + } + + private static BitmapSource LoadBitmap(Bitmap source) + { + var hBitmap = source.GetHbitmap(); + //Memory Leak fixes, for more info : http://social.msdn.microsoft.com/forums/en-US/wpf/thread/edcf2482-b931-4939-9415-15b3515ddac6/ + try + { + return Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + } + finally + { + NativeMethods.MDeleteObject(hBitmap); + } + } + + private static string ReturnKey(string fileName, IconSize size, AbstractFile.SpecialFileType specialType, bool bitmapSource = true) + { + string key; + + if (specialType.HasFlag(AbstractFile.SpecialFileType.Regular)) + key = Path.GetExtension(fileName).ToLower(); + else + key = $"#{Enum.GetName(specialType).ToUpper()}#"; + + var sizeKey = size switch + { + IconSize.Jumbo or IconSize.Thumbnail => "J", + IconSize.ExtraLarge => "XL", + IconSize.Large => "L", + IconSize.Small => "S", + _ => "", + }; + + return $"{key}+{sizeKey}+{(bitmapSource ? "Src" : "Bmp")}"; + } + + private static Bitmap LoadJumbo(int index, int desiredSize) + { + // Used to contain code to support OSs before Windows Vista + // ADB Explorer requires at least Windows 10 build 18362 + + _imgList.ImageListSize = SysImageListSize.SHIL_JUMBO; + Icon icon = _imgList.Icon(index); + Bitmap bitmap = icon.ToBitmap(); + icon.Dispose(); + + var usable = FindUsableSize(bitmap); + if (usable is SysImageListSize.SHIL_JUMBO) + { + // we are unable to downscale here, so it will be handled in the UI + bitmap = ResizeImage(bitmap, new System.Drawing.Size(256, 256), 0, false); + } + else + { + _imgList.ImageListSize = usable; + bitmap = ResizeImage(_imgList.Icon(index).ToBitmap(), new System.Drawing.Size(desiredSize, desiredSize), 0); + } + + return bitmap; + } + + private static Bitmap LoadJumbo(string lookup, int desiredSize) + { + _imgList.ImageListSize = SysImageListSize.SHIL_JUMBO; + return LoadJumbo(_imgList.IconIndex(lookup), desiredSize); + } + + private static SysImageListSize FindUsableSize(Bitmap bitmap) + { + System.Drawing.Color empty = System.Drawing.Color.FromArgb(0, 0, 0, 0); + int ValidColumns = 0; + + for (int i = 0; i < bitmap.Width; i++) + { + int validPixels = 0; + for (int j = 0; j < bitmap.Height; j++) + { + if (bitmap.GetPixel(i, j) != empty) + validPixels++; + } + if (validPixels > 0) + ValidColumns++; + } + + return ValidColumns switch + { + > 48 => SysImageListSize.SHIL_JUMBO, + > 32 => SysImageListSize.SHIL_EXTRALARGE, + > 16 => SysImageListSize.SHIL_LARGE, + _ => SysImageListSize.SHIL_SMALL, + }; + } + + private static T AddToDic(string fileName, IconSize size, int desiredSize, AbstractFile.SpecialFileType specialType = AbstractFile.SpecialFileType.Regular) + { + var bitmapSource = typeof(T) == typeof(BitmapSource); + var key = ReturnKey(fileName, size, specialType, bitmapSource); + + if (!iconDic.ContainsKey(key)) + lock (iconDic) + iconDic.Add(key, bitmapSource + ? GetImage(fileName, size, desiredSize, specialType) + : GetBitmap(fileName, size, desiredSize, specialType)); + + return (T)iconDic[key]; + } + + private static T AddToDic(Icon icon, IconSize size, AbstractFile.SpecialFileType specialType) + { + var bitmapSource = typeof(T) == typeof(BitmapSource); + var key = ReturnKey("", size, specialType); + + if (!iconDic.ContainsKey(key)) + lock (iconDic) + iconDic.Add(key, bitmapSource + ? LoadBitmap(icon.ToBitmap()) + : icon.ToBitmap()); + + return (T)iconDic[key]; + } + + private static BitmapSource GetImage(string fileName, IconSize size, int desiredSize, AbstractFile.SpecialFileType specialType = AbstractFile.SpecialFileType.Regular) + => LoadBitmap(GetBitmap(fileName, size, desiredSize, specialType)); + + private static Bitmap GetBitmap(string fileName, IconSize size, int desiredSize, AbstractFile.SpecialFileType specialType = AbstractFile.SpecialFileType.Regular) + { + Icon icon; + var lookup = !ReturnKey(fileName, size, specialType).StartsWith('.') + ? fileName + : $"aaa{Path.GetExtension(fileName).ToLower()}"; + + var specialIndex = SpecialTypeIndex(specialType); + + switch (size) + { + case IconSize.Jumbo or IconSize.Thumbnail: + return specialIndex < 0 + ? LoadJumbo(lookup, desiredSize) + : LoadJumbo(specialIndex, desiredSize); + + case IconSize.ExtraLarge: + _imgList.ImageListSize = SysImageListSize.SHIL_EXTRALARGE; + icon = _imgList.Icon(specialIndex < 0 ? _imgList.IconIndex(lookup) : specialIndex); + + return icon.ToBitmap(); + + default: + icon = specialIndex < 0 ? GetFileIcon(lookup, size) : GetIconFromIndex(specialIndex, size); + + return icon.ToBitmap(); + } + } + + private static int SpecialTypeIndex(AbstractFile.SpecialFileType specialType) + => specialType switch + { + AbstractFile.SpecialFileType.Folder => FOLDER_ICON_INDEX, + AbstractFile.SpecialFileType.BrokenLink => BROKEN_LINK_ICON_INDEX, + AbstractFile.SpecialFileType.Unknown => UNKNOWN_ICON_INDEX, + AbstractFile.SpecialFileType.LinkOverlay => LINK_OVERLAY_INDEX, + _ => -1, + }; + + private static System.Drawing.Size IconToSize(IconSize size) => size switch + { + IconSize.Small => new(16, 16), + IconSize.Large => new(32, 32), + IconSize.ExtraLarge => new(48, 48), + IconSize.Jumbo or IconSize.Thumbnail => new(256, 256), + _ => throw new NotSupportedException(), + }; + + public static BitmapSource GetImage(string fileName, int iconSize, AbstractFile.SpecialFileType specialType = AbstractFile.SpecialFileType.Regular) + { + IconSize size = iconSize switch + { + <= 16 => IconSize.Small, + <= 32 => IconSize.Large, + <= 48 => IconSize.ExtraLarge, + _ => IconSize.Jumbo, + }; + + return AddToDic(fileName, size, iconSize, specialType); + } + + public static IEnumerable GetImage(FilePath file, bool smallIcon = true) + { + var size = smallIcon ? IconSize.Small : IconSize.Jumbo; + var specialType = file.SpecialType; + + if (specialType is 0) + yield break; + + if (specialType.HasFlag(AbstractFile.SpecialFileType.Apk)) + { + Icon apkIcon = new(Properties.AppGlobal.APK_icon, IconToSize(size)); + + yield return AddToDic(apkIcon, size, AbstractFile.SpecialFileType.Apk); + } + else + { + // Get icon without link overlay + yield return AddToDic(file.FullName, size, smallIcon ? 16 : 96, specialType & ~AbstractFile.SpecialFileType.LinkOverlay); + } + + if (specialType.HasFlag(AbstractFile.SpecialFileType.LinkOverlay)) + { + // Get link overlay if required + yield return AddToDic(file.FullName, size, smallIcon ? 16 : 96, AbstractFile.SpecialFileType.LinkOverlay); + } + } + + private static T GetImage(FilePath file) + { + var specialType = file.SpecialType; + if (specialType.HasFlag(AbstractFile.SpecialFileType.Apk)) + { + Icon apkIcon = new(Properties.AppGlobal.APK_icon, IconToSize(IconSize.Jumbo)); + + return AddToDic(apkIcon, IconSize.Jumbo, AbstractFile.SpecialFileType.Apk); + } + else + { + // Get icon without link overlay + return AddToDic(file.FullName, IconSize.Jumbo, 96, specialType & ~AbstractFile.SpecialFileType.LinkOverlay); + } + } + + public static Bitmap GetBitmap(FilePath file) + => GetImage(file); + + public static BitmapSource GetBitmapSource(FilePath file) + => GetImage(file); +} diff --git a/ADB Explorer _WpfUi/Helpers/File/FolderHelper.cs b/ADB Explorer _WpfUi/Helpers/File/FolderHelper.cs new file mode 100644 index 00000000..61c31cc4 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/File/FolderHelper.cs @@ -0,0 +1,128 @@ +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; +using Vanara.Windows.Shell; + +namespace ADB_Explorer.Helpers; + +public static class FolderHelper +{ + public static void CombineDisplayNames() + { + var driveView = AdbLocation.StringFromLocation(Navigation.SpecialLocation.DriveView); + if (Data.CurrentDisplayNames.ContainsKey(driveView)) + Data.CurrentDisplayNames[driveView] = Data.DevicesObject.Current.Name; + else + Data.CurrentDisplayNames.Add(driveView, Data.DevicesObject.Current.Name); + + foreach (var drive in Data.DevicesObject.Current.Drives.OfType().Where(d => d.Type + is not AbstractDrive.DriveType.Root + and not AbstractDrive.DriveType.Internal)) + { + Data.CurrentDisplayNames.TryAdd(drive.Path, drive.Type is AbstractDrive.DriveType.External + ? drive.ID : drive.DisplayName); + } + + foreach (var item in AdbExplorerConst.DRIVE_TYPES.Where(d => d.Value is AbstractDrive.DriveType.Root or AbstractDrive.DriveType.Internal)) + { + Data.CurrentDisplayNames.TryAdd(item.Key, AbstractDrive.GetDriveDisplayName(item.Value)); + } + + foreach (var item in AdbExplorerConst.DRIVE_TYPES) + { + var names = Enum.GetValues().Where(n => n == item.Value && item.Value + is not AbstractDrive.DriveType.Root + and not AbstractDrive.DriveType.Internal) + .Select(AbstractDrive.GetDriveDisplayName); + + if (names.Any()) + Data.CurrentDisplayNames.TryAdd(item.Key, names.First()); + } + + Data.RuntimeSettings.RefreshBreadcrumbs = true; + } + + public static string FolderExists(string path) + { + if (path == AdbLocation.StringFromLocation(Navigation.SpecialLocation.PackageDrive)) + return path; + + if (path == AdbLocation.StringFromLocation(Navigation.SpecialLocation.RecycleBin)) + return AdbExplorerConst.RECYCLE_PATH; + + try + { + return Data.CurrentADBDevice.TranslateDevicePath(path); + } + catch (Exception e) + { + if (path != AdbExplorerConst.RECYCLE_PATH) + DialogService.ShowMessage(e.Message, Strings.Resources.S_NAV_ERR_TITLE, DialogService.DialogIcon.Critical, copyToClipboard: true); + + return null; + } + } + + public static IEnumerable GetEmptySubfoldersRecursively(ShellFolder rootFolder) + { + var emptyFolders = new List(); + FindEmptySubfolders(rootFolder, emptyFolders); + return emptyFolders.Where(f => f.ParsingName != rootFolder.ParsingName); + } + + private static bool FindEmptySubfolders(ShellFolder folder, List result) + { + if (!folder.IsFolder) return false; + + bool hasNonFolder = false; + bool hasNonEmptyFolder = false; + + foreach (var child in folder) + { + if (!child.IsFolder) + { + hasNonFolder = true; + continue; + } + + // Recurse into subfolder + if (!FindEmptySubfolders((ShellFolder)child, result)) + { + hasNonEmptyFolder = true; + } + } + + bool isEmpty = !hasNonFolder && !hasNonEmptyFolder && !folder.Any(); + + if (isEmpty) + { + result.Add(folder); + return true; + } + + return false; + } + + public static bool IsNonArchiveFolder(this ShellItem self) + { + // A regular file has IsFolder = false, so we get a short-circuit here. + // But an archive file (e.g. .zip) has IsFolder = true as well, so we need to check the attributes too. + return self.IsFolder && self.FileInfo?.Attributes.HasFlag(FileAttributes.Directory) is true; + } + + /// + /// Retrieves the bottom-most folders from a collection of files. + /// + /// A "bottom-most folder" is defined as a directory that does not contain any other directory + /// from the provided collection as a descendant. This method filters out parent directories to return only the + /// deepest-level directories in the hierarchy. + /// A collection of objects to evaluate. Each object represents a file or directory. + /// An enumerable collection of objects representing directories that are not ancestors of + /// any other directory in the collection. + public static IEnumerable GetBottomMostFolders(IEnumerable files) + { + var dirs = files.Where(f => f.IsDirectory); + + return dirs.Where(dir => !dirs.Any(f => dir.RelationFrom(f) is AbstractFile.RelationType.Descendant)); + } +} diff --git a/ADB Explorer _WpfUi/Helpers/File/TrashHelper.cs b/ADB Explorer _WpfUi/Helpers/File/TrashHelper.cs new file mode 100644 index 00000000..f4bf4c01 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/File/TrashHelper.cs @@ -0,0 +1,62 @@ +using ADB_Explorer.Models; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Helpers; + +internal static class TrashHelper +{ + public static void EnableRecycleButtons(IEnumerable fileList = null) + { + if (fileList is null) + fileList = Data.DirList.FileList; + + Data.FileActions.RestoreEnabled = fileList.Any(file => file.TrashIndex is not null && !string.IsNullOrEmpty(file.TrashIndex.OriginalPath)); + Data.FileActions.DeleteEnabled = fileList.Any(item => item.Extension != AdbExplorerConst.RECYCLE_INDEX_SUFFIX); + } + + public static void UpdateRecycledItemsCount() + { + var countTask = Task.Run(() => ADBService.CountRecycle(Data.DevicesObject.Current.ID)); + countTask.ContinueWith((t) => + { + if (t.IsCanceled || Data.DevicesObject.Current is null) + return; + + var count = t.Result; + if (count < 1) + count = FolderHelper.FolderExists(AdbExplorerConst.RECYCLE_PATH) is null ? -1 : 0; + + var trash = Data.DevicesObject.Current?.Drives.Find(d => d.Type is AbstractDrive.DriveType.Trash); + App.Current.Dispatcher.Invoke(() => ((VirtualDriveViewModel)trash)?.SetItemsCount(count)); + }); + } + + public static Task ParseIndexersAsync() => Task.Run(() => + { + Data.RecycleIndex.Clear(); + + var indexers = ADBService.FindFilesInPath(Data.CurrentADBDevice.ID, + AdbExplorerConst.RECYCLE_PATH, + includeNames: ["*" + AdbExplorerConst.RECYCLE_INDEX_SUFFIX]); + + var lines = ShellFileOperation.ReadAllText(Data.CurrentADBDevice, indexers).Split(ADBService.LINE_SEPARATORS, + StringSplitOptions.RemoveEmptyEntries); + + lines.ToList().ForEach(line => Data.RecycleIndex.Add(new(line))); + }); + + public static void ParseIndexers() + { + Data.RecycleIndex.Clear(); + + var indexers = ADBService.FindFilesInPath(Data.CurrentADBDevice.ID, + AdbExplorerConst.RECYCLE_PATH, + includeNames: ["*" + AdbExplorerConst.RECYCLE_INDEX_SUFFIX]); + + var lines = ShellFileOperation.ReadAllText(Data.CurrentADBDevice, indexers).Split(ADBService.LINE_SEPARATORS, + StringSplitOptions.RemoveEmptyEntries); + + lines.ToList().ForEach(line => Data.RecycleIndex.Add(new(line))); + } +} diff --git a/ADB Explorer _WpfUi/Helpers/QrGenerator.cs b/ADB Explorer _WpfUi/Helpers/QrGenerator.cs new file mode 100644 index 00000000..6437c41c --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/QrGenerator.cs @@ -0,0 +1,17 @@ +using QRCoder; +using QRCoder.Xaml; + +namespace ADB_Explorer.Helpers; + +public static class QrGenerator +{ + public static DrawingImage GenerateQR(string val, SolidColorBrush background, SolidColorBrush foreground) + { + QRCodeGenerator qrGenerator = new QRCodeGenerator(); + QRCodeData qrCodeData = qrGenerator.CreateQrCode(val, QRCodeGenerator.ECCLevel.Q); + var qrCode = new XamlQRCode(qrCodeData); + var image = qrCode.GetGraphic(new System.Windows.Size(256, 256), foreground, background, false); + + return image; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/TabularDateFormatter.cs b/ADB Explorer _WpfUi/Helpers/TabularDateFormatter.cs new file mode 100644 index 00000000..88abf6e9 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TabularDateFormatter.cs @@ -0,0 +1,95 @@ +namespace ADB_Explorer.Helpers; + +public static partial class TabularDateFormatter +{ + /// + /// Formats a DateTime using the given culture's date/time format, + /// but ensures zero-padding for consistent column width. + /// + public static string Format(DateTime? dateTime, CultureInfo culture) + { + if (dateTime is null) + return string.Empty; + + // Build combined pattern from ShortDate and LongTime patterns + string pattern = culture.DateTimeFormat.ShortDatePattern + + " " + culture.DateTimeFormat.LongTimePattern; + + // Normalize to tabular pattern + pattern = PadDateTimePattern(pattern); + + // Format the DateTime + string result = dateTime?.ToString(pattern, culture); + + // Remove RTL marks that might appear in RTL cultures + result = RemoveRtlMarks(result); + + return result; + } + + /// + /// Formats a TimeOnly using the given culture's date/time format, + /// but ensures zero-padding for consistent column width. + /// + public static string Format(TimeOnly? dateTime, CultureInfo culture) + { + if (dateTime is null) + return string.Empty; + + // Build combined pattern from ShortDate and LongTime patterns + string pattern = culture.DateTimeFormat.LongTimePattern; + + // Normalize to tabular pattern + pattern = PadDateTimePattern(pattern); + + // Format the DateTime + string result = dateTime?.ToString(pattern, culture); + + // Remove RTL marks that might appear in RTL cultures + result = RemoveRtlMarks(result); + + return result; + } + + /// + /// Replaces single-character date/time format tokens (d, M, H, h, m, s) + /// with their padded versions (dd, MM, etc.), leaving multi-letter tokens intact. + /// + private static string PadDateTimePattern(string pattern) + { + pattern = RE_Replace_dd().Replace(pattern, "dd"); + pattern = RE_Replace_MM().Replace(pattern, "MM"); + pattern = RE_Replace_HH().Replace(pattern, "HH"); + pattern = RE_Replace_hh().Replace(pattern, "hh"); + pattern = RE_Replace_mm().Replace(pattern, "mm"); + pattern = RE_Replace_ss().Replace(pattern, "ss"); + + return pattern; + } + + /// + /// Removes invisible Right-to-Left and Left-to-Right marks that can affect layout. + /// + private static string RemoveRtlMarks(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + return input + .Replace("\u200F", "") + .Replace("\u200E", ""); + } + + [GeneratedRegex(@"(? item switch + { + LogicalDeviceViewModel => LogicalDeviceTemplate, + ServiceDeviceViewModel => ServiceDeviceTemplate, + HistoryDeviceViewModel => HistoryDeviceTemplate, + NewDeviceViewModel => NewDeviceTemplate, + WsaPkgDeviceViewModel => WsaPkgDeviceTemplate, + MdnsDeviceViewModel => MdnsDeviceTemplate, + _ => throw new NotImplementedException(), + }; +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/DriveTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/DriveTemplateSelector.cs new file mode 100644 index 00000000..7d3f3993 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/DriveTemplateSelector.cs @@ -0,0 +1,16 @@ +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Helpers; + +public class DriveTemplateSelector : DataTemplateSelector +{ + public DataTemplate LogicalDriveTemplate { get; set; } + public DataTemplate VirtualDriveTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) => item switch + { + LogicalDriveViewModel => LogicalDriveTemplate, + VirtualDriveViewModel => VirtualDriveTemplate, + _ => throw new NotImplementedException(), + }; +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpFileNameTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpFileNameTemplateSelector.cs new file mode 100644 index 00000000..c8587e48 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpFileNameTemplateSelector.cs @@ -0,0 +1,25 @@ +using ADB_Explorer.Services; + +namespace ADB_Explorer.Helpers; + +internal class FileOpFileNameTemplateSelector : DataTemplateSelector +{ + public DataTemplate UninstallOpFileNameTemplate { get; set; } + public DataTemplate FolderCompletedOpFileNameTemplate { get; set; } + public DataTemplate FolderInProgOpFileNameTemplate { get; set; } + public DataTemplate RegularOpFileNameTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item is not FileOperation fileop) + return null; + + if (fileop is PackageInstallOperation pkgInstall && pkgInstall.IsUninstall) + return UninstallOpFileNameTemplate; + + if (fileop.FilePath.IsDirectory) + return fileop.Status is FileOperation.OperationStatus.InProgress ? FolderInProgOpFileNameTemplate : FolderCompletedOpFileNameTemplate; + + return RegularOpFileNameTemplate; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpProgressTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpProgressTemplateSelector.cs new file mode 100644 index 00000000..b119e216 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpProgressTemplateSelector.cs @@ -0,0 +1,32 @@ +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Helpers; + +internal class FileOpProgressTemplateSelector : DataTemplateSelector +{ + public DataTemplate WaitingOpProgressTemplate { get; set; } + public DataTemplate InProgSyncProgressTemplate { get; set; } + public DataTemplate InProgShellProgressTemplate { get; set; } + public DataTemplate CompletedSyncProgressTemplate { get; set; } + public DataTemplate CompletedShellProgressTemplate { get; set; } + public DataTemplate CanceledOpProgressTemplate { get; set; } + public DataTemplate FailedOpProgressTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item is null) + return null; + + return item switch + { + WaitingOpProgressViewModel => WaitingOpProgressTemplate, + InProgSyncProgressViewModel => InProgSyncProgressTemplate, + InProgShellProgressViewModel => InProgShellProgressTemplate, + CompletedSyncProgressViewModel => CompletedSyncProgressTemplate, + CompletedShellProgressViewModel => CompletedShellProgressTemplate, + CanceledOpProgressViewModel => CanceledOpProgressTemplate, + FailedOpProgressViewModel => FailedOpProgressTemplate, + _ => throw new NotSupportedException(), + }; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpTreeTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpTreeTemplateSelector.cs new file mode 100644 index 00000000..9f29aa25 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOpTreeTemplateSelector.cs @@ -0,0 +1,20 @@ +using ADB_Explorer.Models; + +namespace ADB_Explorer.Helpers; + +internal class FileOpTreeTemplateSelector : DataTemplateSelector +{ + public HierarchicalDataTemplate FileOpTreeFolderTemplate { get; set; } + + public HierarchicalDataTemplate FileOpTreeFileTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + return item switch + { + SyncFile dir when dir.IsDirectory => FileOpTreeFolderTemplate, + SyncFile => FileOpTreeFileTemplate, + _ => throw new NotSupportedException(), + }; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOperationTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOperationTemplateSelector.cs new file mode 100644 index 00000000..a14c9d42 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/FileOperationTemplateSelector.cs @@ -0,0 +1,21 @@ +using ADB_Explorer.Services; + +namespace ADB_Explorer.Helpers; + +public class FileOperationTemplateSelector : DataTemplateSelector +{ + public DataTemplate PullTemplate { get; set; } + public DataTemplate PushTemplate { get; set; } + public DataTemplate SyncTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + return item switch + { + FileSyncOperation op when op.OperationName is FileOperation.OperationType.Pull => PullTemplate, + FileSyncOperation op when op.OperationName is FileOperation.OperationType.Push => PushTemplate, + FileSyncOperation => SyncTemplate, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/MenuTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/MenuTemplateSelector.cs new file mode 100644 index 00000000..dfd89851 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/MenuTemplateSelector.cs @@ -0,0 +1,74 @@ +using ADB_Explorer.Services; + +namespace ADB_Explorer.Helpers; + +internal class MenuTemplateSelector : DataTemplateSelector +{ + public DataTemplate CompoundIconMenuTemplate { get; set; } + public DataTemplate IconMenuTemplate { get; set; } + public DataTemplate AnimatedNotifyTemplate { get; set; } + public DataTemplate DynamicAltTextTemplate { get; set; } + public DataTemplate SeparatorTemplate { get; set; } + public DataTemplate AltIconTemplate { get; set; } + public DataTemplate SubMenuTemplate { get; set; } + public DataTemplate SubMenuSeparatorTemplate { get; set; } + public DataTemplate AltObjectTemplate { get; set; } + public DataTemplate CompoundIconSubMenuTemplate { get; set; } + public DataTemplate DualActionButtonTemplate { get; set; } + public DataTemplate CompoundDualActionTemplate { get; set; } + public DataTemplate GeneralSubMenuTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) => item switch + { + CompoundDualAction => CompoundDualActionTemplate, + DualActionButton => DualActionButtonTemplate, + SubMenuSeparator => SubMenuSeparatorTemplate, + CompoundIconSubMenu => CompoundIconSubMenuTemplate, + MenuSeparator => SeparatorTemplate, + AnimatedNotifyMenu => AnimatedNotifyTemplate, + GeneralSubMenu or UIElement => GeneralSubMenuTemplate, + SubMenu or string => SubMenuTemplate, + AltTextMenu => DynamicAltTextTemplate, + IconMenu => IconMenuTemplate, + AltObjectMenu => AltObjectTemplate, + CompoundIconMenu => CompoundIconMenuTemplate, + null => new(), + _ => throw new NotSupportedException(), + }; +} + +internal class MenuStyleSelector : StyleSelector +{ + public Style IconMenuStyle { get; set; } + public Style AnimatedNotifyStyle { get; set; } + public Style DynamicAltTextStyle { get; set; } + public Style SeparatorStyle { get; set; } + public Style AltIconStyle { get; set; } + public Style SubMenuStyle { get; set; } + public Style SubMenuSeparatorStyle { get; set; } + public Style CompoundIconSubMenuStyle { get; set; } + public Style AltObjectStyle { get; set; } + public Style CompoundIconMenuStyle { get; set; } + public Style DualActionButtonStyle { get; set; } + public Style CompoundDualActionStyle { get; set; } + public Style GeneralSubMenuStyle { get; set; } + public Style DummySubMenuStyle { get; set; } + + public override Style SelectStyle(object item, DependencyObject container) => item switch + { + CompoundDualAction => CompoundDualActionStyle, + DualActionButton => DualActionButtonStyle, + DummySubMenu => DummySubMenuStyle, + SubMenuSeparator => SubMenuSeparatorStyle, + CompoundIconSubMenu => CompoundIconSubMenuStyle, + MenuSeparator => SeparatorStyle, + AnimatedNotifyMenu => AnimatedNotifyStyle, + GeneralSubMenu or CheckBox => GeneralSubMenuStyle, + SubMenu or string => SubMenuStyle, + AltTextMenu => DynamicAltTextStyle, + IconMenu => IconMenuStyle, + AltObjectMenu => AltObjectStyle, + CompoundIconMenu => CompoundIconMenuStyle, + _ => throw new NotSupportedException(), + }; +} diff --git a/ADB Explorer _WpfUi/Helpers/TemplateSelectors/SettingsTemplateSelector.cs b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/SettingsTemplateSelector.cs new file mode 100644 index 00000000..2009c435 --- /dev/null +++ b/ADB Explorer _WpfUi/Helpers/TemplateSelectors/SettingsTemplateSelector.cs @@ -0,0 +1,31 @@ +using ADB_Explorer.Services; + +namespace ADB_Explorer.Helpers; + +public class SettingsTemplateSelector : DataTemplateSelector +{ + public DataTemplate BoolSettingTemplate { get; set; } + public DataTemplate TextboxSettingTemplate { get; set; } + public DataTemplate EnumSettingTemplate { get; set; } + public DataTemplate CultureInfoSettingTemplate { get; set; } + public DataTemplate InfoSettingTemplate { get; set; } + public DataTemplate LinkSettingTemplate { get; set; } + public DataTemplate MultiLinkSettingTemplate { get; set; } + public DataTemplate LongDescriptionTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + return item switch + { + InfoSetting => InfoSettingTemplate, + LinkSetting => LinkSettingTemplate, + MultiLinkSetting => MultiLinkSettingTemplate, + LongDescriptionSetting => LongDescriptionTemplate, + BoolSetting => BoolSettingTemplate, + TextboxSetting => TextboxSettingTemplate, + EnumSetting => EnumSettingTemplate, + ComboSetting => CultureInfoSettingTemplate, + _ => throw new NotImplementedException(), + }; + } +} diff --git a/ADB Explorer _WpfUi/Models/Battery.cs b/ADB Explorer _WpfUi/Models/Battery.cs new file mode 100644 index 00000000..accffa68 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Battery.cs @@ -0,0 +1,348 @@ +using ADB_Explorer.Converters; +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Models; + +public class Battery : ViewModelBase +{ + #region Enums + + public enum State + { + Unknown = 1, + Charging = 2, + Discharging = 3, + Not_Charging = 4, + Full = 5, + } + + public enum ChargingState + { + Unknown, + Discharging, + Charging, + } + + public enum Health + { + Unknown = 1, + Good = 2, + Overheat = 3, + Dead = 4, + Over_Voltage = 5, + Unspecified_failure = 6, + Cold = 7, + } + + public enum Source + { + None, + AC, + USB, + Wireless, + } + + #endregion + + #region Full properties + + private Source chargeSource = Source.None; + public Source ChargeSource + { + get => chargeSource; + set + { + if (Set(ref chargeSource, value)) + OnPropertyChanged(nameof(BatteryStateString)); + } + } + + private State batteryState = State.Unknown; + public State BatteryState + { + get => batteryState; + set + { + if (Set(ref batteryState, value)) + OnPropertyChanged(nameof(BatteryStateString)); + } + } + + private ChargingState chargeState = ChargingState.Unknown; + public ChargingState ChargeState + { + get => chargeState; + set + { + if (Set(ref chargeState, value)) + { + OnPropertyChanged(nameof(BatteryIcon)); + OnPropertyChanged(nameof(CompactStateString)); + OnPropertyChanged(nameof(BatteryLow)); + OnPropertyChanged(nameof(FullyCharged)); + } + } + } + + private byte? level; + public byte? Level + { + get => level; + set + { + if (Set(ref level, value)) + { + OnPropertyChanged(nameof(BatteryIcon)); + OnPropertyChanged(nameof(CompactStateString)); + OnPropertyChanged(nameof(BatteryLow)); + OnPropertyChanged(nameof(FullyCharged)); + } + } + } + + private double? voltage; + public double? Voltage + { + get => voltage; + set + { + if (Set(ref voltage, value)) + OnPropertyChanged(nameof(VoltageString)); + } + } + + private double? temperature; + public double? Temperature + { + get => temperature; + set + { + if (Set(ref temperature, value)) + OnPropertyChanged(nameof(TemperatureString)); + } + } + + private Health batteryHealth = 0; + public Health BatteryHealth + { + get => batteryHealth; + set + { + if (Set(ref batteryHealth, value)) + OnPropertyChanged(nameof(BatteryHealthString)); + } + } + + #endregion + + private long? chargeCounter = null; + private long? prevChargeCounter = null; + private DateTime? chargeUpdate = null; + private DateTime? prevChargeUpdate = null; + + #region Read only properties + + public bool BatteryLow => Level is not null && Level <= 10 && ChargeState is not ChargingState.Charging; + public bool FullyCharged => Level is not null && Level >= 100 && ChargeState is ChargingState.Charging; + + public string BatteryStateString + { + get + { + if (BatteryState is 0) + return ""; + + var status = byte.TryParse(BatteryState.ToString(), out _) + ? $"{(ChargeSource is Source.None ? Strings.Resources.S_BAT_STATE_DISCHARGING : $"{Strings.Resources.S_BAT_STATE_CHARGING} ({SourceString(ChargeSource)})")}" + : $"{StateString(BatteryState)}{(ChargeSource is Source.None ? "" : $" ({SourceString(ChargeSource)})")}"; + + return string.Format(Strings.Resources.S_BAT_STATUS, status); + } + } + + public string CompactStateString + { + get + { + if (ChargeState is ChargingState.Unknown) + { + return Strings.Resources.S_BAT_STATUS_UNKNOWN; + } + else + { + if (ChargeState is not ChargingState.Charging) + return Data.RuntimeSettings.IsRTL + ? $"%{Level}" + : $"{Level}%"; + + return string.Format(Strings.Resources.S_BAT_STATUS_PLUGGED, Level); + } + } + } + + public string VoltageString => Voltage switch + { + null => "", + _ => string.Format(Strings.Resources.S_BAT_VOLT, Voltage) + }; + + public string CurrentConsumption + { + get + { + if (chargeCounter is null || prevChargeCounter is null || prevChargeUpdate is null) + return ""; + + var currentDiff = chargeCounter.Value - prevChargeCounter.Value; + var timeDiff = DateTime.Now - prevChargeUpdate.Value; + var perHourConsumption = currentDiff / timeDiff.TotalHours; + var positive = perHourConsumption > 0; + + return string.Format(Strings.Resources.S_BAT_BALANCE, $"{(positive ? "+" : "")}{(perHourConsumption / 1000000).AmpsToSize()}"); + } + } + + public string TemperatureString => Temperature switch + { + null => "", + _ => string.Format(Strings.Resources.S_BAT_TEMP, Temperature) + }; + + public string BatteryHealthString + { + get + { + if (BatteryHealth is 0) + return ""; + + string healthString = BatteryHealth switch + { + Health.Unknown => Strings.Resources.S_BAT_STATE_UNKNOWN, + Health.Good => Strings.Resources.S_BAT_HEALTH_GOOD, + Health.Overheat => Strings.Resources.S_BAT_HEALTH_OVERHEAT, + Health.Dead => Strings.Resources.S_BAT_HEALTH_DEAD, + Health.Over_Voltage => Strings.Resources.S_BAT_HEALTH_OVER_VOLTAGE, + Health.Unspecified_failure => Strings.Resources.S_BAT_HEALTH_UNSPECIFIED_FAILURE, + Health.Cold => Strings.Resources.S_BAT_HEALTH_COLD, + _ => null, + }; + + return string.Format(Strings.Resources.S_BAT_HEALTH, healthString); + } + } + + public string BatteryIcon + { + get + { + if (ChargeState == ChargingState.Unknown || Level is null) + return Data.RuntimeSettings.Is22H2 ? "\uEC02" : "\uF608"; + + var level = Data.RuntimeSettings.Is22H2 ? 0xEBA0 : 0xF5F2; + if (ChargeState == ChargingState.Charging) + level += 11; + + level += Level.Value / 10; + + return $"{Convert.ToChar(level)}"; + } + } + + #endregion + + public Battery() + { + } + + public void Update(Dictionary batteryInfo) + { + if (batteryInfo is null) + return; + + if (batteryInfo.TryGetValue("AC powered", out string ac) && ac == "true") + ChargeSource = Source.AC; + + if (batteryInfo.TryGetValue("USB powered", out string usb) && usb == "true") + ChargeSource = Source.USB; + + if (batteryInfo.TryGetValue("Wireless powered", out string wl) && wl == "true") + ChargeSource = Source.Wireless; + + if (batteryInfo.ContainsKey("status")) + { + BatteryState = !byte.TryParse(batteryInfo["status"], out byte status) + ? State.Unknown + : (State)status; + + ChargeState = status switch + { + <= 1 => ChargingState.Unknown, + 3 or 4 => ChargingState.Discharging, + 2 or 5 => ChargingState.Charging, + > 5 when ChargeSource == Source.None => ChargingState.Discharging, + > 5 => ChargingState.Charging, + }; + } + + if (batteryInfo.ContainsKey("level")) + { + Level = !byte.TryParse(batteryInfo["level"], out byte level) + ? null + : level; + } + + if (batteryInfo.ContainsKey("voltage")) + { + Voltage = !int.TryParse(batteryInfo["voltage"], out int volt) + ? -1.0 + : volt / 1000.0; + } + + if (batteryInfo.ContainsKey("temperature")) + { + Temperature = !int.TryParse(batteryInfo["temperature"], out int temp) + ? -1.0 + : temp / 10; + } + + if (batteryInfo.ContainsKey("health")) + { + BatteryHealth = !Enum.TryParse(typeof(Health), batteryInfo["health"], out object health) + ? Health.Unknown + : (Health)health; + } + + if (batteryInfo.TryGetValue("Charge counter", out string value) + && long.TryParse(value, out long charge)) + { + if (chargeCounter != charge || prevChargeUpdate is null) + { + prevChargeUpdate = chargeUpdate; + chargeUpdate = DateTime.Now; + prevChargeCounter = chargeCounter; + chargeCounter = charge; + } + } + + OnPropertyChanged(nameof(CurrentConsumption)); + } + + public static string SourceString(Source source) => source switch + { + Source.None => Strings.Resources.S_BAT_SOURCE_NONE, + Source.AC => Strings.Resources.S_BAT_SOURCE_AC, + Source.USB => Strings.Resources.S_TYPE_USB, + Source.Wireless => Strings.Resources.S_BAT_SOURCE_WIRELESS, + _ => null, + }; + + public static string StateString(State state) => state switch + { + State.Unknown => Strings.Resources.S_BAT_STATE_UNKNOWN, + State.Charging => Strings.Resources.S_BAT_STATE_CHARGING, + State.Discharging => Strings.Resources.S_BAT_STATE_DISCHARGING, + State.Not_Charging => Strings.Resources.S_BAT_STATE_NOT_CHARGING, + State.Full => Strings.Resources.S_BAT_STATE_FULL, + _ => null, + }; +} diff --git a/ADB Explorer _WpfUi/Models/Device/Device.cs b/ADB Explorer _WpfUi/Models/Device/Device.cs new file mode 100644 index 00000000..2e7dd3ec --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Device/Device.cs @@ -0,0 +1,93 @@ +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Models; + +public abstract class AbstractDevice : ViewModelBase +{ + public enum DeviceType + { + Service, + Local, + Remote, + Recovery, + Sideload, + WSA, + Emulator, + History, + New, + } + + public enum DeviceStatus + { + Ok, // online \ does not require attention + Offline, + Unauthorized, + } + + public enum RootStatus + { + Unchecked, + Forbidden, + Disabled, + Enabled, + } +} + +public abstract class Device : AbstractDevice +{ + #region Full properties + + private DeviceType type; + public virtual DeviceType Type + { + get => type; + protected set => Set(ref type, value); + } + + private DeviceStatus status; + public virtual DeviceStatus Status + { + get => status; + set => Set(ref status, value); + } + + private string ipAddress; + public virtual string IpAddress + { + get => ipAddress; + set => Set(ref ipAddress, value); + } + + public virtual string ID { get; protected set; } + + #endregion + + public static implicit operator bool(Device obj) + { + return obj is not null && !string.IsNullOrEmpty(obj.ID); + } +} + +/// +/// Represents all device types that require pairing properties +/// +public abstract class PairingDevice : Device +{ + #region Full properties + + private string pairingPort; + public virtual string PairingPort + { + get => pairingPort; + set => Set(ref pairingPort, value); + } + + private string pairingCode; + public string PairingCode + { + get => pairingCode; + set => Set(ref pairingCode, value); + } + + #endregion +} diff --git a/ADB Explorer _WpfUi/Models/Device/HistoryDevice.cs b/ADB Explorer _WpfUi/Models/Device/HistoryDevice.cs new file mode 100644 index 00000000..757d2ac1 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Device/HistoryDevice.cs @@ -0,0 +1,31 @@ +namespace ADB_Explorer.Models; + +public class HistoryDevice : NewDevice +{ + private string deviceName = null; + public string DeviceName + { + get => deviceName; + set => Set(ref deviceName, value); + } + + public HistoryDevice() + { + Type = DeviceType.History; + Status = DeviceStatus.Ok; + } + + public HistoryDevice(NewDevice device) : this() + { + IpAddress = device.IpAddress; + ConnectPort = device.ConnectPort; + } + + [JsonConstructor] + public HistoryDevice(string ipAddress, string connectPort, string deviceName = "") : this() + { + DeviceName = deviceName; + IpAddress = ipAddress; + ConnectPort = connectPort; + } +} diff --git a/ADB Explorer _WpfUi/Models/Device/LogicalDevice.cs b/ADB Explorer _WpfUi/Models/Device/LogicalDevice.cs new file mode 100644 index 00000000..22ced25e --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Device/LogicalDevice.cs @@ -0,0 +1,249 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; +using AdvancedSharpAdbClient.Models; + +namespace ADB_Explorer.Models; + +/// +/// Represents all devices acquired by adb devices +/// +public class LogicalDevice : Device +{ + #region Full properties + + private string name; + public string Name + { + get => name; + set => Set(ref name, value); + } + + private RootStatus root = RootStatus.Unchecked; + public RootStatus Root + { + get => root; + set => Set(ref root, value); + } + + private Battery battery; + public Battery Battery + { + get => battery; + protected set => Set(ref battery, value); + } + + private ObservableList drives = []; + public ObservableList Drives + { + get => drives; + set => Set(ref drives, value); + } + + #endregion + + public DeviceData DeviceData { get; private set; } + + private LogicalDevice(string name, string id) + { + Name = name; + ID = id; + + Battery = new Battery(); + + InitDeviceDrives(); + } + + public static LogicalDevice New(Match match) + { + var name = DeviceHelper.ParseDeviceName(match.Groups["model"].Value, match.Groups["device"].Value); + var id = match.Groups["id"].Value; + var status = match.Groups["status"].Value; + + var deviceType = DeviceHelper.GetType(id, status); + var deviceStatus = DeviceHelper.GetStatus(status); + var ip = deviceType is DeviceType.Remote ? id.Split(':')[0] : ""; + var rootStatus = deviceType is DeviceType.Recovery + ? RootStatus.Enabled + : RootStatus.Unchecked; + + if (deviceType is DeviceType.WSA && name.Contains("subsystem", StringComparison.InvariantCultureIgnoreCase)) + name = Strings.Resources.S_TYPE_WSA; + + return new LogicalDevice(name, id) + { + Type = deviceType, + Status = deviceStatus, + Root = rootStatus, + IpAddress = ip, + DeviceData = new(match.Value) + }; + } + + public void EnableRoot(bool enable) + { + Root = enable + ? ADBService.Root(this) ? RootStatus.Enabled : RootStatus.Forbidden + : ADBService.Unroot(this) ? RootStatus.Disabled : RootStatus.Unchecked; + + if (Data.CurrentADBDevice.ID == ID) + Data.RuntimeSettings.IsRootActive = Root is RootStatus.Enabled; + } + + public void UpdateBattery() + { + Battery.Update(ADBService.AdbDevice.GetBatteryInfo(this)); + } + + #region Drive handling + + private void InitDeviceDrives() + { + Drives.Add(new LogicalDriveViewModel(new(path: AdbExplorerConst.DRIVE_TYPES.First(d => d.Value is AbstractDrive.DriveType.Root).Key))); + Drives.Add(new LogicalDriveViewModel(new(path: AdbExplorerConst.DRIVE_TYPES.First(d => d.Value is AbstractDrive.DriveType.Internal).Key))); + + Drives.Add(new VirtualDriveViewModel(new(path: AdbLocation.StringFromLocation(Navigation.SpecialLocation.RecycleBin), -1))); + Drives.Add(new VirtualDriveViewModel(new(path: AdbExplorerConst.TEMP_PATH))); + Drives.Add(new VirtualDriveViewModel(new(path: AdbLocation.StringFromLocation(Navigation.SpecialLocation.PackageDrive)))); + } + + /// + /// Update with new drives + /// + /// The new drives to be assigned + /// to update only after fully acquiring all information + public async Task UpdateDrives(IEnumerable drives, Dispatcher dispatcher, bool asyncClassify = false) + { + bool collectionChanged; + + // MMC and OTG drives are searched for and only then UI is updated with all changes + if (asyncClassify) + { + collectionChanged = await UpdateExtensionDrivesAsync(drives, dispatcher); + } + // All drives are first updated in UI, and only then MMC and OTG drives are searched for + else + { + collectionChanged = SetDrives(drives); + UpdateExtensionDrives(drives, dispatcher); + } + + return collectionChanged; + } + + private void UpdateExtensionDrives(IEnumerable drives, Dispatcher dispatcher) + { + var mmcTask = Task.Run(() => DeviceHelper.GetMmcDrive(drives.OfType(), ID)); + mmcTask.ContinueWith((t) => + { + if (t.IsCanceled) + return; + + dispatcher.BeginInvoke(() => + { + SetMmcDrive(t.Result); + SetExternalDrives(); + }); + }); + } + + private async Task UpdateExtensionDrivesAsync(IEnumerable drives, Dispatcher dispatcher) + { + await Task.Run(() => + { + if (DeviceHelper.GetMmcDrive(drives.OfType(), ID) is LogicalDrive mmc) + mmc.Type = AbstractDrive.DriveType.Expansion; + + DeviceHelper.SetExternalDrives(drives.OfType()); + }); + + var result = false; + await dispatcher.BeginInvoke(() => result = SetDrives(drives)); + + return result; + } + + /// + /// Update drive parameters, add new drives, remove non-existent drives + /// + /// + /// if drives have been added or removed + private bool SetDrives(IEnumerable drives) + { + if (drives is null) + return false; + + bool added = false; + + foreach (var other in drives) + { + // Accommodate for changing the path to /sdcard + var selfQ = Drives.Where(d => d.Path == other.Path || (other.Type is AbstractDrive.DriveType.Internal && d.Type is AbstractDrive.DriveType.Internal)); + if (selfQ.Any()) + { + // Update the drive if it exists + var self = selfQ.First(); + + switch (self) + { + case LogicalDriveViewModel logical: + logical.UpdateDrive((LogicalDrive)other); + if (other.Type is not AbstractDrive.DriveType.Unknown) + logical.SetType(other.Type); + break; + case VirtualDriveViewModel virt: + virt.SetItemsCount(((VirtualDrive)other).ItemsCount); + break; + default: + throw new NotSupportedException(); + } + } + // Create a new drive if it doesn't exist + else if (other is LogicalDrive logical) + { + Drives.Add(new LogicalDriveViewModel(logical)); + added = true; + } + else if (other is VirtualDrive virt && !Drives.Any(d => d.Type == virt.Type)) + { + Drives.Add(new VirtualDriveViewModel(virt)); + added = true; + } + else + throw new NotSupportedException(); + } + + // Remove all drives that were not discovered in the last update + var removed = Drives.RemoveAll(self => self is LogicalDriveViewModel + && !drives.Any(other => other.Path == self.Path + || (other.Type is AbstractDrive.DriveType.Internal && self.Type is AbstractDrive.DriveType.Internal))); + + return added || removed; + } + + public void SetMmcDrive(LogicalDrive mmcDrive) + { + if (mmcDrive is null) + return; + + ((LogicalDriveViewModel)Drives.FirstOrDefault(d => d.Path == mmcDrive.Path))?.SetExtension(); + } + + /// + /// Sets type of all with unknown type as external. Changes the local property. + /// + public void SetExternalDrives() + { + if (drives is null) + return; + + foreach (var item in Drives.Where(d => d.Type == AbstractDrive.DriveType.Unknown)) + { + ((LogicalDriveViewModel)item).SetExtension(false); + } + } + + #endregion + + public override string ToString() => Name; +} diff --git a/ADB Explorer _WpfUi/Models/Device/NewDevice.cs b/ADB Explorer _WpfUi/Models/Device/NewDevice.cs new file mode 100644 index 00000000..bcf1e999 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Device/NewDevice.cs @@ -0,0 +1,24 @@ +namespace ADB_Explorer.Models; + +public class NewDevice : PairingDevice +{ + private string connectPort; + public string ConnectPort + { + get => connectPort; + set => Set(ref connectPort, value); + } + + private string hostName; + public string HostName + { + get => hostName; + set => Set(ref hostName, value); + } + + public NewDevice() + { + Type = DeviceType.New; + Status = DeviceStatus.Ok; + } +} diff --git a/ADB Explorer _WpfUi/Models/Device/ServiceDevice.cs b/ADB Explorer _WpfUi/Models/Device/ServiceDevice.cs new file mode 100644 index 00000000..393163e4 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Device/ServiceDevice.cs @@ -0,0 +1,63 @@ +namespace ADB_Explorer.Models; + +/// +/// Represents all services acquired by mdns services +/// +public abstract class ServiceDevice : PairingDevice +{ + public enum ServiceType + { + QrCode, + PairingCode + } + + #region Full properties + + private ServiceType mdnsType; + public ServiceType MdnsType + { + get => mdnsType; + set => Set(ref mdnsType, value); + } + + #endregion + + public ServiceDevice() + { + Type = DeviceType.Service; + } + + public ServiceDevice(string id, string ipAddress, string port = "") : this() + { + PropertyChanged += ServiceDevice_PropertyChanged; + + ID = id; + IpAddress = ipAddress; + PairingPort = port; + } + + private void ServiceDevice_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(PairingPort) or nameof(MdnsType)) + { + UpdateStatus(); + } + } + + private void UpdateStatus() + { + Status = MdnsType is ServiceType.QrCode ? DeviceStatus.Ok : DeviceStatus.Unauthorized; + } +} + +public class PairingService : ServiceDevice +{ + public PairingService(string id, string ipAddress, string port) : base(id, ipAddress, port) + { } +} + +public class ConnectService : ServiceDevice +{ + public ConnectService(string id, string ipAddress, string port) : base(id, ipAddress, port) + { } +} diff --git a/ADB Explorer _WpfUi/Models/Device/WsaPkgDevice.cs b/ADB Explorer _WpfUi/Models/Device/WsaPkgDevice.cs new file mode 100644 index 00000000..3d329597 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Device/WsaPkgDevice.cs @@ -0,0 +1,17 @@ +namespace ADB_Explorer.Models; + +public class WsaPkgDevice : Device +{ + private DateTime lastLaunch = DateTime.MinValue; + public DateTime LastLaunch + { + get => lastLaunch; + set => Set(ref lastLaunch, value); + } + + public WsaPkgDevice() + { + Type = DeviceType.WSA; + Status = DeviceStatus.Offline; + } +} diff --git a/ADB Explorer _WpfUi/Models/DirectoryLister.cs b/ADB Explorer _WpfUi/Models/DirectoryLister.cs new file mode 100644 index 00000000..c01ff7ec --- /dev/null +++ b/ADB Explorer _WpfUi/Models/DirectoryLister.cs @@ -0,0 +1,223 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; +using static ADB_Explorer.Models.AbstractFile; +using static ADB_Explorer.Models.AdbExplorerConst; + +namespace ADB_Explorer.Models; + +public class DirectoryLister(Dispatcher dispatcher, ADBService.AdbDevice adbDevice, Func fileManipulator = null) : ViewModelBase +{ + public ADBService.AdbDevice Device { get; } = adbDevice; + public ObservableList FileList { get; } = []; + + private string currentPath; + public string CurrentPath + { + get => currentPath; + private set => Set(ref currentPath, value); + } + + private bool inProgress; + public bool InProgress + { + get => inProgress; + private set => Set(ref inProgress, value); + } + + private bool isProgressVisible = false; + public bool IsProgressVisible + { + get => isProgressVisible; + private set => Set(ref isProgressVisible, value); + } + + private bool isLinkListingFinished = false; + public bool IsLinkListingFinished + { + get => isLinkListingFinished; + private set => Set(ref isLinkListingFinished, value); + } + + private Dispatcher Dispatcher { get; } = dispatcher; + private Task UpdateTask { get; set; } + private TimeSpan UpdateInterval { get; set; } + private int MinUpdateThreshold { get; set; } + private Task ReadTask { get; set; } = null; + private CancellationTokenSource CurrentCancellationToken { get; set; } + private CancellationTokenSource LinkListCancellation { get; set; } + private Func FileManipulator { get; } = fileManipulator; + + private ConcurrentQueue currentFileQueue; + + public void Navigate(string path) + { + StartDirectoryList(path); + } + + public void Stop() + { + LinkListCancellation?.Cancel(); + StopDirectoryList(); + IsLinkListingFinished = true; + } + + private void StartDirectoryList(string path) + { + Dispatcher.BeginInvoke(() => + { + IsLinkListingFinished = false; + + LinkListCancellation?.Cancel(); + StopDirectoryList(); + FileList.RemoveAll(); + + InProgress = true; + IsProgressVisible = false; + CurrentPath = path; + }).Wait(); + + CurrentCancellationToken = new(); + LinkListCancellation = new(); + currentFileQueue = new ConcurrentQueue(); + + ReadTask = Task.Run(() => Device.ListDirectory(CurrentPath, ref currentFileQueue, Dispatcher, CurrentCancellationToken.Token), CurrentCancellationToken.Token); + ReadTask.ContinueWith((t) => Dispatcher.BeginInvoke(() => StopDirectoryList()), CurrentCancellationToken.Token); + + Task.Delay(DIR_LIST_VISIBLE_PROGRESS_DELAY).ContinueWith((t) => Dispatcher.BeginInvoke(() => IsProgressVisible = InProgress), CurrentCancellationToken.Token); + + ScheduleUpdate(); + } + + private void ScheduleUpdate() + { + UpdateDelays(currentFileQueue.Count); + + UpdateTask = Task.Delay(UpdateInterval); + UpdateTask.ContinueWith( + (t) => Dispatcher.BeginInvoke(() => UpdateDirectoryList(!InProgress)), + CurrentCancellationToken.Token, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default); + } + + private void UpdateDelays(int queueCount) + { + bool manyPendingFilesExist = queueCount >= DIR_LIST_UPDATE_THRESHOLD_MAX; + bool isListingStarting = FileList.Count < DIR_LIST_START_COUNT; + + if (isListingStarting || manyPendingFilesExist) + { + UpdateInterval = DIR_LIST_UPDATE_START_INTERVAL; + MinUpdateThreshold = DIR_LIST_UPDATE_START_THRESHOLD_MIN; + } + else + { + UpdateInterval = DIR_LIST_UPDATE_INTERVAL; + MinUpdateThreshold = DIR_LIST_UPDATE_THRESHOLD_MIN; + } + } + + private void UpdateDirectoryList(bool finish) + { + if (finish || (currentFileQueue.Count >= MinUpdateThreshold)) + { + for (int i = 0; finish || (i < DIR_LIST_UPDATE_THRESHOLD_MAX); i++) + { + if (!currentFileQueue.TryDequeue(out FileStat fileStat)) + { + break; + } + + FileClass item = FileClass.GenerateAndroidFile(fileStat); + + if (FileManipulator is not null) + { + item = FileManipulator(item); + } + + FileList.Add(item); + } + } + + if (!finish) + { + ScheduleUpdate(); + } + } + + private void StopDirectoryList() + { + if (ReadTask == null) + { + return; + } + + CurrentCancellationToken.Cancel(); + + try + { + ReadTask.Wait(); + } + catch (AggregateException e) when (e.InnerException is TaskCanceledException) + { } + + UpdateDirectoryList(true); + + InProgress = false; + IsProgressVisible = false; + ReadTask = null; + CurrentCancellationToken = null; + + if (currentFileQueue.IsEmpty && !FileList.Any()) + { + isLinkListingFinished = true; + return; + } + + Task.Run(ListLinks, LinkListCancellation.Token); + } + + private async void ListLinks() + { + await AsyncHelper.WaitUntil(() => FileList.Count > 0, DIR_LIST_UPDATE_INTERVAL, TimeSpan.FromMilliseconds(20), LinkListCancellation.Token); + + var items = FileList.Where(f => f.IsLink && f.Type is FileType.Unknown).ToList(); + if (items.Count < 1) + { + IsLinkListingFinished = true; + return; + } + + List<(string, FileType)> result = null; + try + { + result = [.. Device.GetLinkType(items.Select(f => f.FullPath), LinkListCancellation.Token)]; + } + catch (AggregateException e) when (e.InnerException is TaskCanceledException) + { } + + if (result is null) + { + IsLinkListingFinished = true; + return; + } + + for (var i = 0; i < items.Count; i++) + { + var file = items[i]; + var target = result[i]; + + Dispatcher.Invoke(() => + { + file.LinkTarget = target.Item1; + file.Type = target.Item2; + file.UpdateType(); + }); + } + + IsLinkListingFinished = true; + + Data.RuntimeSettings.RefreshExplorerSorting = true; + } +} diff --git a/ADB Explorer _WpfUi/Models/Drive/Drive.cs b/ADB Explorer _WpfUi/Models/Drive/Drive.cs new file mode 100644 index 00000000..7edfca86 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Drive/Drive.cs @@ -0,0 +1,79 @@ +using ADB_Explorer.ViewModels; +using static ADB_Explorer.Models.AdbExplorerConst; + +namespace ADB_Explorer.Models; + +public abstract class AbstractDrive : ViewModelBase +{ + public enum DriveType + { + Root, + Internal, + Expansion, + External, + Emulated, + Unknown, + Trash, + Temp, + Package, + } + + private DriveType type = DriveType.Unknown; + public DriveType Type + { + get => type; + set => Set(ref type, value); + } + + + public static implicit operator bool(AbstractDrive obj) + { + return obj is not null; + } + + public string DisplayName + { + get => GetDriveDisplayName(Type); + } + + public static string GetDriveDisplayName(DriveType type) => type switch + { + DriveType.Root => Strings.Resources.S_DRIVE_ROOT, + DriveType.Internal => Strings.Resources.S_DRIVE_INTERNAL_STORAGE, + DriveType.Expansion => Strings.Resources.S_DRIVE_SD, + DriveType.External => Strings.Resources.S_DRIVE_OTG, + DriveType.Unknown => "", + DriveType.Emulated => Strings.Resources.S_DRIVE_EMULATED, + DriveType.Trash => Strings.Resources.S_DRIVE_TRASH, + DriveType.Temp => Strings.Resources.S_DRIVE_TEMP, + DriveType.Package => Strings.Resources.S_DRIVE_APPS, + _ => null, + }; +} + +public class Drive : AbstractDrive +{ + public string Path { get; protected set; } + + /// + /// Filesystem in USEr space. An emulated / virtual filesystem on Android.

+ /// Does not support:
+ /// • Symbolic links
+ /// • Special chars in file name (like NTFS)
+ /// • Installing APK from it + ///
+ public virtual bool IsFUSE { get; } + + + public Drive(string path = "") + { + Path = path; + + if (Type is DriveType.Unknown && DRIVE_TYPES.TryGetValue(path, out var type)) + { + Type = type; + if (Type is DriveType.Internal) + Path = "/sdcard"; + } + } +} diff --git a/ADB Explorer _WpfUi/Models/Drive/LogicalDrive.cs b/ADB Explorer _WpfUi/Models/Drive/LogicalDrive.cs new file mode 100644 index 00000000..1b3bfa87 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Drive/LogicalDrive.cs @@ -0,0 +1,100 @@ +using ADB_Explorer.Converters; + +namespace ADB_Explorer.Models; + +public class LogicalDrive : Drive +{ + private string size; + public string Size + { + get => size; + set => Set(ref size, value); + } + + private string used; + public string Used + { + get => used; + set => Set(ref used, value); + } + + private string available; + public string Available + { + get => available; + set => Set(ref available, value); + } + + private sbyte usageP; + public sbyte UsageP + { + get => usageP; + set => Set(ref usageP, value); + } + + private string fileSystem = ""; + public string FileSystem + { + get => fileSystem; + set + { + if (Set(ref fileSystem, value)) + OnPropertyChanged(nameof(IsFUSE)); + } + } + + public override bool IsFUSE => FileSystem.Contains("fuse"); + + public string ID => Path.Count(c => c == '/') > 1 ? Path[(Path.LastIndexOf('/') + 1)..] : Path; + + + public LogicalDrive(string size = "", + string used = "", + string available = "", + sbyte usageP = -1, + string path = "", + bool isMMC = false, + bool isEmulator = false, + string fileSystem = "") + : base(path) + { + if (usageP is < -1 or > 100) + throw new ArgumentOutOfRangeException(nameof(usageP)); + + Size = size; + Used = used; + Available = available; + UsageP = usageP; + + if (path == "/") + { + Type = DriveType.Root; + } + else if (AdbExplorerConst.DRIVE_TYPES.Where(kv => kv.Value is DriveType.Internal).Any(kv => kv.Key.Contains(path))) + { + Type = DriveType.Internal; + } + else if (isMMC) + { + Type = DriveType.Expansion; + } + else if (isEmulator) + { + Type = DriveType.Emulated; + } + + FileSystem = fileSystem; + } + + public LogicalDrive(GroupCollection match, bool isMMC = false, bool isEmulator = false, string forcePath = "") + : this( + (long.Parse(match["size_kB"].Value) * 1024).BytesToSize(true, 1, 0), + (long.Parse(match["used_kB"].Value) * 1024).BytesToSize(true, 1, 0), + (long.Parse(match["available_kB"].Value) * 1024).BytesToSize(true, 1, 0), + sbyte.Parse(match["usage_P"].Value), + string.IsNullOrEmpty(forcePath) ? match["path"].Value : forcePath, + isMMC, + isEmulator, + match["FileSystem"].Value) + { } +} diff --git a/ADB Explorer _WpfUi/Models/Drive/VirtualDrive.cs b/ADB Explorer _WpfUi/Models/Drive/VirtualDrive.cs new file mode 100644 index 00000000..6e27256d --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Drive/VirtualDrive.cs @@ -0,0 +1,25 @@ +namespace ADB_Explorer.Models; + +public class VirtualDrive : Drive +{ + private long? itemsCount = 0; + public long? ItemsCount + { + get => itemsCount; + set => Set(ref itemsCount, value); + } + + public override bool IsFUSE => Type switch + { + // Temp drive is under the root filesystem + DriveType.Temp => Data.DevicesObject.Current?.Drives?.Find(d => d.Type is DriveType.Root).IsFUSE is true, + // App drive isn't really a drive, and the recycle bin doesn't allow any of the actions limited on FUSE + // So it is useless to display the icon + _ => false, + }; + + public VirtualDrive(string path = "", long itemsCount = 0) : base(path) + { + ItemsCount = itemsCount; + } +} diff --git a/ADB Explorer _WpfUi/Models/File/FileClass.cs b/ADB Explorer _WpfUi/Models/File/FileClass.cs new file mode 100644 index 00000000..2591963b --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/FileClass.cs @@ -0,0 +1,498 @@ +using ADB_Explorer.Converters; +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using ADB_Explorer.Services.AppInfra; +using Vanara.PInvoke; +using Vanara.Windows.Shell; + +namespace ADB_Explorer.Models; + +public class FileClass : FilePath, IFileStat, IBrowserItem +{ + #region Notify Properties + + private long? size; + public long? Size + { + get => size; + set => Set(ref size, value); + } + + private bool isLink; + public bool IsLink + { + get => isLink; + set + { + if (Set(ref isLink, value)) + UpdateSpecialType(); + } + } + + private string linkTarget = ""; + public string LinkTarget + { + get => linkTarget; + set => Set(ref linkTarget, value); + } + + private FileType type; + public FileType Type + { + get => type; + set + { + if (Set(ref type, value)) + UpdateSpecialType(); + } + } + + private string typeName; + public string TypeName + { + get => typeName; + private set => Set(ref typeName, value); + } + + private DateTime? modifiedTime; + public DateTime? ModifiedTime + { + get => modifiedTime; + set + { + if (Set(ref modifiedTime, value)) + OnPropertyChanged(nameof(ModifiedTimeString)); + } + } + + public double? UnixTime => ModifiedTime.ToUnixTime(); + + private BitmapSource icon = null; + public BitmapSource Icon + { + get => icon; + private set => Set(ref icon, value); + } + + private BitmapSource iconOverlay = null; + public BitmapSource IconOverlay + { + get => iconOverlay; + private set => Set(ref iconOverlay, value); + } + + private DragDropEffects cutState = DragDropEffects.None; + public DragDropEffects CutState + { + get => cutState; + set => Set(ref cutState, value); + } + + private TrashIndexer trashIndex; + public TrashIndexer TrashIndex + { + get => trashIndex; + set + { + Set(ref trashIndex, value); + if (value is not null && value.OriginalPath is not null) + FullName = FileHelper.GetFullName(value.OriginalPath); + } + } + + #endregion + + private bool extensionIsGlyph = false; + public bool ExtensionIsGlyph + { + get => extensionIsGlyph; + set => Set(ref extensionIsGlyph, value); + } + + private bool extensionIsFontIcon = false; + public bool ExtensionIsFontIcon + { + get => extensionIsFontIcon; + set => Set(ref extensionIsFontIcon, value); + } + + public bool IsTemp { get; set; } + + public FileNameSort SortName { get; private set; } + + public (string, long?, double?)[] Children => !IsDirectory + ? null + : FileHelper.GetFolderTree([FullPath]); + + public IEnumerable Descriptors { get; private set; } + + #region Read Only Properties + + public bool IsApk => AdbExplorerConst.APK_NAMES.Contains(Extension.ToUpper()); + + public bool IsInstallApk => Array.IndexOf(AdbExplorerConst.INSTALL_APK, Extension.ToUpper()) > -1; + + /// + /// Returns the extension (including the period ".") of a regular file.
+ /// Returns an empty string if file has no extension, or is not a regular file. + ///
+ public override string Extension => Type is FileType.File ? base.Extension : ""; + + public string ShortExtension + { + get + { + return (Extension.Length > 1 && Array.IndexOf(AdbExplorerConst.UNICODE_ICONS, char.GetUnicodeCategory(Extension[1])) > -1) + ? Extension[1..] + : ""; + } + } + + public string ModifiedTimeString => TabularDateFormatter.Format(ModifiedTime, Thread.CurrentThread.CurrentCulture); + + public string SizeString => IsDirectory ? "" : Size?.BytesToSize(true); + + #endregion + + public FileClass( + string fileName, + string path, + FileType type, + bool isLink = false, + long? size = null, + DateTime? modifiedTime = null, + bool isTemp = false) + : base(path, fileName, type) + { + Type = type; + Size = size; + ModifiedTime = modifiedTime; + IsLink = isLink; + + GetIcon(); + TypeName = GetTypeName(); + IsTemp = isTemp; + + SortName = new(fileName); + } + + public FileClass(FileClass other) + : this(other.FullName, other.FullPath, other.Type, other.IsLink, other.Size, other.ModifiedTime, other.IsTemp) + { } + + public FileClass(FilePath other) + : this(other.FullName, other.FullPath, other.IsDirectory ? FileType.Folder : FileType.File) + { } + + public FileClass(SyncFile other) + : this(other.FullName, + other.FullPath, + other.IsDirectory ? FileType.Folder : FileType.File, + other.SpecialType.HasFlag(SpecialFileType.LinkOverlay), + other.Size, + other.DateModified) + { } + + public FileClass(ShellItem windowsPath) + : base(windowsPath) + { + Type = IsDirectory ? FileType.Folder : FileType.File; + Size = IsDirectory ? null : windowsPath.FileInfo.Length; + ModifiedTime = windowsPath.FileInfo?.LastWriteTime; + IsLink = windowsPath.IsLink; + + GetIcon(); + TypeName = GetTypeName(); + + SortName = new(FullName); + } + + public FileClass(FileDescriptor fileDescriptor) + : base(fileDescriptor.SourcePath, fileDescriptor.Name, fileDescriptor.IsDirectory ? FileType.Folder : FileType.File) + { + Size = fileDescriptor.Length; + ModifiedTime = fileDescriptor.ChangeTimeUtc; + Type = fileDescriptor.IsDirectory ? FileType.Folder : FileType.File; + } + + public static FileClass GenerateAndroidFile(FileStat fileStat) => new( + fileName: fileStat.FullName, + path: fileStat.FullPath, + type: fileStat.Type, + size: fileStat.Size, + modifiedTime: fileStat.ModifiedTime, + isLink: fileStat.IsLink + ); + + public override void UpdatePath(string androidPath) + { + base.UpdatePath(androidPath); + UpdateType(); + + SortName = new(FullName); + } + + public void UpdateType() + { + TypeName = GetTypeName(); + GetIcon(); + OnPropertyChanged(nameof(ExtensionIsGlyph)); + OnPropertyChanged(nameof(ExtensionIsFontIcon)); + } + + public void UpdateSpecialType() + { + SpecialType = Type switch + { + FileType.Folder => SpecialFileType.Folder, + FileType.Unknown => SpecialFileType.Unknown, + FileType.BrokenLink => SpecialFileType.BrokenLink, + _ => SpecialFileType.Regular + }; + + if (IsApk) + SpecialType |= SpecialFileType.Apk; + + if (IsLink && Type is not FileType.BrokenLink) + SpecialType |= SpecialFileType.LinkOverlay; + } + + private string GetTypeName() + { + var type = Type switch + { + FileType.File => GetTypeName(FullName), + FileType.Folder => Strings.Resources.S_MENU_FOLDER, + FileType.Unknown => "", + _ => GetFileTypeName(Type), + }; + + if (IsLink && Type is not FileType.BrokenLink) + type = string.IsNullOrEmpty(type) + ? Strings.Resources.S_FILE_TYPE_LINK + : string.Format(Strings.Resources.S_KNOWN_TYPE_LINK, type); + + return type; + } + + public string GetTypeName(string fileName) + { + if (IsApk) + return Strings.Resources.S_FILE_TYPE_APK; + + if (string.IsNullOrEmpty(fileName) || (IsHidden && FullName.Count(c => c == '.') == 1)) + return Strings.Resources.S_MENU_FILE; + + if (Extension.Equals(".exe", StringComparison.CurrentCultureIgnoreCase)) + return Strings.Resources.S_FILE_TYPE_EXE; + + if (!Ascii.IsValid(Extension)) + { + if (ShortExtension.Length == 1) + ExtensionIsGlyph = true; + else if (ShortExtension.Length > 1) + ExtensionIsFontIcon = true; + else + { + ExtensionIsGlyph = + ExtensionIsFontIcon = false; + + return $"{Extension[1..]} {Strings.Resources.S_MENU_FILE}"; + } + + return $"{ShortExtension} {Strings.Resources.S_MENU_FILE}"; + } + else + { + ExtensionIsGlyph = + ExtensionIsFontIcon = false; + + return NativeMethods.GetShellFileType(fileName); + } + } + + public SyncFile GetSyncFile() => new(this, Children); + + public FileSyncOperation PrepareDescriptors(VirtualFileDataObject vfdo, bool includeContent = true) + { + var name = Data.FileActions.IsAppDrive + ? Data.SelectedPackages.FirstOrDefault(pkg => pkg.Path == FullPath)?.Name + ".apk" + : FullName; + + SyncFile target = new(FileHelper.ConcatPaths(Data.RuntimeSettings.TempDragPath, name, '\\')) + { PathType = FilePathType.Windows }; + + var children = Children; + + var fileOp = FileSyncOperation.PullFile(new(this, children), target, Data.CurrentADBDevice, App.Current.Dispatcher); + fileOp.PropertyChanged += PullOperation_PropertyChanged; + fileOp.VFDO = vfdo; + + (string, long?, double?)[] items = [(name, Size, UnixTime)]; + if (includeContent && children is not null) + { + items = [.. items, .. children]; + } + + Descriptors = items.Select(item => new FileDescriptor + { + Name = FileHelper.ExtractRelativePath(item.Item1, ParentPath), + SourcePath = FullPath, + IsDirectory = item.Item2 is null, + Length = item.Item2, + ChangeTimeUtc = item.Item3.FromUnixTime(), + Stream = () => + { + var isActive = App.Current.Dispatcher.Invoke(() => App.Current.MainWindow.IsActive); + var operations = vfdo.Operations.Where(op => op.Status is FileOperation.OperationStatus.None); + + // When a VFDO that does not contain folders is sent to the clipboard, the shell immediately requests the file contents. + // To prevent this, we refuse to give data when the app is focused. + // When a legitimate request for data is made, the app can't be focused during the first file, but it can become focused again for the next files. + if ((Data.CopyPaste.IsClipboard && isActive && operations.Any()) + || !includeContent) + return null; + +#if !DEPLOY + DebugLog.PrintLine($"Total uninitiated operations: {operations.Count()}"); +#endif + + // Add all uninitiated operations to the queue. + // For all consecutive files this list will be empty. + foreach (var op in operations) + { + App.Current.Dispatcher.Invoke(() => Data.FileOpQ.AddOperation(op)); + } + + // Wait for the operation to complete + while (fileOp.Status is not FileOperation.OperationStatus.Completed) + { + Thread.Sleep(100); + } + + var file = FileHelper.ConcatPaths(Data.RuntimeSettings.TempDragPath, FileHelper.ExtractRelativePath(item.Item1, ParentPath), '\\'); + + // Try 10 times to read from the file and write to the stream, + // in case the file is still in use by ADB or hasn't appeared yet + for (int i = 0; i < 10; i++) + { + try + { + var stream = NativeMethods.GetComStreamFromFile(file); + + if (stream is not null) + return stream; + } + catch (Exception e) + { +#if !DEPLOY + DebugLog.PrintLine($"Failed to open stream on {file}: {e.Message}"); +#endif + + Thread.Sleep(100); + continue; + } + } + + return null; + } + }); + + return fileOp; + } + + private static void PullOperation_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + var op = sender as FileSyncOperation; + + if (e.PropertyName != nameof(FileOperation.Status) + || op.Status is FileOperation.OperationStatus.Waiting + or FileOperation.OperationStatus.InProgress) + return; + + if (op.Status is FileOperation.OperationStatus.Completed) + { + if (op.VFDO.CurrentEffect.HasFlag(DragDropEffects.Move)) + { + // Delete file from device + ShellFileOperation.SilentDelete(op.Device, op.FilePath.FullPath); + + // Remove file in UI if present + if (op.Device.ID == Data.CurrentADBDevice.ID + && op.FilePath.ParentPath == Data.CurrentPath) + { + op.Dispatcher.Invoke(() => + { + Data.DirList.FileList.RemoveAll(f => f.FullPath == op.FilePath.FullPath); + FileActionLogic.UpdateFileActions(); + }); + } + + if (op.VFDO.Operations.All(op => op.Status is FileOperation.OperationStatus.Completed)) + Data.CopyPaste.Clear(); + } + + op.VFDO = null; + } + + op.PropertyChanged -= PullOperation_PropertyChanged; + } + + public void GetIcon() + { + var icons = FileToIconConverter.GetImage(this, true).ToArray(); + + if (icons.Length > 0 && icons[0] is BitmapSource icon) + Icon = icon; + else + Icon = null; + + if (icons.Length > 1 && icons[1] is BitmapSource icon2) + IconOverlay = icon2; + else + IconOverlay = null; + } + + public override string ToString() + { + if (TrashIndex is null) + { + return $"{DisplayName} \n{ModifiedTimeString} \n{TypeName} \n{SizeString}"; + } + else + { + return $"{DisplayName} \n{TrashIndex.OriginalPath} \n{TrashIndex.ModifiedTimeString} \n{TypeName} \n{SizeString} \n{ModifiedTimeString}"; + } + } + + protected override bool Set(ref T storage, T value, [CallerMemberName] string propertyName = null) + { + if (propertyName is nameof(FullName) or nameof(Type) or nameof(IsLink)) + { + UpdateType(); + } + + return base.Set(ref storage, value, propertyName); + } + + public static explicit operator SyncFile(FileClass self) + => new(self.FullPath, self.Type); +} + +public class FileNameSort(string name) : IComparable +{ + public string Name { get; } = name; + + public override string ToString() + { + return Name; + } + + public int CompareTo(object obj) + { + if (obj is not FileNameSort other) + return 0; + + return NativeMethods.StringCompareLogical(Name, other.Name); + } +} diff --git a/ADB Explorer _WpfUi/Models/File/FilePath.cs b/ADB Explorer _WpfUi/Models/File/FilePath.cs new file mode 100644 index 00000000..244dff09 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/FilePath.cs @@ -0,0 +1,182 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.ViewModels; +using Vanara.Windows.Shell; + +namespace ADB_Explorer.Models; + +public abstract class AbstractFile : ViewModelBase +{ + public enum FilePathType + { + Android, + Windows, + } + + public enum RelationType + { + Ancestor, + Descendant, + Self, + Unrelated, + } + + public enum FileType + { + Socket = 0, + File = 1, + BlockDevice = 2, + Folder = 3, + CharDevice = 4, + FIFO = 5, + Unknown = 6, + BrokenLink = 7, + } + + [Flags] + public enum SpecialFileType + { + Regular = 1, + Folder = 2, + Apk = 4, + BrokenLink = 8, + Unknown = 16, + LinkOverlay = 32, + Archive = 64, + } + + public static string GetFileTypeName(FileType type) => type switch + { + FileType.Socket => Strings.Resources.S_FILE_SOCKET, + FileType.File => Strings.Resources.S_MENU_FILE, + FileType.BlockDevice => Strings.Resources.S_FILE_BLOCK, + FileType.Folder => Strings.Resources.S_MENU_FOLDER, + FileType.CharDevice => Strings.Resources.S_FILE_CHAR, + FileType.FIFO => Strings.Resources.S_FILE_FIFO, + FileType.BrokenLink => Strings.Resources.S_FILE_BROKEN_LINK, + _ => Strings.Resources.S_FILE_UNKNOWN, + }; +} + +public class FilePath : AbstractFile, IBaseFile +{ + public FilePathType PathType { get; set; } + + public SpecialFileType SpecialType { get; protected set; } + + public bool IsRegularFile => SpecialType.HasFlag(SpecialFileType.Regular); + + public bool IsDirectory => SpecialType.HasFlag(SpecialFileType.Folder); + + private string fullPath; + public string FullPath + { + get => fullPath; + protected set => Set(ref fullPath, value); + } + + public string ParentPath => FileHelper.GetParentPath(FullPath); + + private string fullName; + public string FullName + { + get => fullName; + protected set => Set(ref fullName, value); + } + public string NoExtName => IsRegularFile ? FullName[..^Extension.Length] : FullName; + + public string DisplayName + { + get + { + var noExtName = NoExtName; + // Add RTL mark to end of RTL file names with LTR extensions. + // This prevents numbers and punctuation from breaking the RTL ordering. + if (TextHelper.ContainsRtl(NoExtName) + && NoExtName[^1] != TextHelper.RTL_MARK + && !TextHelper.ContainsRtl(Extension)) + { + noExtName = $"{NoExtName}{TextHelper.RTL_MARK}"; + } + return Data.Settings.ShowExtensions ? $"{noExtName}{Extension}" : noExtName; + } + } + + public bool IsRtlName => TextHelper.ContainsRtl(FullName); + + public ShellItem ShellItem { get; set; } + + public bool IsHidden => FullName.StartsWith('.'); + + /// + /// Returns the extension (including the period ".").
+ /// Returns an empty string if file has no extension. + ///
+ public virtual string Extension => FileHelper.GetExtension(FullName); + + public FilePath(ShellItem windowsPath) + { + ShellItem = windowsPath; + PathType = FilePathType.Windows; + + FullPath = windowsPath.ParsingName; + FullName = windowsPath.GetDisplayName(ShellItemDisplayString.ParentRelativeParsing); + + SpecialType = windowsPath.IsNonArchiveFolder() + ? SpecialFileType.Folder + : SpecialFileType.Regular; + } + + public FilePath(string androidPath, + string fullName = "", + FileType fileType = FileType.File) + { + PathType = FilePathType.Android; + + FullPath = androidPath; + FullName = string.IsNullOrEmpty(fullName) ? FileHelper.GetFullName(androidPath) : fullName; + + if (fileType is FileType.Folder) + SpecialType = SpecialFileType.Folder; + else + { + var ext = FileHelper.GetExtension(FullName).ToUpper(); + SpecialType = AdbExplorerConst.APK_NAMES.Contains(ext) + ? SpecialFileType.Apk + : SpecialFileType.Regular; + + if (AdbExplorerConst.ARCHIVE_NAMES.Contains(ext)) + SpecialType |= SpecialFileType.Archive; + } + + Data.Settings.PropertyChanged += (sender, args) => + { + if (args.PropertyName == nameof(Data.Settings.ShowExtensions)) + { + OnPropertyChanged(nameof(DisplayName)); + } + }; + } + + public virtual void UpdatePath(string newPath) + { + FullPath = newPath; + FullName = FileHelper.GetFullName(newPath); + + OnPropertyChanged(nameof(NoExtName)); + OnPropertyChanged(nameof(Extension)); + OnPropertyChanged(nameof(DisplayName)); + } + + /// + /// Returns the relation of the file to file.
+ /// Example: File.RelationFrom(File.Parent) = Ancestor + ///
+ public RelationType RelationFrom(FilePath other) => Relation(other.FullPath); + + public RelationType Relation(string other) => FileHelper.RelationFrom(FullPath, other); + + public override string ToString() + { + return FullName; + } +} diff --git a/ADB Explorer _WpfUi/Models/File/FileStat.cs b/ADB Explorer _WpfUi/Models/File/FileStat.cs new file mode 100644 index 00000000..2a78dbff --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/FileStat.cs @@ -0,0 +1,33 @@ +namespace ADB_Explorer.Models; + +public class FileStat : AbstractFile, IBaseFile, IFileStat +{ + public FileStat(string fileName, + string path, + FileType type, + bool isLink, + long? size, + DateTime? modifiedTime) + { + FullName = fileName; + FullPath = path; + + Type = type; + Size = size; + ModifiedTime = modifiedTime; + + IsLink = isLink; + } + + public string FullName { get; set; } + + public string FullPath { get; set; } + + public FileType Type { get; set; } + + public long? Size { get; set; } + + public DateTime? ModifiedTime { get; set; } + + public bool IsLink { get; set; } +} diff --git a/ADB Explorer _WpfUi/Models/File/IBaseFile.cs b/ADB Explorer _WpfUi/Models/File/IBaseFile.cs new file mode 100644 index 00000000..5686505b --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/IBaseFile.cs @@ -0,0 +1,35 @@ +using static ADB_Explorer.Models.AbstractFile; + +namespace ADB_Explorer.Models; + +/// +/// Represents the basic properties returned by the `stat` command on Android devices. +/// +public interface IFileStat +{ + public FileType Type { get; set; } + + public long? Size { get; set; } + + public DateTime? ModifiedTime { get; set; } + + public bool IsLink { get; set; } +} + +/// +/// Represents the basic properties of a file or directory. +/// +public interface IBaseFile +{ + public string FullName { get; } + + public string FullPath { get; } +} + +/// +/// Represents the items that can be displayed in the browser - and . +/// +public interface IBrowserItem +{ + public string DisplayName { get; } +} diff --git a/ADB Explorer _WpfUi/Models/File/Package.cs b/ADB Explorer _WpfUi/Models/File/Package.cs new file mode 100644 index 00000000..b8a62e58 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/Package.cs @@ -0,0 +1,76 @@ +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Models; + +public class Package : ViewModelBase, IBrowserItem +{ + public enum PackageType + { + System, + User, + } + + private string name; + public string Name + { + get => name; + private set => Set(ref name, value); + } + + private string path; + public string Path + { + get => path; + private set => Set(ref path, value); + } + + public string DisplayName => Name; + + private PackageType type; + public PackageType Type + { + get => type; + private set => Set(ref type, value); + } + + private long? uid = null; + public long? Uid + { + get => uid; + private set => Set(ref uid, value); + } + + private long? version = null; + public long? Version + { + get => version; + private set => Set(ref version, value); + } + + public static Package New(string package, PackageType type) + { + var match = AdbRegEx.RE_PACKAGE_LISTING().Match(package); + if (!match.Success) + return null; + + return new Package(match.Groups["Name"].Value, type, match.Groups["Uid"].Value, match.Groups["Version"].Value, match.Groups["Path"].Value); + } + + public Package(string name, PackageType type, string uid, string version, string path) + { + Name = name; + Type = type; + Path = path; + + if (long.TryParse(uid, out long resU)) + Uid = resU; + + if (long.TryParse(version, out long resV)) + Version = resV; + } + + public override string ToString() + { + return $"{Name}\n{Type}\n{Uid}\n{Version}\n{Path}"; + } +} diff --git a/ADB Explorer _WpfUi/Models/File/SyncFile.cs b/ADB Explorer _WpfUi/Models/File/SyncFile.cs new file mode 100644 index 00000000..11635307 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/SyncFile.cs @@ -0,0 +1,209 @@ +using ADB_Explorer.Converters; +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using Vanara.Windows.Shell; + +namespace ADB_Explorer.Models; + +public class SyncFile : FilePath +{ + public ObservableList ProgressUpdates { get; private set; } = []; + + public FileOpProgressInfo LastUpdate => ProgressUpdates.LastOrDefault(); + + public int? CurrentPercentage => LastUpdate is AdbSyncProgressInfo adbInfo ? adbInfo.CurrentFilePercentage : null; + + public long? BytesTransferred => LastUpdate is AdbSyncProgressInfo adbInfo ? adbInfo.CurrentFileBytesTransferred : null; + + public ObservableList Children { get; private set; } = []; + + public long? Size { get; set; } + + public double? UnixTime { get; set; } + public DateTime? DateModified + { + get + { + if (!Data.Settings.KeepDateModified) + return null; + + return ShellItem?.FileInfo is not null + ? ShellItem.FileInfo.LastWriteTime + : UnixTime.FromUnixTime(); + } + } + + public SyncFile(string androidPath, FileType fileType = FileType.File) + : base(androidPath, fileType: fileType) + { + + } + + public SyncFile(ShellItem windowsPath, bool includeContent = false) + : base(windowsPath) + { + Size = IsDirectory ? null : windowsPath.FileInfo.Length; + + if (includeContent && IsDirectory) + { + Children = [.. GetFolderTree((ShellFolder)windowsPath)]; + } + } + + public SyncFile(FileClass fileClass, IEnumerable<(string, long?, double?)> tree = null) + : base(fileClass.FullPath, fileClass.FullName, fileClass.Type) + { + Size = fileClass.Size; + UnixTime = fileClass.ModifiedTime.ToUnixTime(); + + if (tree is not null && IsDirectory) + Children = [.. GetFolderTree(tree, FullPath)]; + } + + public SyncFile(SyncFile other) : this(new FileClass(other)) + { } + + static IEnumerable GetFolderTree(ShellFolder rootFolder) + { + foreach (var child in rootFolder) + { + if (child.IsNonArchiveFolder()) + { + yield return new(child) + { + Children = [.. GetFolderTree((ShellFolder)child)], + ProgressUpdates = [new AdbSyncProgressInfo(child.ParsingName, null, null, null)] + }; + } + else + { + yield return new(child) + { + ProgressUpdates = [new AdbSyncProgressInfo(child.ParsingName, null, null, null)] + }; + } + } + } + + static IEnumerable GetFolderTree(IEnumerable<(string, long?, double?)> tree, string parent) + { + // empty folder + if (!tree.Any()) + yield break; + + var groups = tree.GroupBy(f => f.Item1.Split(parent)[1].Trim('/').Split('/')[0]); + + foreach (var group in groups.Where(g => g.Key is not null)) + { + var fullPath = FileHelper.ConcatPaths(parent, group.Key); + + if (group.First().Item2 is null) + { + var children = GetFolderTree(group.Skip(1), fullPath); + + yield return new(fullPath, FileType.Folder) + { + Children = [.. children], + ProgressUpdates = [new AdbSyncProgressInfo(fullPath, null, null, null)] + }; + } + else + { + yield return new(fullPath, FileType.File) + { + Size = group.First().Item2, + UnixTime = group.First().Item3, + ProgressUpdates = [new AdbSyncProgressInfo(fullPath, null, null, null)] + }; + } + } + } + + public void AddUpdates(params FileOpProgressInfo[] newUpdates) + => AddUpdates(newUpdates.Where(o => o is not null)); + + public void AddUpdates(IEnumerable newUpdates, FileOperation fileOp = null, bool executeInDispatcher = true) + { + if (!newUpdates.Any()) + return; + + if (!IsDirectory || newUpdates.All(u => u.AndroidPath is not null && u.AndroidPath.Equals(FullPath))) + { + ProgressUpdates.AddRange(newUpdates); + return; + } + + if (fileOp is FileSyncOperation) + { + foreach (var update in newUpdates) + { + if (string.IsNullOrEmpty(update.AndroidPath)) + update.SetPathToCurrent(fileOp); + } + } + else + newUpdates = newUpdates.Where(u => !string.IsNullOrEmpty(u.AndroidPath)); + + var groups = newUpdates.GroupBy(update => DirectChildPath(update.AndroidPath)); + + foreach (var group in groups.Where(g => g.Key is not null)) + { + SyncFile file = Children.FirstOrDefault(child => child.FullPath.Equals(group.Key)); + + if (file is null) + { + bool isDir = !group.Key.Equals(group.First().AndroidPath) || group.Key[^1] is '/' or '\\'; + file = new(group.Key, isDir ? FileType.Folder : FileType.File) + { + PathType = PathType + }; + + ExecuteInDispatcher(() => + { + Children.Add(file); + + OnPropertyChanged(nameof(Children)); + }, executeInDispatcher); + } + + file.AddUpdates(group); + } + } + + public string DirectChildPath(string fullPath) + => FileHelper.DirectChildPath(FullPath, fullPath); + + public IEnumerable AllChildren() + { + foreach (var child in Children) + { + yield return child; + + foreach (var grandChild in child.AllChildren()) + { + yield return grandChild; + } + } + } + + public static SyncFile MergeToWindowsPath(SyncFile syncFile, ShellItem windowsPath) + { + SyncFile copy = new(syncFile); + + copy.UpdatePath(FileHelper.ConcatPaths(windowsPath.ParsingName, syncFile.FullName, '\\')); + copy.PathType = FilePathType.Windows; + + return copy; + } +} + +public class SyncFileComparer : IEqualityComparer +{ + public bool Equals(SyncFile x, SyncFile y) + => x.FullPath.Equals(y.FullPath); + + public int GetHashCode([DisallowNull] SyncFile obj) + { + throw new NotImplementedException(); + } +} diff --git a/ADB Explorer _WpfUi/Models/File/TrashIndexer.cs b/ADB Explorer _WpfUi/Models/File/TrashIndexer.cs new file mode 100644 index 00000000..5db4f78e --- /dev/null +++ b/ADB Explorer _WpfUi/Models/File/TrashIndexer.cs @@ -0,0 +1,83 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Models; + +public class TrashIndexer : ViewModelBase +{ + private string recycleName; + public string RecycleName + { + get => recycleName; + set => Set(ref recycleName, value); + } + + private string originalPath; + public string OriginalPath + { + get => originalPath; + set => Set(ref originalPath, value); + } + + private DateTime? dateModified; + public DateTime? DateModified + { + get => dateModified; + set => Set(ref dateModified, value); + } + + public string ModifiedTimeString => TabularDateFormatter.Format(DateModified, Thread.CurrentThread.CurrentCulture); + + public string IndexerPath => $"{AdbExplorerConst.RECYCLE_PATH}/.{RecycleName}{AdbExplorerConst.RECYCLE_INDEX_SUFFIX}"; + + public string ParentPath + { + get + { + int originalIndex = OriginalPath.LastIndexOf('/'); + Index index; + if (originalIndex == 0) + index = 1; + else if (originalIndex < 0) + index = ^0; + else + index = originalIndex; + + return OriginalPath[..index]; + } + } + + public TrashIndexer() + { } + + public TrashIndexer(string recycleIndex) : this(recycleIndex.Split('|')) + { } + + public TrashIndexer(params string[] recycleIndex) : this(recycleIndex[0], recycleIndex[1], recycleIndex[2]) + { } + + public TrashIndexer(string recycleName, string originalPath, string dateModified) + : this(recycleName, originalPath, DateTime.TryParseExact(dateModified, AdbExplorerConst.ADB_EXPLORER_DATE_FORMAT, null, DateTimeStyles.None, out var res) ? res : null) + { } + + public TrashIndexer(string recycleName, string originalPath, DateTime? dateModified) + { + RecycleName = recycleName; + OriginalPath = originalPath; + DateModified = dateModified; + } + + public TrashIndexer(FileMoveOperation op) + { + RecycleName = op.RecycleName; + OriginalPath = op.FilePath.FullPath; + DateModified = op.DateModified; + } + + public override string ToString() + { + var date = DateModified is null ? "?" : DateModified.Value.ToString(AdbExplorerConst.ADB_EXPLORER_DATE_FORMAT); + return $"{RecycleName}|{OriginalPath}|{date}"; + } +} diff --git a/ADB Explorer _WpfUi/Models/FileOpColumnConfig.cs b/ADB Explorer _WpfUi/Models/FileOpColumnConfig.cs new file mode 100644 index 00000000..eb865d14 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/FileOpColumnConfig.cs @@ -0,0 +1,265 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; +using Wpf.Ui.Controls; + +namespace ADB_Explorer.Models; + +public static class FileOpColumns +{ + public static void Init() + { + List tempList = + [ + new(FileOpColumnConfig.ColumnType.OpType, + (DataTemplate)App.Current.FindResource("OperationIconTemplate"), + 0, + 30, + icon: "\uE8AF", + sortPath: nameof(FileOperation.OperationName)), + new(FileOpColumnConfig.ColumnType.FileName, + (DataTemplate)App.Current.FindResource("FileOpFileNameTemplate"), + 1, + 250, + sortPath: $"{nameof(FileOperation.AndroidPath)}.{nameof(FileOperation.AndroidPath.FullName)}"), + new(FileOpColumnConfig.ColumnType.Progress, + (DataTemplate)App.Current.FindResource("FileOpProgressTemplate"), + 2, + 180, + sortPath: nameof(FileOperation.Filter)), + new(FileOpColumnConfig.ColumnType.Source, + (DataTemplate)App.Current.FindResource("FileOpSourcePathTemplate"), + 3, + 200, + sortPath: nameof(FileOperation.SourcePathString)), + new(FileOpColumnConfig.ColumnType.Dest, + (DataTemplate)App.Current.FindResource("FileOpTargetPathTemplate"), + 4, + 200, + sortPath: nameof(FileOperation.TargetPathString)), + new(FileOpColumnConfig.ColumnType.TimeStamp, + (DataTemplate)App.Current.FindResource("FileOpTimeStampTemplate"), + 5, + 70, + sortPath: nameof(FileOperation.Time)), + new(FileOpColumnConfig.ColumnType.Device, + (DataTemplate)App.Current.FindResource("FileOpDeviceColumnStyle"), + 6, + 100, + visibleByDefault: false, + sortPath: $"{nameof(FileOperation.Device)}.{nameof(FileOperation.Device.Device)}.{nameof(FileOperation.Device.Device.Name)}"), + ]; + + List = [.. tempList.OrderBy(c => c.Index)]; + + UpdateCheckedColumns(); + } + + public static void UpdateCheckedColumns() => + CheckedColumnsCount.Value = List.Count(col => col.IsChecked is true); + + public static void UpdateColumnIndexes() => + List.ForEach(c => c.Index = c.Column.DisplayIndex); + + public static List List { get; private set; } = []; + + public static ObservableProperty CheckedColumnsCount { get; set; } = new(); +} + +public class FileOpColumnConfig : ViewModelBase +{ + public enum ColumnType + { + OpType, + FileName, + Progress, + Source, + Dest, + TimeStamp, + Device, + } + + #region Full properties + + private bool? isChecked; + public bool? IsChecked + { + get => isChecked; + set + { + if (Set(ref isChecked, value)) + { + if (Column is not null) + Column.Visibility = VisibilityHelper.Visible(value); + + FileOpColumns.UpdateCheckedColumns(); + + Store(); + } + } + } + + private int index; + public int Index + { + get => index; + set + { + if (Set(ref index, value)) + { + if (Column is not null && value >= 0) + Column.DisplayIndex = value; + + Store(); + } + } + } + + private double columnWidth; + public double ColumnWidth + { + get => columnWidth; + set + { + if (Set(ref columnWidth, value)) + { + if (Column is not null) + Column.Width = value; + + Store(); + } + } + } + + #endregion + + #region Read-only properties + + private object header = null; + public object Header + { + get + { + if (header is null) + { + header = string.IsNullOrEmpty(Icon) + ? Name + : new FontIcon() + { + Glyph = Icon, + ToolTip = Name, + }; + } + + return header; + } + } + + private Style headerStyle = null; + public Style HeaderStyle + { + get + { + if (headerStyle is null) + { + void FileOpColumnHeader_SizeChanged(object sender, SizeChangedEventArgs e) + => ColumnWidth = e.NewSize.Width; + + headerStyle = new() + { + TargetType = typeof(DataGridColumnHeader), + BasedOn = App.Current.FindResource("FileOpColumnHeaderStyle") as Style + }; + + headerStyle.Setters.Add(new EventSetter(FrameworkElement.SizeChangedEvent, new SizeChangedEventHandler(FileOpColumnHeader_SizeChanged))); + } + + return headerStyle; + } + } + + private DataGridColumn column = null; + public DataGridColumn Column + { + get + { + if (column is null) + { + column = new DataGridTemplateColumn() + { + Header = Header, + CellTemplate = CellTemplate, + HeaderStyle = HeaderStyle, + CanUserResize = string.IsNullOrEmpty(Icon), + SortMemberPath = SortPath, + Visibility = VisibilityHelper.Visible(IsChecked), + Width = ColumnWidth, + DisplayIndex = Index, + }; + } + + return column; + } + } + + public DataTemplate CellTemplate { get; } + + public string Icon { get; } + + public ColumnType Type { get; } + + public string SortPath { get; } + + public string Name => TypeString(Type); + + public bool IsEnabled => IsChecked is false || FileOpColumns.CheckedColumnsCount > 1; + + #endregion + + public FileOpColumnConfig(ColumnType type, DataTemplate cellTemplate, int defaultIndex, double defaultWidth = 0, string icon = null, bool visibleByDefault = true, string sortPath = "") + { + Type = type; + Icon = icon; + CellTemplate = cellTemplate; + SortPath = sortPath; + + bool isChecked = visibleByDefault; + int index = defaultIndex; + double width = defaultWidth; + + if (Retrieve() is string storage && storage.Count(c => c == ',') == 2) + { + var split = storage.Split(','); + if (!bool.TryParse(split[0], out isChecked)) + isChecked = visibleByDefault; + + if (!int.TryParse(split[1], out index)) + index = -1; + + if (!string.IsNullOrEmpty(icon) || !double.TryParse(split[2], out width)) + width = defaultWidth; + } + + IsChecked = isChecked; + Index = index; + ColumnWidth = width; + + FileOpColumns.CheckedColumnsCount.PropertyChanged += (sender, e) => OnPropertyChanged(nameof(IsEnabled)); + } + + public static string TypeString(ColumnType columnType) => columnType switch + { + ColumnType.OpType => Strings.Resources.S_COLUMN_OP_TYPE, + ColumnType.FileName => Strings.Resources.S_COLUMN_FILE_NAME, + ColumnType.Progress => Strings.Resources.S_COLUMN_PROGRESS, + ColumnType.Source => Strings.Resources.S_COLUMN_SOURCE, + ColumnType.Dest => Strings.Resources.S_COLUMN_DESTINATION, + ColumnType.TimeStamp => Strings.Resources.S_COLUMN_ADDED, + ColumnType.Device => Strings.Resources.S_SETTINGS_GROUP_DEVICE, + _ => throw new NotSupportedException(), + }; + + private string Retrieve() => Storage.RetrieveValue(Type.ToString())?.ToString(); + + private void Store() => Storage.StoreValue(Type.ToString(), $"{IsChecked},{Index},{ColumnWidth}"); +} diff --git a/ADB Explorer _WpfUi/Models/FileOpFilter.cs b/ADB Explorer _WpfUi/Models/FileOpFilter.cs new file mode 100644 index 00000000..ce318fa0 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/FileOpFilter.cs @@ -0,0 +1,121 @@ +using ADB_Explorer.Helpers; +using ADB_Explorer.Services; +using ADB_Explorer.ViewModels; + +namespace ADB_Explorer.Models; + +public static class FileOpFilters +{ + private static List list = null; + public static List List + { + get + { + if (list is null) + { + list = [.. Enum.GetValues().Select(f => new FileOpFilter(f))]; + + UpdateCheckedColumns(); + } + + return list; + } + } + + public static ObservableProperty CheckedFilterCount { get; private set; } = new() { Value = -1 }; + + public static void UpdateCheckedColumns() => + CheckedFilterCount.Value = List.Count(col => col.IsChecked is true); +} + +public class FileOpFilter : ViewModelBase +{ + public enum FilterType + { + Running, + Pending, + Completed, + Validated, + Failed, + Canceled, + Previous, + } + + private bool? isChecked = null; + public bool? IsChecked + { + get => isChecked; + set + { + if (Set(ref isChecked, value)) + { + if (FileOpFilters.CheckedFilterCount > 0) + FileOpFilters.UpdateCheckedColumns(); + + Store(); + } + } + } + + private CheckBox checkBox = null; + public CheckBox CheckBox + { + get + { + if (checkBox is null) + { + checkBox = new CheckBox() + { + Style = (Style)App.Current.FindResource("FileOpFilterCheckBox"), + DataContext = this, + Content = GetFilterName(Type), + Margin = new(0, -6, 0, -6), + }; + } + + return checkBox; + } + } + + public static string GetFilterName(FilterType filterType) + => filterType switch + { + FilterType.Running => Strings.Resources.S_FILEOP_RUNNING, + FilterType.Pending => Strings.Resources.S_FILEOP_WAITING, + FilterType.Completed => Strings.Resources.S_FILEOP_COMPLETED, + FilterType.Validated => Strings.Resources.S_FILEOP_VALIDATED, + FilterType.Failed => Strings.Resources.S_FILEOP_FAILED, + FilterType.Canceled => Strings.Resources.S_FILEOP_CANCELED, + FilterType.Previous => Strings.Resources.S_FILEOP_PREVIOUS, + _ => throw new NotSupportedException() + }; + + public FilterType Type { get; } + + public bool IsEnabled + { + get + { + if (IsChecked is null) + return false; + + return IsChecked is false || FileOpFilters.CheckedFilterCount > 1; + } + } + + public FileOpFilter(FilterType type) + { + Type = type; + + if (type is FilterType.Running) + IsChecked = null; + else + IsChecked = Retrieve() is bool val ? val : true; + + FileOpFilters.CheckedFilterCount.PropertyChanged += (object sender, PropertyChangedEventArgs e) => OnPropertyChanged(nameof(IsEnabled)); + } + + private bool? Retrieve() => Storage.RetrieveBool($"{nameof(FilterType)}_{Type}"); + + private void Store() => Storage.StoreValue($"{nameof(FilterType)}_{Type}", $"{IsChecked}"); +} diff --git a/ADB Explorer _WpfUi/Models/Log.cs b/ADB Explorer _WpfUi/Models/Log.cs new file mode 100644 index 00000000..2b75aa24 --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Log.cs @@ -0,0 +1,19 @@ +namespace ADB_Explorer.Models; + +public class Log +{ + public string Content { get; set; } + + public DateTime TimeStamp { get; set; } + + public Log(string content, DateTime? timeStamp = null) + { + Content = content; + TimeStamp = timeStamp is null ? DateTime.Now : timeStamp.Value; + } + + public override string ToString() + { + return $"{TimeStamp:HH:mm:ss:fff} ⁞ {Content}"; + } +} diff --git a/ADB Explorer _WpfUi/Models/Static/AdbRegEx.cs b/ADB Explorer _WpfUi/Models/Static/AdbRegEx.cs new file mode 100644 index 00000000..7ff2a5af --- /dev/null +++ b/ADB Explorer _WpfUi/Models/Static/AdbRegEx.cs @@ -0,0 +1,71 @@ +namespace ADB_Explorer.Models +{ + public static partial class AdbRegEx + { + [GeneratedRegex(@"^(?[0-9a-f]+) (?[0-9a-f]+) (?