diff --git a/VoidLink/AlertControllerUtil.swift b/VoidLink/AlertControllerUtil.swift index 41fbe735..3cd75f8c 100644 --- a/VoidLink/AlertControllerUtil.swift +++ b/VoidLink/AlertControllerUtil.swift @@ -31,7 +31,7 @@ import UIKit @objc public static var autoCompletion:Bool = false @objc class func showAlert( - in viewController: UIViewController, + in viewController: UIViewController? = nil, title: String?, message: String?, withCancel: Bool, @@ -43,6 +43,8 @@ import UIKit var remainingSeconds = countdown let originalMessage = message var isAlertDismissed = false + + guard let viewController = viewController else {return} alertController = UIAlertController( title: title, diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 2289cc3e..9c839009 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -3564,7 +3564,7 @@ import ObjectiveC.runtime offshootRootFolders.remove(currentRootFolder) for folder in offshootRootFolders { - print("offshootRootFolder \(CACurrentMediaTime()) \(folder.label.text ?? "")") + // print("offshootRootFolder \(CACurrentMediaTime()) \(folder.label.text ?? "")") } guard !folder.sequenceSet.isEmpty else {return} @@ -3596,7 +3596,7 @@ import ObjectiveC.runtime return widgetRef } - private static func getParentFolders(of widget: OnScreenWidgetView) -> Set { + static func getParentFolders(of widget: OnScreenWidgetView) -> Set { var parents = Set() var current = widget while let parent = OnScreenWidgetView.mapping[current.parentSequence] { @@ -3637,6 +3637,9 @@ import ObjectiveC.runtime } widget.parentSequence = -1 if widget.isFolder { + if widget.folded { + OnScreenWidgetView.set(folded: false, for: widget) + } for sequence in widget.sequenceSet { guard let subWidget = OnScreenWidgetView.mapping[sequence] else {continue} subWidget.parentSequence = -1 diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 1450c765..3958bac5 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -3455,6 +3455,29 @@ } } }, + "Detailed Tutorial" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "详细教程" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "詳細教程" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "詳細教程" + } + } + } + }, "Detecting..." : { "extractionState" : "manual", "localizations" : { @@ -5022,6 +5045,87 @@ } } }, + "Folder Button" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "控件夹按键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "控制項夾按鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "控制項夾按鍵" + } + } + } + }, + "folderTutorialTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "\nDrag other widgets (buttons or touchpads). Release when the touch point enters the folder’s bounding frame to complete collecting.\n\n- When the folder is unfolded, the collected widget will snap back to its original position.\n\n- When the folder is folded, the grouped widget will be hidden.\n\n- Drag a sub-widget or a folder over the trash icon (do not release) to detach it from the collection." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "\n拖动其他控件(按键或触控板),当手指触点进入控件夹边框时松开,即可完成收纳。\n\n- 当控件夹处于展开状态时, 被收纳控件将弹回原先的位置。\n\n- 当控件夹处于折叠状态时,被收纳控件将隐藏。\n\n- 拖动子控件或控件夹划过垃圾桶图标(不要松开),即可解除收纳关系。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "\n拖動其他螢幕控制項(按鍵或觸控板),當手指觸點進入資料夾邊框時放開,即可完成收納。\n\n- 當控制項夾處於展開狀態時,被收納的控制項會彈回原本的位置。\n\n- 當控制項夾處於折疊狀態時,被收納的控制項會隱藏。\n\n- 拖動子控制項或控制項夾按键,劃過垃圾桶圖示(不要放開),即可解除收納關係。\n" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "\n拖動其他螢幕控制項(按鍵或觸控板),當手指觸點進入資料夾邊框時放開,即可完成收納。\n\n- 當控制項夾處於展開狀態時,被收納的控制項會彈回原本的位置。\n\n- 當控制項夾處於折疊狀態時,被收納的控制項會隱藏。\n\n- 拖動子控制項或控制項夾按键,劃過垃圾桶圖示(不要放開),即可解除收納關係。\n" + } + } + } + }, + "folderTutorialUrl" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/it0qruyul6be8gr1" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/li9ogn1rlb50fn52" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/li9ogn1rlb50fn52" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/li9ogn1rlb50fn52" + } + } + } + }, "Force pencil entering hovering on host" : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/Utility/GenericUtils.swift b/VoidLink/Utility/GenericUtils.swift index 12d3248e..f5cc3923 100644 --- a/VoidLink/Utility/GenericUtils.swift +++ b/VoidLink/Utility/GenericUtils.swift @@ -77,6 +77,18 @@ import UIKit return false } + @objc static func isFirstTappingFolderInLayoutTool() -> Bool { + let key = "hasTappedFolderInLayoutTool" + let defaults = UserDefaults.standard + let launchedBefore = defaults.bool(forKey: key) + + if !launchedBefore { + defaults.set(true, forKey: key) + return true + } + return false + } + @objc static func isFirstTappingInputAccessoryBar() -> Bool { let key = "isFirstTappingInputAccessoryBar" let defaults = UserDefaults.standard @@ -275,6 +287,13 @@ import UIKit @objc static func toCGFloat(_ str: String) -> CGFloat { return CGFloat(Double(str) ?? 0) } + + @objc(openUrl:) + static func openUrl(_ urlString: String) { + guard let url = URL(string: urlString), + UIApplication.shared.canOpenURL(url) else { return } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } @objc static func isLandscape() -> Bool { if #available(iOS 13.0, *) { diff --git a/VoidLink/Utility/IAPManager.swift b/VoidLink/Utility/IAPManager.swift index 16394946..52177aee 100644 --- a/VoidLink/Utility/IAPManager.swift +++ b/VoidLink/Utility/IAPManager.swift @@ -400,9 +400,7 @@ import StoreKit let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.learnMore, userInfo:["interruption": PurchaseInterruption.learnMore.rawValue]) - if let url = URL(string: product.productURL()) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } + GenericUtils.openUrl(product.productURL()) return } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift index fd6084df..b8294df8 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift @@ -368,6 +368,10 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg private func loadWidgets(from oscProfile: OSCProfile, to folder: OnScreenWidgetView? = nil) { var sequence: Int16 = folder == nil ? -1 : (folder?.getAvailableSequence() ?? -1) + var importedWidgetSequenceMap: [Int16: Int16] = [:] + var independentWidgetSequencesPriorToImport: [Int16] = [] + var newWidgetBatch: Set = Set() + var hasLegacyWidget = false for case let encoded as Data in oscProfile.buttonStatesEncoded { guard let buttonState = self.profilesManager?.unarchiveButtonStateEncoded(encoded) else { continue } @@ -427,14 +431,31 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg guard let folder = folder else { continue } if !folder.folded { - folder.sequenceSet.insert(widgetView.sequence) - widgetView.parentSequence = folder.sequence + if widgetView.parentSequence == -1 { + folder.sequenceSet.insert(widgetView.sequence) + widgetView.parentSequence = folder.sequence + independentWidgetSequencesPriorToImport.append(widgetView.sequence) + } + importedWidgetSequenceMap[buttonState.sequence] = widgetView.sequence + newWidgetBatch.insert(widgetView) } } else if buttonState.widgetType == 0 { hasLegacyWidget = true } } + + if let folder = folder { + for widget in newWidgetBatch where + widget.isFolder + && widget.parentSequence != -1 + && widget != folder + { + widget.sequenceSet = Set(widget.sequenceSet.map {importedWidgetSequenceMap[$0] ?? -1}) + if !independentWidgetSequencesPriorToImport.contains(widget.sequence) {widget.parentSequence = importedWidgetSequenceMap[widget.parentSequence] ?? -1} + // print("label: \(widget.widgetLabel) sequence: \(widget.sequence) set: \(widget.sequenceSet) parent: \(String(describing: OnScreenWidgetView.mapping[widget.parentSequence]?.widgetLabel))") + } + } for case let widget as OnScreenWidgetView in self.onScreenWidgetViews { widget.accessWidgetAttributes() @@ -984,6 +1005,24 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg autoFitStack(widgetPanelStack) } + private func popFolderTutorialTip() { + AlertControllerUtil.cancelButtonString = LocalizationHelper.localizedString(forKey: "Detailed Tutorial") + AlertControllerUtil.showAlert( + in: self, + title: LocalizationHelper.localizedString(forKey: "Folder Button"), + message: LocalizationHelper.localizedString(forKey: "folderTutorialTip"), + withCancel: true, + buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "Got it!"), + countdown: 5, + completion: { + if AlertControllerUtil.actionCancelled { + GenericUtils.openUrl(LocalizationHelper.localizedString(forKey: "folderTutorialUrl")) + } + else {return} + } + ) + } + @objc private func widgetViewTapped(_ notification: Notification) { guard let widgetView = notification.object as? OnScreenWidgetView else { return } selectedWidgetView = widgetView @@ -993,6 +1032,10 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg return } refreshPanelForSelectedWidget(widgetView) + + if (widgetView.isFolder && GenericUtils.isFirstTappingFolderInLayoutTool()) { + self.popFolderTutorialTip() + } } @objc private func legacyOscLayerTapped(_ notification: Notification) { diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 111e1d8d..4f8bec17 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -1788,10 +1788,7 @@ BOOL isCustomResolution(int resolutionSelected) { UIAlertAction *readInstruction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Online Documentation"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ - NSURL *url = [NSURL URLWithString:onlineDocLink]; - if ([[UIApplication sharedApplication] canOpenURL:url]) { - [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; - } + [GenericUtils openUrl:onlineDocLink]; }];