diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index 6980936d..68528c74 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 140158992A430DD000D14E52 /* ThirdPartyLicense.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140158982A430DD000D14E52 /* ThirdPartyLicense.swift */; }; 1401589B2A45A07200D14E52 /* WikimediaAndScribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1401589A2A45A07200D14E52 /* WikimediaAndScribe.swift */; }; - 140158A22A4EDB2200D14E52 /* TableViewTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140158A12A4EDB2200D14E52 /* TableViewTemplateViewController.swift */; }; 1406B7872A2DFCDD001DF45B /* AboutTableData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1406B7862A2DFCDD001DF45B /* AboutTableData.swift */; }; 1406B78C2A3209CF001DF45B /* AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1406B78B2A3209CF001DF45B /* AppExtensions.swift */; }; 146B70BD2A853A3800710BD4 /* SwipeableTabBarController in Frameworks */ = {isa = PBXBuildFile; productRef = 146B70BC2A853A3800710BD4 /* SwipeableTabBarController */; }; @@ -123,13 +122,13 @@ 38BD214F22D592CA00C6795D /* DEKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BD214E22D592CA00C6795D /* DEKeyboardViewController.swift */; }; 38BD215322D592CA00C6795D /* German.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 38BD214C22D592CA00C6795D /* German.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 38DD94F122D6A40000FF8845 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DD94F022D6A40000FF8845 /* Extensions.swift */; }; - 5A68DA412CDE7B7A00897FAD /* RadioTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A68DA3D2CDE7B7900897FAD /* RadioTableViewCell.swift */; }; - 5A68DA422CDE7B7A00897FAD /* RadioTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A68DA3E2CDE7B7900897FAD /* RadioTableViewCell.xib */; }; - 5A68DA432CDE7B7A00897FAD /* SelectionViewTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A68DA3F2CDE7B7900897FAD /* SelectionViewTemplateViewController.swift */; }; 5A8FFB6B2C5A9D9C00F4B571 /* English.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1AFDF3D29CA66D00033BF27 /* English.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 693150472C881DCE005F99E8 /* BaseTableViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693150462C881DCE005F99E8 /* BaseTableViewControllerTest.swift */; }; 69B81EBC2BFB8C77008CAB85 /* TipCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B81EBB2BFB8C77008CAB85 /* TipCardView.swift */; }; 84AF4D882C3575EA009AE0D2 /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF4D872C3575EA009AE0D2 /* UIDeviceExtensions.swift */; }; + 8B82A46F2F7D6E0400F1B04D /* TranslationLanguagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B82A46E2F7D6E0300F1B04D /* TranslationLanguagePickerView.swift */; }; + 8BA036E22F7FCA2D00F5ADF9 /* LanguageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA036E12F7FCA2D00F5ADF9 /* LanguageSettingsView.swift */; }; + 8BA036E42F7FCDC300F5ADF9 /* SettingsHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA036E32F7FCDC300F5ADF9 /* SettingsHomeView.swift */; }; A4C829A12F8400800015D657 /* ConjugateTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = F725CAE42F6A782500A8C950 /* ConjugateTab.swift */; }; A4C829A52F84366C0015D657 /* InstallationDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96111472F04EC62001E4F95 /* InstallationDownload.swift */; }; CE1378C428F5D7AC00E1CBC2 /* ScribeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1378C228F5D7AC00E1CBC2 /* ScribeColor.swift */; }; @@ -771,7 +770,6 @@ F786BB292F1E8F70003F7505 /* IDLanguageData.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D16150FB2E9DBDC500131732 /* IDLanguageData.sqlite */; }; F786BB2A2F1E8F70003F7505 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D1895BD12C1D816F009FBEB0 /* Settings.bundle */; }; F786BB2B2F1E8F70003F7505 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 38BD213F22D5908100C6795D /* LaunchScreen.storyboard */; }; - F786BB2C2F1E8F70003F7505 /* RadioTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A68DA3E2CDE7B7900897FAD /* RadioTableViewCell.xib */; }; F786BB2D2F1E8F70003F7505 /* InfoChildTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 147797AF2A2CD3370044A53E /* InfoChildTableViewCell.xib */; }; F786BB2E2F1E8F70003F7505 /* AboutTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = ED2486F22B0B4E8C0038AE6A /* AboutTableViewCell.xib */; }; F786BB2F2F1E8F70003F7505 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = E93179A32F03AE77002ED334 /* Localizable.xcstrings */; }; @@ -1017,7 +1015,6 @@ /* Begin PBXFileReference section */ 140158982A430DD000D14E52 /* ThirdPartyLicense.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyLicense.swift; sourceTree = ""; }; 1401589A2A45A07200D14E52 /* WikimediaAndScribe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikimediaAndScribe.swift; sourceTree = ""; }; - 140158A12A4EDB2200D14E52 /* TableViewTemplateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewTemplateViewController.swift; sourceTree = ""; }; 1406B7862A2DFCDD001DF45B /* AboutTableData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTableData.swift; sourceTree = ""; }; 1406B78B2A3209CF001DF45B /* AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensions.swift; sourceTree = ""; }; 144B56F22A568AC200C2F447 /* Scribe.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Scribe.entitlements; sourceTree = ""; }; @@ -1049,12 +1046,12 @@ 38BD214E22D592CA00C6795D /* DEKeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DEKeyboardViewController.swift; sourceTree = ""; }; 38BD215022D592CA00C6795D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38DD94F022D6A40000FF8845 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - 5A68DA3D2CDE7B7900897FAD /* RadioTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioTableViewCell.swift; sourceTree = ""; }; - 5A68DA3E2CDE7B7900897FAD /* RadioTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RadioTableViewCell.xib; sourceTree = ""; }; - 5A68DA3F2CDE7B7900897FAD /* SelectionViewTemplateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionViewTemplateViewController.swift; sourceTree = ""; }; 693150462C881DCE005F99E8 /* BaseTableViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewControllerTest.swift; sourceTree = ""; }; 69B81EBB2BFB8C77008CAB85 /* TipCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCardView.swift; sourceTree = ""; }; 84AF4D872C3575EA009AE0D2 /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = ""; }; + 8B82A46E2F7D6E0300F1B04D /* TranslationLanguagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationLanguagePickerView.swift; sourceTree = ""; }; + 8BA036E12F7FCA2D00F5ADF9 /* LanguageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSettingsView.swift; sourceTree = ""; }; + 8BA036E32F7FCDC300F5ADF9 /* SettingsHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHomeView.swift; sourceTree = ""; }; CE1378C228F5D7AC00E1CBC2 /* ScribeColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScribeColor.swift; sourceTree = ""; }; CE1378C328F5D7AC00E1CBC2 /* UIColor+ScribeColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+ScribeColors.swift"; sourceTree = ""; }; D109A20B275B6888005E2271 /* French.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = French.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1695,8 +1692,6 @@ 5A68DA402CDE7B7900897FAD /* RadioTableViewCell */ = { isa = PBXGroup; children = ( - 5A68DA3D2CDE7B7900897FAD /* RadioTableViewCell.swift */, - 5A68DA3E2CDE7B7900897FAD /* RadioTableViewCell.xib */, ); path = RadioTableViewCell; sourceTree = ""; @@ -2030,10 +2025,11 @@ EDB4601F2B03B3B400BEA967 /* Views */ = { isa = PBXGroup; children = ( + 8BA036E32F7FCDC300F5ADF9 /* SettingsHomeView.swift */, + 8BA036E12F7FCA2D00F5ADF9 /* LanguageSettingsView.swift */, 5A68DA442CDE7D2900897FAD /* Cells */, EDB460202B03B3E400BEA967 /* BaseTableViewController.swift */, - 5A68DA3F2CDE7B7900897FAD /* SelectionViewTemplateViewController.swift */, - 140158A12A4EDB2200D14E52 /* TableViewTemplateViewController.swift */, + 8B82A46E2F7D6E0300F1B04D /* TranslationLanguagePickerView.swift */, ); path = Views; sourceTree = ""; @@ -2670,7 +2666,6 @@ D16150FD2E9DBDED00131732 /* IDLanguageData.sqlite in Resources */, D1895BD22C1D816F009FBEB0 /* Settings.bundle in Resources */, 38BD214122D5908100C6795D /* LaunchScreen.storyboard in Resources */, - 5A68DA422CDE7B7A00897FAD /* RadioTableViewCell.xib in Resources */, 147797B12A2CD3370044A53E /* InfoChildTableViewCell.xib in Resources */, ED2486F42B0B4E8C0038AE6A /* AboutTableViewCell.xib in Resources */, E93179A42F03AE78002ED334 /* Localizable.xcstrings in Resources */, @@ -2824,7 +2819,6 @@ F786BB292F1E8F70003F7505 /* IDLanguageData.sqlite in Resources */, F786BB2A2F1E8F70003F7505 /* Settings.bundle in Resources */, F786BB2B2F1E8F70003F7505 /* LaunchScreen.storyboard in Resources */, - F786BB2C2F1E8F70003F7505 /* RadioTableViewCell.xib in Resources */, F786BB2D2F1E8F70003F7505 /* InfoChildTableViewCell.xib in Resources */, F786BB2E2F1E8F70003F7505 /* AboutTableViewCell.xib in Resources */, F786BB2F2F1E8F70003F7505 /* Localizable.xcstrings in Resources */, @@ -2886,8 +2880,10 @@ D17193C027AEA33A0038660B /* AppTextStyling.swift in Sources */, CE1378C428F5D7AC00E1CBC2 /* ScribeColor.swift in Sources */, D171946527AF31770038660B /* Conjugate.swift in Sources */, + 8BA036E42F7FCDC300F5ADF9 /* SettingsHomeView.swift in Sources */, 147797B52A2CFB490044A53E /* SettingsViewController.swift in Sources */, 1406B7872A2DFCDD001DF45B /* AboutTableData.swift in Sources */, + 8B82A46F2F7D6E0400F1B04D /* TranslationLanguagePickerView.swift in Sources */, E9202DF02F0FAA0C001590FC /* DownloadStateManager.swift in Sources */, E996498A2E98AC6000200F53 /* IDInterfaceVariables.swift in Sources */, 3045396D293B9DDC003AE55B /* ToolTipViewDatasource.swift in Sources */, @@ -2914,13 +2910,12 @@ 198369CC2C7980BA00C1B583 /* KeyboardProvider.swift in Sources */, CE1378CC28F5D7AC00E1CBC2 /* UIColor+ScribeColors.swift in Sources */, D17193F027AECB350038660B /* SVInterfaceVariables.swift in Sources */, - 140158A22A4EDB2200D14E52 /* TableViewTemplateViewController.swift in Sources */, 38BD213422D5907F00C6795D /* AppDelegate.swift in Sources */, 30489C1E2936DAB700B59393 /* ToolTipView.swift in Sources */, 3045396F293B9DF2003AE55B /* ToolTipViewTheme.swift in Sources */, - 5A68DA412CDE7B7A00897FAD /* RadioTableViewCell.swift in Sources */, E9F7273E2F45A6DE0060B92D /* LanguageDataService.swift in Sources */, D1F0367327AAE1B400CD7921 /* CommandVariables.swift in Sources */, + 8BA036E22F7FCA2D00F5ADF9 /* LanguageSettingsView.swift in Sources */, E9CE5EA82F063E230068A930 /* DownloadDataScreen.swift in Sources */, D16DD3A529E78A1500FB9022 /* Utilities.swift in Sources */, EDB460212B03B3E400BEA967 /* BaseTableViewController.swift in Sources */, @@ -2942,7 +2937,6 @@ D111E9A227AFE4F300746F92 /* CommandBar.swift in Sources */, 30453967293B9D31003AE55B /* ViewThemeable.swift in Sources */, D1362A39274C106A00C00E48 /* ColorVariables.swift in Sources */, - 5A68DA432CDE7B7A00897FAD /* SelectionViewTemplateViewController.swift in Sources */, ED2486F32B0B4E8C0038AE6A /* AboutTableViewCell.swift in Sources */, D1A2DCB127AD37BD0057A10D /* InstallScreen.swift in Sources */, D171944927AEF7290038660B /* KeyboardKeys.swift in Sources */, @@ -3951,7 +3945,7 @@ DEVELOPMENT_TEAM = ATJ9U3WZ27; INFOPLIST_FILE = "$(SRCROOT)/Scribe/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3979,7 +3973,7 @@ DEVELOPMENT_TEAM = ATJ9U3WZ27; INFOPLIST_FILE = "$(SRCROOT)/Scribe/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Scribe/Base.lproj/AppScreen.storyboard b/Scribe/Base.lproj/AppScreen.storyboard index e2030782..0c6de0ea 100644 --- a/Scribe/Base.lproj/AppScreen.storyboard +++ b/Scribe/Base.lproj/AppScreen.storyboard @@ -238,61 +238,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + @@ -543,47 +492,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Scribe/InstallationTab/InstallationVC.swift b/Scribe/InstallationTab/InstallationVC.swift index 7a44b06d..a397ef04 100644 --- a/Scribe/InstallationTab/InstallationVC.swift +++ b/Scribe/InstallationTab/InstallationVC.swift @@ -469,14 +469,6 @@ extension InstallationVC { } private func navigateToTranslationSourceSelection(languageCode: String, languageName: String) { - guard - let selectionVC = storyboard?.instantiateViewController( - identifier: "SelectionViewTemplateViewController" - ) as? SelectionViewTemplateViewController - else { - return - } - if let hostingController = navigationController?.viewControllers.last as? UIHostingController { hostingController.navigationItem.backButtonTitle = NSLocalizedString( @@ -495,7 +487,7 @@ extension InstallationVC { translateData[0].section.remove(at: langCodeIndex) } - let parentSection = Section( + let parentSection = Scribe.Section( sectionTitle: languageName, imageString: nil, hasToggle: false, @@ -505,9 +497,13 @@ extension InstallationVC { externalLink: false ) - selectionVC.configureTable( - for: translateData, parentSection: parentSection, langCode: languageCode + let pickerView = TranslationLanguagePickerView( + tableData: translateData, + parentSection: parentSection, + langCode: languageCode ) + + let selectionVC = UIHostingController(rootView: pickerView) selectionVC.edgesForExtendedLayout = .all // Copy navigation bar appearance from Settings tab. diff --git a/Scribe/SettingsTab/SettingsViewController.swift b/Scribe/SettingsTab/SettingsViewController.swift index e653c0ee..4949d8bf 100644 --- a/Scribe/SettingsTab/SettingsViewController.swift +++ b/Scribe/SettingsTab/SettingsViewController.swift @@ -8,370 +8,36 @@ import SwiftUI import UIKit final class SettingsViewController: UIViewController { - // MARK: Constants - - private var sectionHeaderHeight: CGFloat = 0 - private let separatorInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) - private let cornerRadius: CGFloat = 12 - - private let settingsTipCardState: Bool = { - let userDefault = UserDefaults.standard - return userDefault.object(forKey: "settingsTipCardState") as? Bool ?? true - }() - - func setHeaderHeight() { - if DeviceType.isPad { - sectionHeaderHeight = 42 - } else { - sectionHeaderHeight = 32 - } - } - - // MARK: Properties - - @IBOutlet var footerFrame: UIView! - @IBOutlet var footerButton: UIButton! - @IBOutlet var parentTable: UITableView! - - var tableData = SettingsTableData.settingsTableData - // MARK: Functions override func viewDidLoad() { super.viewDidLoad() - setHeaderHeight() - showTipCardView() - + setupSwiftUI() + + // Match the navigation style of the rest of the app. title = NSLocalizedString("i18n.app.settings.title", value: "Settings", comment: "") - navigationItem.backButtonTitle = title - - parentTable.register( - WrapperCell.self, - forCellReuseIdentifier: WrapperCell.reuseIdentifier - ) - - parentTable.register( - UINib(nibName: "InfoChildTableViewCell", bundle: nil), - forCellReuseIdentifier: InfoChildTableViewCell.reuseIdentifier - ) - parentTable.dataSource = self - parentTable.delegate = self - parentTable.backgroundColor = .clear - parentTable.sectionHeaderHeight = sectionHeaderHeight - parentTable.separatorInset = separatorInset - - setFooterButtonView() - - DispatchQueue.main.async { - self.parentTable.reloadData() - - self.commonMethodToRefresh() - } - NotificationCenter.default.addObserver( - self, - selector: #selector(handleFontSizeUpdate), - name: .fontSizeUpdatedNotification, - object: nil - ) - } - - @objc func handleFontSizeUpdate() { - DispatchQueue.main.async { - self.parentTable.reloadData() - self.setFooterButtonView() - } - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - commonMethodToRefresh() - } - - func commonMethodToRefresh() { - DispatchQueue.main.async { - self.tableData[1].section = SettingsTableData.getInstalledKeyboardsSections() - self.parentTable.reloadData() - self.setFooterButtonView() - } - - let notification = Notification( - name: .keyboardsUpdatedNotification, object: nil, userInfo: nil - ) - NotificationCenter.default.post(notification) - } - - func setFooterButtonView() { - if tableData.count > 1, tableData[1].section.count != 0 { - parentTable.tableFooterView?.isHidden = true - } else { - parentTable.tableFooterView?.isHidden = false - } - - footerButton.setTitle( - NSLocalizedString( - "i18n.app.settings.button_install_keyboards", value: "Install keyboards", - comment: "" - ), - for: .normal - ) - footerButton.titleLabel?.font = .systemFont(ofSize: fontSize * 1.5, weight: .bold) - - footerButton.backgroundColor = appBtnColor - if UITraitCollection.current.userInterfaceStyle == .dark { - footerButton.layer.borderWidth = 1 - footerButton.layer.borderColor = scribeCTAColor.cgColor - } - footerButton.setTitleColor(lightTextDarkCTA, for: .normal) - footerFrame.layer.cornerRadius = footerFrame.frame.width * 0.025 - footerButton.layer.cornerRadius = footerFrame.frame.width * 0.025 - footerButton.layer.shadowColor = - UIColor(red: 0.247, green: 0.247, blue: 0.275, alpha: 0.25).cgColor - footerButton.layer.shadowOffset = CGSize(width: 0.0, height: 3.0) - footerButton.layer.shadowOpacity = 1.0 - footerButton.layer.masksToBounds = false - - footerButton.addTarget(self, action: #selector(openSettingsApp), for: .touchUpInside) - footerButton.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) - } -} - -// MARK: UITableViewDataSource - -extension SettingsViewController: UITableViewDataSource { - func numberOfSections(in _: UITableView) -> Int { - tableData.count - } - - func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { - tableData[section].section.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard - let cell = tableView.dequeueReusableCell( - withIdentifier: WrapperCell.reuseIdentifier, - for: indexPath - ) as? WrapperCell - else { - fatalError("Failed to dequeue WrapperCell") - } - - let section = tableData[indexPath.section] - let setting = section.section[indexPath.row] - - cell.configure(withCellNamed: "InfoChildTableViewCell", section: setting) - - let isFirstRow = indexPath.row == 0 - let isLastRow = indexPath.row == section.section.count - 1 - WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) - - return cell - } -} - -// MARK: UITableViewDelegate - -extension SettingsViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let tableSection = tableData[indexPath.section] - let section = tableSection.section[indexPath.row] - - switch section.sectionState { - case .appLang: - let preferredLanguages = NSLocale.preferredLanguages - if preferredLanguages.count == 1 { - let alert = UIAlertController( - title: NSLocalizedString( - "i18n.app.settings.menu.app_language.one_device_language_warning.title", - value: "No languages installed", - comment: "" - ), - message: NSLocalizedString( - "i18n.app.settings.menu.app_language.one_device_language_warning.message", - value: - "You only have one language installed on your device. Please install more languages in Settings and then you can select different localizations of Scribe.", - comment: "" - ), - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alert, animated: true) - } else { - openSettingsApp() - } - - case .specificLang: - if let viewController = storyboard?.instantiateViewController( - identifier: "TableViewTemplateViewController" - ) as? TableViewTemplateViewController { - // Copy base settings. - var data = SettingsTableData.languageSettingsData - // Check if the device is an iPad to remove the Layout Section. - if DeviceType.isPad { - for menuOption in data[1].section { - if menuOption.sectionState == .none(.toggleAccentCharacters) - || menuOption.sectionState == .none(.toggleCommaAndPeriod) { - data[1].section.remove(at: 0) - } - } - if data[1].section.isEmpty { - data.remove(at: 1) - } - } else { - // Languages where we can disable accent keys. - let accentKeyLanguages: [String] = [ - languagesStringDict["German"]!, - languagesStringDict["Spanish"]!, - languagesStringDict["Swedish"]! - ] - - let accentKeyOptionIndex = - SettingsTableData.languageSettingsData[1].section.firstIndex(where: { s in - s.sectionTitle.elementsEqual( - NSLocalizedString( - "i18n.app.settings.keyboard.layout.disable_accent_characters", - value: "Disable accent characters", comment: "" - ) - ) - }) ?? -1 - - // If there are no accent keys we can remove the `Disable accent characters` option. - if accentKeyLanguages.firstIndex(of: section.sectionTitle) == nil, - accentKeyOptionIndex != -1 { - data[1].section.remove(at: accentKeyOptionIndex) - } else if accentKeyLanguages.firstIndex(of: section.sectionTitle) != nil, - accentKeyOptionIndex == -1 { - data[1].section.insert( - SettingsTableData.languageSettingsData[2].section[2], at: 1 - ) - } - } - - viewController.configureTable(for: data, parentSection: section) - - navigationController?.pushViewController(viewController, animated: true) - } - - default: break - } - - if let selectedIndexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: selectedIndexPath, animated: false) - } - } - - func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let section = tableData[indexPath.section] - let setting = section.section[indexPath.row] - - let hasDescription = setting.shortDescription != nil - return hasDescription ? 80.0 : 48.0 - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let headerView: UIView - - if let reusableHeaderView = tableView.headerView(forSection: section) { - headerView = reusableHeaderView - } else { - headerView = UIView( - frame: CGRect(x: 0, y: 0, width: parentTable.bounds.width, height: 32) - ) - } - - let label = UILabel( - frame: CGRect( - x: preferredLanguage.prefix(2) == "ar" ? -1 * headerView.bounds.width / 10 : 0, - y: 0, - width: headerView.bounds.width, - height: 32 - ) - ) - - label.text = tableData[section].headingTitle - label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) - label.textColor = keyCharColor - headerView.addSubview(label) - - return headerView - } - - /// Function to open the settings app that is targeted by settingsBtn. - @objc func openSettingsApp() { - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) - else { - fatalError("Failed to create settings URL.") - } - UIApplication.shared.open(settingsURL) + navigationController?.navigationBar.prefersLargeTitles = true } - /// Function to change the key coloration given a touch down. - /// - /// - Parameters - /// - sender: the button that has been pressed. - @objc func keyTouchDown(_ sender: UIButton) { - sender.backgroundColor = .black - sender.alpha = 0.2 - - // Bring sender's opacity back up to fully opaque and replace the background color. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { - sender.backgroundColor = .clear - sender.alpha = 1.0 - } - } -} - -// MARK: TipCardView - -extension SettingsViewController { - private func showTipCardView() { - let overlayView = SettingsTipCardView( - settingsTipCardState: settingsTipCardState - ) - - let hostingController = UIHostingController(rootView: overlayView) - hostingController.view.frame = CGRect( - x: 0, y: 0, width: view.bounds.width, height: -20 - ) - hostingController.view.backgroundColor = .clear + private func setupSwiftUI() { + let settingsHomeView = SettingsHomeView() + let hostingController = UIHostingController(rootView: settingsHomeView) + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false - - let navigationView = navigationController?.navigationBar - guard let navigationView else { return } - navigationView.addSubview(hostingController.view) - navigationView.bringSubviewToFront(hostingController.view) - NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint( - equalTo: navigationView.topAnchor, constant: 30 - ), - hostingController.view.leadingAnchor.constraint(equalTo: navigationView.leadingAnchor), - hostingController.view.trailingAnchor.constraint( - equalTo: navigationView.trailingAnchor - ) + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) + hostingController.didMove(toParent: self) - startGlowingEffect(on: hostingController.view) - } - - func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { - view.layer.shadowColor = UIColor.scribeCTA.cgColor - view.layer.shadowRadius = 8 - view.layer.shadowOpacity = 0.0 - view.layer.shadowOffset = CGSize(width: 0, height: 0) - - UIView.animate( - withDuration: duration, - delay: 0, - options: [.curveEaseOut, .autoreverse], - animations: { - view.layer.shadowOpacity = 0.6 - }, completion: nil - ) + + // Ensure the background matches the app theme. + hostingController.view.backgroundColor = .clear } } diff --git a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift b/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift deleted file mode 100644 index 9b9a98c5..00000000 --- a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -/* - * DESCRIPTION_OF_THE_PURPOSE_OF_THE_FILE - */ - -import UIKit - -final class RadioTableViewCell: UITableViewCell { - // MARK: Constants - - static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) - - // MARK: Properties - - @IBOutlet var titleLabelPhone: UILabel! - @IBOutlet var titleLabelPad: UILabel! - var titleLabel: UILabel! - - @IBOutlet var iconImageViewPhone: UIImageView! - @IBOutlet var iconImageViewPad: UIImageView! - var iconImageView: UIImageView! - - var section: Section? - var parentSection: Section? - var inUse: Bool = false - - func setTableView() { - if DeviceType.isPad { - titleLabel = titleLabelPad - iconImageView = iconImageViewPad - - titleLabelPhone.removeFromSuperview() - iconImageViewPhone.removeFromSuperview() - } else { - titleLabel = titleLabelPhone - iconImageView = iconImageViewPhone - - titleLabelPad.removeFromSuperview() - iconImageViewPad.removeFromSuperview() - } - } - - var selectedLang: String { - guard let section = section, - case let .specificLang(lang) = section.sectionState - else { return "n/a" } - - return lang - } - - var togglePurpose: UserInteractiveState { - guard let section = section, - case let .none(action) = section.sectionState - else { return .none } - - return action - } - - // MARK: Functions - - func configureCell(for section: Section) { - self.section = section - selectionStyle = .none - - setTableView() - titleLabel.text = section.sectionTitle - iconImageView.image = UIImage(named: "radioButton") - } -} diff --git a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.xib b/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.xib deleted file mode 100644 index b1a53d2c..00000000 --- a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.xib +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Scribe/Views/LanguageSettingsView.swift b/Scribe/Views/LanguageSettingsView.swift new file mode 100644 index 00000000..bb4646fa --- /dev/null +++ b/Scribe/Views/LanguageSettingsView.swift @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct LanguageSettingsView: View { + // MARK: Properties + let parentSection: Scribe.Section + let tableData: [ParentTableCellModel] + let languageCode: String + + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + + private var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } + + private let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + + @State private var navigationPath = NavigationPath() + + // MARK: Initialization + init(parentSection: Scribe.Section) { + self.parentSection = parentSection + + // Extract language code from parentSection. + if case let .specificLang(lang) = parentSection.sectionState { + self.languageCode = lang + } else { + self.languageCode = "en" + } + + // Filtering logic moved from SettingsViewController. + var data = SettingsTableData.languageSettingsData + + if DeviceType.isPad { + // Check if Layout Section exists. + if data.count > 1 { + // Keep only functioning settings for iPad in the Layout section. + // In the original UIKit, it was quite complex. Let's replicate. + data[1].section.removeAll { item in + item.sectionState == .none(.toggleAccentCharacters) || + item.sectionState == .none(.toggleCommaAndPeriod) + } + if data[1].section.isEmpty { + data.remove(at: 1) + } + } + } else { + // Logic for iPhone (accent keys). + let accentKeyLanguages: [String] = [ + languagesStringDict["German"]!, + languagesStringDict["Spanish"]!, + languagesStringDict["Swedish"]! + ] + + let accentKeyOptionIndex = + SettingsTableData.languageSettingsData[1].section.firstIndex(where: { s in + s.sectionTitle.elementsEqual( + NSLocalizedString( + "i18n.app.settings.keyboard.layout.disable_accent_characters", + value: "Disable accent characters", comment: "" + ) + ) + }) ?? -1 + + if accentKeyLanguages.firstIndex(of: parentSection.sectionTitle) == nil, + accentKeyOptionIndex != -1 { + data[1].section.remove(at: accentKeyOptionIndex) + } + } + self.tableData = data + } + + // MARK: Body + var body: some View { + List { + ForEach(Array(tableData.enumerated()), id: \.offset) { _, sectionModel in + SwiftUI.Section { + ForEach(Array(sectionModel.section.enumerated()), id: \.offset) { _, item in + settingRow(for: item) + } + } header: { + if !sectionModel.headingTitle.isEmpty { + Text(sectionModel.headingTitle) + .font(.system(size: (DeviceType.isPad ? 18 : 14) * textSizeMultiplier, weight: .bold)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + .textCase(nil) + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle(parentSection.sectionTitle) + .background(Color(UIColor(ScribeColor.scribeAppBackground)).edgesIgnoringSafeArea(.all)) + } + + // MARK: Row Builder + @ViewBuilder + private func settingRow(for item: Scribe.Section) -> some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(item.sectionTitle) + .font(.system(size: (DeviceType.isPad ? fontSize * 1.5 : fontSize) * textSizeMultiplier, weight: .medium)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + + Spacer() + + if item.hasToggle { + Toggle("", isOn: binding(for: item.sectionState)) + .labelsHidden() + .tint(Color(UIColor(ScribeColor.scribeCTA)).opacity(0.6)) + } else { + // Navigation + if item.sectionState == .translateLang { + translationSubLabel() + } + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color(UIColor(ScribeColor.menuOption))) + } + } + + if let description = item.shortDescription { + Text(description) + .font(.system(size: (DeviceType.isPad ? fontSize * 1.1 : fontSize * 0.9) * textSizeMultiplier)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + .opacity(0.8) + } + } + .padding(.vertical, 8) + .contentShape(Rectangle()) + .onTapGesture { + if !item.hasToggle { + handleNavigation(for: item) + } + } + .listRowBackground(Color(UIColor(ScribeColor.lightWhiteDarkBlack))) + } + + // MARK: Navigation + private func handleNavigation(for item: Scribe.Section) { + if item.sectionState == .translateLang { + pushTranslationPicker(parentItem: item) + } + } + + private func pushTranslationPicker(parentItem: Scribe.Section) { + var data = SettingsTableData.translateLangSettingsData + let langCodeIndex = data[0].section.firstIndex(where: { s in + s.sectionState == .specificLang(languageCode) + }) ?? -1 + if langCodeIndex != -1 { + data[0].section.remove(at: langCodeIndex) + } + + let pickerView = TranslationLanguagePickerView( + tableData: data, + parentSection: parentItem, + langCode: languageCode + ) + + // Find the hosting controller's navigation controller and push. + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let tabBarController = window.rootViewController as? UITabBarController, + let navigationController = tabBarController.selectedViewController as? UINavigationController { + let hostingController = UIHostingController(rootView: pickerView) + navigationController.pushViewController(hostingController, animated: true) + } + } + + @ViewBuilder + private func translationSubLabel() -> some View { + let key = languageCode + "TranslateLanguage" + let selectedLang = userDefaults.string(forKey: key) ?? "en" + let langName = getKeyInDict(givenValue: selectedLang, dict: languagesAbbrDict) + let localizedName = NSLocalizedString( + "i18n.app._global." + langName.lowercased(), + value: langName, + comment: "" + ) + + Text(localizedName) + .font(.system(size: (DeviceType.isPad ? fontSize * 1.3 : fontSize * 0.9) * textSizeMultiplier)) + .foregroundColor(Color(UIColor(ScribeColor.menuOption))) + } + + // MARK: Binding Helpers + private func binding(for state: SectionState) -> Binding { + guard case let .none(action) = state else { + return .constant(false) + } + + let key: String + let defaultValue: Bool + + switch action { + case .toggleCommaAndPeriod: + key = languageCode + "CommaAndPeriod" + defaultValue = false + case .toggleAccentCharacters: + key = languageCode + "AccentCharacters" + defaultValue = false + case .doubleSpacePeriods: + key = languageCode + "DoubleSpacePeriods" + defaultValue = true + case .autosuggestEmojis: + key = languageCode + "EmojiAutosuggest" + defaultValue = true + case .toggleWordForWordDeletion: + key = languageCode + "WordForWordDeletion" + defaultValue = false + case .increaseTextSize: + key = "increaseTextSize" + defaultValue = false + case .none: + return .constant(false) + } + + return Binding( + get: { + if let val = userDefaults.object(forKey: key) as? Bool { + return val + } + return defaultValue + }, + set: { newValue in + userDefaults.setValue(newValue, forKey: key) + if action == .increaseTextSize { + initializeFontSize() + NotificationCenter.default.post(name: .fontSizeUpdatedNotification, object: nil) + } + } + ) + } +} diff --git a/Scribe/Views/SelectionViewTemplateViewController.swift b/Scribe/Views/SelectionViewTemplateViewController.swift deleted file mode 100644 index 0113d282..00000000 --- a/Scribe/Views/SelectionViewTemplateViewController.swift +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -/* - * This file describes - */ - -import SwiftUI -import UIKit - -final class SelectionViewTemplateViewController: BaseTableViewController { - // MARK: Properties - - override var dataSet: [ParentTableCellModel] { - tableData - } - - private var tableData: [ParentTableCellModel] = [] - private var parentSection: Section? - private var selectedPath: IndexPath? - - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - - private var langCode: String = "de" - @ObservedObject private var stateManager = DownloadStateManager.shared - - // MARK: Functions - - override func viewDidLoad() { - super.viewDidLoad() - edgesForExtendedLayout = .all - extendedLayoutIncludesOpaqueBars = true - tableView.register( - UINib(nibName: "RadioTableViewCell", bundle: nil), - forCellReuseIdentifier: RadioTableViewCell.reuseIdentifier - ) - } - - func configureTable( - for tableData: [ParentTableCellModel], parentSection: Section, langCode: String - ) { - self.tableData = tableData - self.parentSection = parentSection - self.langCode = langCode - - title = NSLocalizedString( - "i18n.app.settings.keyboard.translation.select_source.title", - value: "Translation language", - comment: "" - ) - } -} - -// MARK: UITableViewDataSource - -extension SelectionViewTemplateViewController { - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) - -> UITableViewCell { - guard - let cell = tableView.dequeueReusableCell( - withIdentifier: RadioTableViewCell.reuseIdentifier, - for: indexPath - ) as? RadioTableViewCell - else { - fatalError("Failed to dequeue RadioTableViewCell.") - } - cell.parentSection = parentSection - cell.configureCell(for: tableData[indexPath.section].section[indexPath.row]) - cell.backgroundColor = lightWhiteDarkBlackColor - if cell.selectedLang == userDefaults.string(forKey: langCode + "TranslateLanguage") { - selectedPath = indexPath - cell.iconImageView.image = UIImage(named: "radioButtonSelected") - } - - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cell = tableView.cellForRow(at: indexPath) as! RadioTableViewCell - let oldLang = userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en" - let newLang = cell.selectedLang ?? "en" - - if let selectedIndexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: selectedIndexPath, animated: true) - } - - // Only show popup if selecting a different language. - if newLang != oldLang { - let oldIndexPath = selectedPath - updateRadioButton(to: indexPath, in: tableView) - showPopup( - oldLang: oldLang, newLang: newLang, oldIndexPath: oldIndexPath, - newIndexPath: indexPath, - tableView: tableView - ) - } - } - - private func updateRadioButton(to indexPath: IndexPath, in tableView: UITableView) { - if selectedPath != nil { - let previousCell = tableView.cellForRow(at: selectedPath!) as! RadioTableViewCell - previousCell.iconImageView.image = UIImage(named: "radioButton") - } - - let cell = tableView.cellForRow(at: indexPath) as! RadioTableViewCell - cell.iconImageView.image = UIImage(named: "radioButtonSelected") - selectedPath = indexPath - } - - private func showPopup( - oldLang: String, newLang: String, oldIndexPath: IndexPath?, newIndexPath _: IndexPath, - tableView: UITableView - ) { - let oldSourceLanguage = getKeyInDict(givenValue: oldLang, dict: languagesAbbrDict) - let newSourceLanguage = getKeyInDict(givenValue: newLang, dict: languagesAbbrDict) - - let infoText = NSLocalizedString( - "i18n.app.settings.keyboard.translation.change_source_tooltip.download_warning", - value: - "You've changed your source translation language. Would you like to download new data so that you can translate from {source_language}?", - comment: "" - ) - - let changeButtonText = NSLocalizedString( - "i18n.app.settings.keyboard.translation.change_source_tooltip.keep_source_language", - value: "Keep {source_language}", comment: "" - ) - - let confirmButtonText = NSLocalizedString( - "i18n.app._global.download_data", value: "Download data", comment: "" - ) - - let localizedOldSourceLanguage = NSLocalizedString( - "i18n.app._global." + oldSourceLanguage.lowercased(), - value: oldSourceLanguage, - comment: "" - ) - let localizedNewSourceLanguage = NSLocalizedString( - "i18n.app._global." + newSourceLanguage.lowercased(), - value: newSourceLanguage, - comment: "" - ) - - func onKeep() { - // Keep old language - revert and dismiss. - dismiss(animated: true) { - if let oldPath = oldIndexPath { - self.updateRadioButton(to: oldPath, in: tableView) - } - } - } - - func confirmDownload() { - dismiss(animated: true) { - let dictionaryKey = self.langCode + "TranslateLanguage" - self.userDefaults.setValue(newLang, forKey: dictionaryKey) - - // Trigger download state change. - DownloadStateManager.shared.handleDownloadAction( - key: self.langCode, forceDownload: true - ) - - guard let tabBarController = self.tabBarController else { return } - - guard - let installationNavController = tabBarController.viewControllers?[0] - as? UINavigationController - else { return } - - // Check if download screen exists in nav stack. - if let downloadScreen = installationNavController.viewControllers.first(where: { - $0 is UIHostingController - }) { - // Download screen found - popping to it. - tabBarController.selectedIndex = 0 - installationNavController.popToViewController(downloadScreen, animated: true) - } else { - // Download screen not found - creating new one. - tabBarController.selectedIndex = 0 - installationNavController.popToRootViewController(animated: false) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - NotificationCenter.default.post( - name: NSNotification.Name("NavigateToDownloadScreen"), - object: nil - ) - } - } - } - } - - let popupView = ConfirmTranslationSource( - infoText: infoText.replacingOccurrences( - of: "{source_language}", with: localizedNewSourceLanguage - ).replacingOccurrences(of: "{source_language}", with: localizedNewSourceLanguage), - changeButtonText: changeButtonText.replacingOccurrences( - of: "{source_language}", with: localizedOldSourceLanguage - ), - confirmButtonText: confirmButtonText, - onDismiss: { onKeep() }, - onChange: { onKeep() }, - onConfirm: { confirmDownload() } - ) - - let hostingController = UIHostingController(rootView: popupView) - hostingController.modalPresentationStyle = .overFullScreen - hostingController.modalTransitionStyle = .crossDissolve - hostingController.view.backgroundColor = .clear - - present(hostingController, animated: true) - } -} diff --git a/Scribe/Views/SettingsHomeView.swift b/Scribe/Views/SettingsHomeView.swift new file mode 100644 index 00000000..018205d4 --- /dev/null +++ b/Scribe/Views/SettingsHomeView.swift @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct SettingsHomeView: View { + // MARK: Properties + @State private var tableData = SettingsTableData.settingsTableData + @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) + var increaseTextSize: Bool = false + + private var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } + + @Environment(\.colorScheme) var colorScheme + @State private var showLanguageAlert = false + @State private var tipCardVisible = true + + private let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + + // MARK: Body + var body: some View { + List { + // MARK: Tip Card Header + if tipCardVisible { + SwiftUI.Section { + SettingsTipCardView { + withAnimation { tipCardVisible = false } + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } + } + + // MARK: Settings Sections + ForEach(Array(tableData.enumerated()), id: \.offset) { _, sectionModel in + SwiftUI.Section { + ForEach(Array(sectionModel.section.enumerated()), id: \.offset) { _, item in + settingRow(for: item) + } + } header: { + if !sectionModel.headingTitle.isEmpty { + Text(sectionModel.headingTitle) + .font(.system(size: (DeviceType.isPad ? 18 : 14) * textSizeMultiplier, weight: .bold)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + .textCase(nil) + } + } + } + + // MARK: Footer Button Section + if tableData.indices.contains(1), tableData[1].section.isEmpty { + SwiftUI.Section { + installKeyboardsButton() + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } + } + } + .listStyle(.insetGrouped) + .navigationTitle(NSLocalizedString("i18n.app.settings.title", value: "Settings", comment: "")) + .background(Color(UIColor(ScribeColor.scribeAppBackground)).edgesIgnoringSafeArea(.all)) + .onAppear { + refreshKeyboards() + // Check if tip card was already dismissed (stored in @AppStorage inside SettingsTipCardView) + tipCardVisible = UserDefaults.standard.bool(forKey: "settingsTipCardState") + if !UserDefaults.standard.dictionaryRepresentation().keys.contains("settingsTipCardState") { + tipCardVisible = true + } + } + .alert(isPresented: $showLanguageAlert) { + Alert( + title: Text(NSLocalizedString("i18n.app.settings.menu.app_language.one_device_language_warning.title", value: "No languages installed", comment: "")), + message: Text(NSLocalizedString("i18n.app.settings.menu.app_language.one_device_language_warning.message", value: "You only have one language installed on your device. Please install more languages in Settings and then you can select different localizations of Scribe.", comment: "")), + dismissButton: .cancel(Text("Cancel")) + ) + } + } + + // MARK: Row Builder + @ViewBuilder + private func settingRow(for item: Scribe.Section) -> some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(item.sectionTitle) + .font(.system(size: (DeviceType.isPad ? fontSize * 1.5 : fontSize) * textSizeMultiplier, weight: .medium)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + + Spacer() + + if item.hasToggle { + Toggle("", isOn: binding(for: item.sectionState)) + .labelsHidden() + .tint(Color(UIColor(ScribeColor.scribeCTA)).opacity(0.6)) + } else { + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color(UIColor(ScribeColor.menuOption))) + } + } + + if let description = item.shortDescription { + Text(description) + .font(.system(size: (DeviceType.isPad ? fontSize * 1.1 : fontSize * 0.9) * textSizeMultiplier)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + .opacity(0.8) + } + } + .padding(.vertical, 8) + .contentShape(Rectangle()) + .onTapGesture { + if !item.hasToggle { + handleNavigation(for: item) + } + } + .listRowBackground(Color(UIColor(ScribeColor.lightWhiteDarkBlack))) + } + + // MARK: Component Helpers + @ViewBuilder + private func installKeyboardsButton() -> some View { + Button { + openSettingsApp() + } label: { + Text(NSLocalizedString("i18n.app.settings.button_install_keyboards", value: "Install keyboards", comment: "")) + .font(.system(size: fontSize * 1.5, weight: .bold)) + .frame(maxWidth: .infinity) + .frame(height: 60) + .background(Color(UIColor(ScribeColor.appBtn))) + .foregroundColor(Color(UIColor(ScribeColor.lightTextDarkCTA))) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color(UIColor(ScribeColor.scribeCTA)), lineWidth: colorScheme == .dark ? 1 : 0) + ) + .shadow(color: Color.black.opacity(0.25), radius: 3, x: 0, y: 3) + } + .padding(.horizontal, 16) + .padding(.top, 20) + } + + // MARK: Logic + private func refreshKeyboards() { + tableData[1].section = SettingsTableData.getInstalledKeyboardsSections() + } + + private func handleNavigation(for item: Scribe.Section) { + switch item.sectionState { + case .appLang: + if NSLocale.preferredLanguages.count == 1 { + showLanguageAlert = true + } else { + openSettingsApp() + } + case .specificLang: + pushLanguageSettings(parentItem: item) + default: break + } + } + + private func pushLanguageSettings(parentItem: Scribe.Section) { + let nextView = LanguageSettingsView(parentSection: parentItem) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let tabBarController = window.rootViewController as? UITabBarController, + let navigationController = tabBarController.selectedViewController as? UINavigationController { + let hostingController = UIHostingController(rootView: nextView) + navigationController.pushViewController(hostingController, animated: true) + } + } + + private func openSettingsApp() { + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + UIApplication.shared.open(url) + } + + private func binding(for state: SectionState) -> Binding { + guard case let .none(action) = state, action == .increaseTextSize else { + return .constant(false) + } + + return Binding( + get: { increaseTextSize }, + set: { newValue in + increaseTextSize = newValue + initializeFontSize() + NotificationCenter.default.post(name: .fontSizeUpdatedNotification, object: nil) + } + ) + } +} + diff --git a/Scribe/Views/TableViewTemplateViewController.swift b/Scribe/Views/TableViewTemplateViewController.swift deleted file mode 100644 index e2c50b24..00000000 --- a/Scribe/Views/TableViewTemplateViewController.swift +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -/* - * Controls the table views within the app. - */ - -import UIKit - -final class TableViewTemplateViewController: BaseTableViewController { - // MARK: Properties - - override var dataSet: [ParentTableCellModel] { - tableData - } - - private var tableData: [ParentTableCellModel] = [] - private var parentSection: Section? - private let cornerRadius: CGFloat = 12 - - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - - private var langCode: String { - guard let parentSection - else { - return "" - } - - guard case let .specificLang(lang) = parentSection.sectionState - else { - return "de" - } - - return lang - } - - // MARK: Functions - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.register( - WrapperCell.self, - forCellReuseIdentifier: WrapperCell.reuseIdentifier - ) - - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 250 - tableView.separatorStyle = .none - } - - func configureTable(for tableData: [ParentTableCellModel], parentSection: Section) { - self.tableData = tableData - self.parentSection = parentSection - - title = parentSection.sectionTitle - } - - /// Refreshes to check for changes when a translation language is selected. - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - for visibleCell in tableView.visibleCells { - // Cast to wrapper cell first. - guard let wrapperCell = visibleCell as? WrapperCell, - let innerCell = wrapperCell.wrappedCell as? InfoChildTableViewCell - else { - continue - } - - // Now check if it's a translate lang section. - guard innerCell.section?.sectionState == .translateLang - else { - continue - } - - let langTranslateLanguage = getKeyInDict( - givenValue: userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en", - dict: languagesAbbrDict - ) - let currentLang = "i18n.app._global." + langTranslateLanguage.lowercased() - innerCell.subLabel.text = NSLocalizedString( - currentLang, value: langTranslateLanguage, comment: "" - ) - } - } -} - -// MARK: UITableViewDataSource - -extension TableViewTemplateViewController { - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) - -> UITableViewCell { - guard - let cell = tableView.dequeueReusableCell( - withIdentifier: WrapperCell.reuseIdentifier, - for: indexPath - ) as? WrapperCell - else { - fatalError("Failed to dequeue WrapperCell") - } - - let section = dataSet[indexPath.section] - let setting = section.section[indexPath.row] - - cell.configure( - withCellNamed: "InfoChildTableViewCell", section: setting, - parentSection: parentSection - ) - - let isFirstRow = indexPath.row == 0 - let isLastRow = indexPath.row == section.section.count - 1 - WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) - - return cell - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) - -> CGFloat { - let section = dataSet[indexPath.section] - let setting = section.section[indexPath.row] - - // If there's no description, return a fixed small height. - guard let description = setting.shortDescription - else { - return 54.0 - } - - // Calculate available width for text. - let availableWidth = tableView.bounds.width - 32 - - let titleFont: UIFont - let descFont: UIFont - - if DeviceType.isPad { - titleFont = UIFont.systemFont(ofSize: fontSize * 1.5, weight: .medium) - descFont = UIFont.systemFont(ofSize: fontSize * 1.1) - } else { - titleFont = UIFont.systemFont(ofSize: fontSize, weight: .medium) - descFont = UIFont.systemFont(ofSize: fontSize * 0.9) - } - - // Calculate actual height needed for title text. - let titleHeight = setting.sectionTitle.boundingRect( - with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude), - options: .usesLineFragmentOrigin, - attributes: [.font: titleFont], - context: nil - ).height - - // Calculate actual height needed for description text. - let descHeight = description.boundingRect( - with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude), - options: .usesLineFragmentOrigin, - attributes: [.font: descFont], - context: nil - ).height - - // Return total height: title + description + padding buffer. - return ceil(titleHeight + descHeight + 52) - } - - override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { - let tableSection = tableData[indexPath.section] - let section = tableSection.section[indexPath.row] - - if section.sectionState == .translateLang { - if let viewController = storyboard?.instantiateViewController( - identifier: "SelectionViewTemplateViewController" - ) as? SelectionViewTemplateViewController { - var data = SettingsTableData.translateLangSettingsData - - // Removes keyboard language from possible translation languages. - let langCodeIndex = - SettingsTableData.translateLangSettingsData[0].section.firstIndex(where: { s in - s.sectionState == .specificLang(langCode) - }) ?? -1 - data[0].section.remove(at: langCodeIndex) - - viewController.configureTable(for: data, parentSection: section, langCode: langCode) - - navigationController?.pushViewController(viewController, animated: true) - } - } - } -} diff --git a/Scribe/Views/TranslationLanguagePickerView.swift b/Scribe/Views/TranslationLanguagePickerView.swift new file mode 100644 index 00000000..989d66c3 --- /dev/null +++ b/Scribe/Views/TranslationLanguagePickerView.swift @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct TranslationLanguagePickerView: View { + // MARK: Properties + let tableData: [ParentTableCellModel] + let parentSection: Scribe.Section? + let langCode: String + + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + + private var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } + + private let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + + @State private var selectedLang: String = "en" + @State private var showConfirmation = false + @State private var pendingNewLang: String = "" + + // MARK: Body + var body: some View { + List { + ForEach(Array(tableData.enumerated()), id: \.offset) { sectionIndex, sectionModel in + SwiftUI.Section { + ForEach(Array(sectionModel.section.enumerated()), id: \.offset) { _, item in + let itemLang = extractLangCode(from: item) + languageRow(item: item, itemLang: itemLang) + } + } header: { + if !sectionModel.headingTitle.isEmpty { + Text(sectionModel.headingTitle) + .font(.system(size: (DeviceType.isPad ? 18 : 14) * textSizeMultiplier, weight: .bold)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + .textCase(nil) + } + } + } + } + .listStyle(.insetGrouped) + .onAppear { + selectedLang = userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en" + } + .fullScreenCover(isPresented: $showConfirmation) { + confirmationPopup() + } + .navigationTitle(NSLocalizedString("i18n.app.settings.keyboard.translation.select_source.title", value: "Translation language", comment: "")) + } + + // MARK: Language Row + @ViewBuilder + private func languageRow(item: Scribe.Section, itemLang: String) -> some View { + Button { + handleSelection(newLang: itemLang) + } label: { + HStack { + Image(selectedLang == itemLang ? "radioButtonSelected" : "radioButton") + .resizable() + .frame(width: DeviceType.isPad ? 28 : 22, height: DeviceType.isPad ? 28 : 22) + + Text(item.sectionTitle) + .font(.system(size: (DeviceType.isPad ? fontSize * 1.5 : fontSize) * textSizeMultiplier)) + .foregroundColor(Color(UIColor(ScribeColor.keyChar))) + + Spacer() + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .listRowBackground(Color(UIColor(ScribeColor.lightWhiteDarkBlack))) + } + + // MARK: Selection Handler + private func handleSelection(newLang: String) { + let oldLang = selectedLang + if newLang != oldLang { + pendingNewLang = newLang + // We temporarily don't update selectedLang so we can revert if cancelled. + showConfirmation = true + } + } + + // MARK: Confirmation Popup + @ViewBuilder + private func confirmationPopup() -> some View { + let oldLang = userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en" + let oldSourceLanguage = getKeyInDict(givenValue: oldLang, dict: languagesAbbrDict) + let newSourceLanguage = getKeyInDict(givenValue: pendingNewLang, dict: languagesAbbrDict) + + let localizedOldSourceLanguage = NSLocalizedString( + "i18n.app._global." + oldSourceLanguage.lowercased(), + value: oldSourceLanguage, + comment: "" + ) + let localizedNewSourceLanguage = NSLocalizedString( + "i18n.app._global." + newSourceLanguage.lowercased(), + value: newSourceLanguage, + comment: "" + ) + + let infoText = NSLocalizedString( + "i18n.app.settings.keyboard.translation.change_source_tooltip.download_warning", + value: "You've changed your source translation language. Would you like to download new data so that you can translate from {source_language}?", + comment: "" + ).replacingOccurrences(of: "{source_language}", with: localizedNewSourceLanguage) + + let changeButtonText = NSLocalizedString( + "i18n.app.settings.keyboard.translation.change_source_tooltip.keep_source_language", + value: "Keep {source_language}", comment: "" + ).replacingOccurrences(of: "{source_language}", with: localizedOldSourceLanguage) + + let confirmButtonText = NSLocalizedString( + "i18n.app._global.download_data", value: "Download data", comment: "" + ) + + ConfirmTranslationSource( + infoText: infoText, + changeButtonText: changeButtonText, + confirmButtonText: confirmButtonText, + onDismiss: { showConfirmation = false }, + onChange: { showConfirmation = false }, + onConfirm: { confirmDownload() } + ) + .background(BackgroundClearView()) + } + + // MARK: Actions + private func confirmDownload() { + showConfirmation = false + let dictionaryKey = langCode + "TranslateLanguage" + userDefaults.setValue(pendingNewLang, forKey: dictionaryKey) + selectedLang = pendingNewLang + + DownloadStateManager.shared.handleDownloadAction(key: langCode, forceDownload: true) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + navigateToDownloadScreen() + } + } + + private func navigateToDownloadScreen() { + guard + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let tabBarController = window.rootViewController as? UITabBarController, + let installationNavController = tabBarController.viewControllers?[0] as? UINavigationController + else { return } + + if let downloadScreen = installationNavController.viewControllers.first(where: { + $0 is UIHostingController + }) { + tabBarController.selectedIndex = 0 + installationNavController.popToViewController(downloadScreen, animated: true) + } else { + tabBarController.selectedIndex = 0 + installationNavController.popToRootViewController(animated: false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + NotificationCenter.default.post( + name: NSNotification.Name("NavigateToDownloadScreen"), + object: nil + ) + } + } + } + + private func extractLangCode(from section: Scribe.Section) -> String { + if case let .specificLang(lang) = section.sectionState { + return lang + } + return "n/a" + } +} + +private struct BackgroundClearView: UIViewRepresentable { + func makeUIView(context _: Context) -> UIView { + let view = InnerView() + DispatchQueue.main.async { + view.superview?.superview?.backgroundColor = .clear + } + return view + } + + func updateUIView(_: UIView, context _: Context) {} + + private class InnerView: UIView { + override func didMoveToWindow() { + super.didMoveToWindow() + superview?.superview?.backgroundColor = .clear + } + } +}