From dc3d9b7915e99bc0619dc65a4073618216bf57a4 Mon Sep 17 00:00:00 2001 From: Fin Date: Tue, 25 Oct 2022 17:30:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=BC=E5=85=A5=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bark.xcodeproj/project.pbxproj | 4 + Bark/en.lproj/Localizable.strings | 4 + Bark/zh-Hans.lproj/Localizable.strings | 4 + .../MessageSettingsViewController.swift | 124 +++++++++++++++++- Controller/MessageSettingsViewModel.swift | 83 +++++++++++- Model/Object+Dictionary.swift | 40 ++++++ View/iCloudStatusCell.swift | 1 + 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 Model/Object+Dictionary.swift diff --git a/Bark.xcodeproj/project.pbxproj b/Bark.xcodeproj/project.pbxproj index a22a10b..5baaba6 100644 --- a/Bark.xcodeproj/project.pbxproj +++ b/Bark.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ 06BBB8C12567B3EF0076F63E /* BaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8C02567B3EF0076F63E /* BaseTableViewCell.swift */; }; 06BBB8C92567B6730076F63E /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8C82567B6730076F63E /* Operators.swift */; }; 06BBB8CE2567B8E60076F63E /* silence.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06BBB8CD2567B8E60076F63E /* silence.caf */; }; + 06BD4DAA2901352E003364DB /* Object+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BD4DA92901352E003364DB /* Object+Dictionary.swift */; }; 06C2CF232685B88D0034B127 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C2CF222685B88D0034B127 /* TextCell.swift */; }; 06C2CF252685BDB80034B127 /* SpacerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C2CF242685BDB80034B127 /* SpacerCell.swift */; }; 06C5952D2480E3F8006B98F3 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5952C2480E3F8006B98F3 /* LabelCell.swift */; }; @@ -274,6 +275,7 @@ 06BBB8C02567B3EF0076F63E /* BaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewCell.swift; sourceTree = ""; }; 06BBB8C82567B6730076F63E /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; 06BBB8CD2567B8E60076F63E /* silence.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = silence.caf; sourceTree = ""; }; + 06BD4DA92901352E003364DB /* Object+Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Object+Dictionary.swift"; sourceTree = ""; }; 06C2CF222685B88D0034B127 /* TextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = ""; }; 06C2CF242685BDB80034B127 /* SpacerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacerCell.swift; sourceTree = ""; }; 06C5952C2480E3F8006B98F3 /* LabelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCell.swift; sourceTree = ""; }; @@ -404,6 +406,7 @@ children = ( 06B1158E247BB1FB006D91FB /* Message.swift */, 064CABA5256BE9510018155C /* PreviewModel.swift */, + 06BD4DA92901352E003364DB /* Object+Dictionary.swift */, ); path = Model; sourceTree = ""; @@ -952,6 +955,7 @@ 060481EE250F404500BC9799 /* SoundsViewController.swift in Sources */, 0603706D20E23EC000F4CA05 /* BarkSFSafariViewController.swift in Sources */, 06AE311A266F4E6600B39FBB /* GroupFilterViewModel.swift in Sources */, + 06BD4DAA2901352E003364DB /* Object+Dictionary.swift in Sources */, 06C5953124811392006B98F3 /* ArchiveSettingCell.swift in Sources */, 068EC15A27ED99E700D5D11E /* ServerListViewModel.swift in Sources */, 06172FDC27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift in Sources */, diff --git a/Bark/en.lproj/Localizable.strings b/Bark/en.lproj/Localizable.strings index 23775e8..42c650c 100644 --- a/Bark/en.lproj/Localizable.strings +++ b/Bark/en.lproj/Localizable.strings @@ -119,3 +119,7 @@ deleteFailed = "You must have at least one server configured."; deletedSuccessfully = "Server has been deleted successfully."; resetFailed = "Reset Failed"; resetFailed2 = "Bark could not get a DeviceToken from the server."; +export = "Export"; +import = "Import"; +exportOrImport = "Exporting or importing messages"; +items = "messages"; diff --git a/Bark/zh-Hans.lproj/Localizable.strings b/Bark/zh-Hans.lproj/Localizable.strings index 1b9d0bb..4df7cff 100644 --- a/Bark/zh-Hans.lproj/Localizable.strings +++ b/Bark/zh-Hans.lproj/Localizable.strings @@ -122,3 +122,7 @@ deleteFailed = "不能删除,服务器至少需保留一个"; deletedSuccessfully = "删除成功"; resetFailed = "重置失败"; resetFailed2 = "重置失败,没有获取到 DeviceToken"; +export = "导出"; +import = "导入"; +exportOrImport = "导出或导入消息列表"; +items = "条消息"; diff --git a/Controller/MessageSettingsViewController.swift b/Controller/MessageSettingsViewController.swift index cd9751a..a641af3 100644 --- a/Controller/MessageSettingsViewController.swift +++ b/Controller/MessageSettingsViewController.swift @@ -7,9 +7,13 @@ // import Material +import RxCocoa import RxDataSources +import RxSwift import UIKit -class MessageSettingsViewController: BaseViewController { +import UniformTypeIdentifiers + +class MessageSettingsViewController: BaseViewController, UIDocumentPickerDelegate { let tableView: UITableView = { let tableView = UITableView() tableView.separatorStyle = .none @@ -32,12 +36,92 @@ class MessageSettingsViewController: BaseViewController() + + /// 生成导入导出事件 + func getBackupOrRestoreAction() -> (Driver, Driver) { + let backupOrRestoreAction = self.tableView.rx + .modelSelected(MessageSettingItem.self) + .filter { item in + if case MessageSettingItem.backup = item { + return true + } + return false + } + .flatMapLatest { [weak self] _ in + guard let strongSelf = self else { + return Observable.empty() + } + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: NSLocalizedString("export"), style: .default, handler: { _ in + strongSelf.backupOrRestoreActionRelay.accept(.export) + })) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("import"), style: .default, handler: { [weak self] _ in + if #available(iOS 14.0, *) { + let supportedType: [UTType] = [UTType.json] + let pickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedType, asCopy: false) + pickerViewController.delegate = self + self?.present(pickerViewController, animated: true, completion: nil) + } + })) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil)) + strongSelf.navigationController?.present(alertController, animated: true, completion: nil) + return strongSelf.backupOrRestoreActionRelay.asObservable() + } + + let backupAction = backupOrRestoreAction + .filter { action in + if case .export = action { return true } else { return false } + } + .map { _ in () } + .asDriver(onErrorDriveWith: .empty()) + + let restoreAction = backupOrRestoreAction + .compactMap { action in + if case let .import(data) = action { return data } else { return nil } + } + .asDriver(onErrorDriveWith: .empty()) + + return (backupAction, restoreAction) + } + override func bindViewModel() { + let actions = getBackupOrRestoreAction() let output = viewModel.transform( input: MessageSettingsViewModel.Input( itemSelected: self.tableView.rx.modelSelected(MessageSettingItem.self).asDriver(), - deviceToken: Client.shared.deviceToken.asDriver() + deviceToken: Client.shared.deviceToken.asDriver(), + backupAction: actions.0, + restoreAction: actions.1, + viewDidAppear: self.rx.methodInvoked(#selector(viewDidAppear(_:))) + .map { _ in () } ) ) @@ -52,6 +136,12 @@ class MessageSettingsViewController: BaseViewController var deviceToken: Driver + var backupAction: Driver + var restoreAction: Driver + var viewDidAppear: Observable } struct Output { var settings: Driver<[SectionModel]> var openUrl: Driver var copyDeviceToken: Driver + var exportData: Driver } func transform(input: Input) -> Output { + + let restoreSuccess = input + .restoreAction + .compactMap { data -> Void? in + guard let json = try? JSON(data: data), let arr = json.array else { + return nil + } + guard let realm = try? Realm() else { + return nil + } + try? realm.write { + for message in arr { + guard let id = message["id"].string else { + continue + } + guard let createDate = message["createDate"].int64 else { + continue + } + + let title = message["title"].string + let body = message["body"].string + let url = message["url"].string + let group = message["group"].string + + let messageObject = Message() + messageObject.id = id + messageObject.title = title + messageObject.body = body + messageObject.url = url + messageObject.group = group + messageObject.createDate = Date(timeIntervalSince1970: TimeInterval(createDate)) + realm.add(messageObject, update: .modified) + } + } + return () + }.asObservable().share() + let settings: [MessageSettingItem] = { var settings = [MessageSettingItem]() settings.append(.label(text: "iCloud")) settings.append(.iCloudStatus) settings.append(.label(text: NSLocalizedString("iCloudSync"))) + settings.append(.backup(viewModel: MutableTextCellViewModel( + title: "\(NSLocalizedString("export"))/\(NSLocalizedString("import"))", + text: Observable.merge([restoreSuccess, input.viewDidAppear]) + .map { _ in + if let realm = try? Realm() { + return realm.objects(Message.self) + .filter("isDeleted != true") + .count + } + return 0 + } + .map { count in + "\(count) \(NSLocalizedString("items"))" + } + .asDriver(onErrorDriveWith: .empty())) + )) + settings.append(.label(text: NSLocalizedString("exportOrImport"))) settings.append(.archiveSetting(viewModel: ArchiveSettingCellViewModel(on: ArchiveSettingManager.shared.isArchive))) settings.append(.label(text: NSLocalizedString("archiveNote"))) @@ -112,11 +172,30 @@ class MessageSettingsViewModel: ViewModel, ViewModelType { return nil } + // 导出数据 + let exportSuccess = input.backupAction + .asObservable() + .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + .compactMap { _ in + if let realm = try? Realm() { + let messages = realm.objects(Message.self) + .filter("isDeleted != true") + + var arr = [[String: AnyObject]]() + for message in messages { + arr.append(message.toDictionary()) + } + return try? JSON(arr).rawData(options: JSONSerialization.WritingOptions.prettyPrinted) + } + return nil + } + return Output( settings: Driver<[SectionModel]> .just([SectionModel(model: "model", items: settings)]), openUrl: openUrl, - copyDeviceToken: copyDeviceToken) + copyDeviceToken: copyDeviceToken, + exportData: exportSuccess.asDriver(onErrorDriveWith: .empty())) } } @@ -129,6 +208,8 @@ enum MessageSettingItem { case archiveSetting(viewModel: ArchiveSettingCellViewModel) // 带 详细按钮的 文本cell case detail(title: String?, text: String?, textColor: UIColor?, url: URL?) + // 备份还原按钮 + case backup(viewModel: MutableTextCellViewModel) // deviceToken case deviceToken(viewModel: MutableTextCellViewModel) // 分隔线 diff --git a/Model/Object+Dictionary.swift b/Model/Object+Dictionary.swift new file mode 100644 index 0000000..c5e7fc6 --- /dev/null +++ b/Model/Object+Dictionary.swift @@ -0,0 +1,40 @@ +// +// Object+Dictionary.swift +// Bark +// +// Created by huangfeng on 2022/10/20. +// Copyright © 2022 Fin. All rights reserved. +// + +import Foundation +import RealmSwift + +extension Object { + func toDictionary() -> [String: AnyObject] { + let properties = self.objectSchema.properties.map { $0.name } + var dicProps = [String: AnyObject]() + for (key, value) in self.dictionaryWithValues(forKeys: properties) { + if let value = value as? ListBase { + dicProps[key] = value.toArray1() as AnyObject + } else if let value = value as? Object { + dicProps[key] = value.toDictionary() as AnyObject + } else if let value = value as? Date { + dicProps[key] = Int64(value.timeIntervalSince1970) as AnyObject + } else { + dicProps[key] = value as AnyObject + } + } + return dicProps + } +} + +extension ListBase { + func toArray1() -> [AnyObject] { + var _toArray = [AnyObject]() + for i in 0 ..< self._rlmArray.count { + let obj = unsafeBitCast(self._rlmArray[i], to: Object.self) + _toArray.append(obj.toDictionary() as AnyObject) + } + return _toArray + } +} diff --git a/View/iCloudStatusCell.swift b/View/iCloudStatusCell.swift index ef76151..bdb68eb 100644 --- a/View/iCloudStatusCell.swift +++ b/View/iCloudStatusCell.swift @@ -16,6 +16,7 @@ class iCloudStatusCell: UITableViewCell { self.backgroundColor = BKColor.background.secondary self.textLabel?.text = NSLocalizedString("iCloudSatatus") self.detailTextLabel?.text = "" + self.detailTextLabel?.textColor = BKColor.grey.darken2 CKContainer.default().accountStatus { status, _ in dispatch_sync_safely_main_queue { switch status {