From 319c81e4469235b046d58aa26f3526fd77d551bb Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:44:24 +0530 Subject: [PATCH 1/2] feat: add download data empty state button to keyboard (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a Scribe keyboard is installed but language data has not been downloaded yet, a blue 'Please download language data' button now appears at the top of the keyboard. Tapping it opens the Scribe app and navigates directly to the Download Data screen. Changes: - KeyboardViewController.swift - Added downloadDataBtn (UIButton?) property - Added hasLanguageData() to check if the language SQLite file exists in the shared app group container - Added showDownloadDataBtn() to create and layout the blue CTA button at the top of the keyboard view - Added conditionallyShowDownloadDataBtn() to show/hide the button based on data availability and current command state (only shown in .idle state, hidden during translate/conjugate/plural/info) - Added openScribeApp() using the UIResponder chain to open the scribe:// URL scheme and direct the user to the download screen - Called conditionallyShowDownloadDataBtn() at the end of loadKeys() - Button is also hidden during displayInformation state - CommandVariables.swift - Added downloadDataMsg variable with default English fallback text 'Please download language data' - Language interface variables (all 11 keyboards) - ENInterfaceVariables: 'Please download language data' - DEInterfaceVariables: 'Bitte Sprachdaten herunterladen' - FRInterfaceVariables: 'Veuillez télécharger les données linguistiques' - ESInterfaceVariables: 'Por favor descarga los datos del idioma' - ITInterfaceVariables: 'Scarica i dati della lingua' - PTInterfaceVariables: 'Por favor baixe os dados do idioma' - RUInterfaceVariables: 'Загрузите языковые данные' - SVInterfaceVariables: 'Ladda ner språkdata' - NBInterfaceVariables: 'Last ned språkdata' - HEInterfaceVariables: 'אנא הורד נתוני שפה' - IDInterfaceVariables: 'Unduh data bahasa' - Scribe/Info.plist - Registered scribe:// URL scheme (CFBundleURLSchemes) so the app can be opened from the keyboard extension - Scribe/AppDelegate.swift - Added application(_:open:options:) handler for the scribe:// URL - Posts NavigateToDownloadScreen notification which InstallationVC already handles to push the DownloadDataScreen Closes #627 --- .../KeyboardViewController.swift | 96 +++++++++++++++++++ .../CommandVariables.swift | 5 + .../English/ENInterfaceVariables.swift | 1 + .../French/FR-AZERTYInterfaceVariables.swift | 1 + .../German/DEInterfaceVariables.swift | 1 + .../Hebrew/HEInterfaceVariables.swift | 1 + .../Indonesian/IDInterfaceVariables.swift | 1 + .../Italian/ITInterfaceVariables.swift | 1 + .../Norwegian/NBInterfaceVariables.swift | 1 + .../Portuguese/PTInterfaceVariables.swift | 1 + .../Russian/RUInterfaceVariables.swift | 1 + .../Spanish/ESInterfaceVariables.swift | 1 + .../Swedish/SVInterfaceVariables.swift | 1 + Scribe/AppDelegate.swift | 9 ++ Scribe/Info.plist | 11 +++ 15 files changed, 132 insertions(+) diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index d74847f9..61dfee62 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -290,6 +290,100 @@ class KeyboardViewController: UIInputViewController { pluralKey.isHidden = state } + // MARK: Download Data Button + + /// A button shown above the keyboard when no language data has been downloaded. + var downloadDataBtn: UIButton? + + /// Checks whether language data has been downloaded for the current keyboard language. + func hasLanguageData() -> Bool { + let langAbbr = getControllerLanguageAbbr() + guard !langAbbr.isEmpty, + let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.be.scri.userDefaultsContainer" + ) else { + return true // default to true to avoid showing the button unnecessarily + } + let dbPath = containerURL.appendingPathComponent("\(langAbbr.uppercased())LanguageData.sqlite").path + return FileManager.default.fileExists(atPath: dbPath) + } + + /// Opens the Scribe app via the responder chain so the user can download language data. + @objc func openScribeApp() { + guard let url = URL(string: "scribe://") else { return } + var responder: UIResponder? = self + while responder != nil { + if let application = responder as? UIApplication { + application.open(url) + return + } + responder = responder?.next + } + } + + /// Shows or hides the download data button based on whether language data is available. + func conditionallyShowDownloadDataBtn() { + // Only show the download button in idle state (not during commands). + guard commandState == .idle else { + downloadDataBtn?.isHidden = true + return + } + if hasLanguageData() { + downloadDataBtn?.isHidden = true + } else { + showDownloadDataBtn() + } + } + + /// Creates and displays the download data button above the keyboard. + func showDownloadDataBtn() { + // Remove any existing button first. + downloadDataBtn?.removeFromSuperview() + + let btn = UIButton(type: .system) + btn.translatesAutoresizingMaskIntoConstraints = false + btn.backgroundColor = scribeCTAColor + btn.setTitleColor(.white, for: .normal) + btn.setTitle(downloadDataMsg, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + btn.layer.cornerRadius = commandKeyCornerRadius + btn.layer.masksToBounds = true + + // Add a download icon on the left side. + let iconConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .medium) + btn.setImage(UIImage(systemName: "arrow.down.circle.fill", withConfiguration: iconConfig), for: .normal) + btn.tintColor = .white + if #available(iOS 15.0, *) { + var config = UIButton.Configuration.plain() + config.baseForegroundColor = .white + config.image = UIImage(systemName: "arrow.down.circle.fill", withConfiguration: iconConfig) + config.imagePadding = 8 + config.imagePlacement = .leading + config.title = downloadDataMsg + config.attributedTitle = AttributedString( + downloadDataMsg, + attributes: AttributeContainer([.font: UIFont.systemFont(ofSize: 16, weight: .medium)]) + ) + btn.configuration = config + btn.backgroundColor = scribeCTAColor + } else { + btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) + btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) + } + + btn.addTarget(self, action: #selector(openScribeApp), for: .touchUpInside) + + view.addSubview(btn) + downloadDataBtn = btn + + NSLayoutConstraint.activate([ + btn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + btn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + btn.topAnchor.constraint(equalTo: view.topAnchor, constant: 4), + btn.heightAnchor.constraint(equalToConstant: scribeKey.frame.height > 0 ? scribeKey.frame.height : 36) + ]) + } + /// Logic to create notification tooltip. func createInformationStateDatasource(text: NSMutableAttributedString, backgroundColor: UIColor) -> ToolTipViewDatasource { @@ -1714,6 +1808,7 @@ class KeyboardViewController: UIInputViewController { deactivateBtn(btn: padEmojiKey2) setInformationState() + downloadDataBtn?.isHidden = true return // return to skip normal keyboard setup } @@ -1835,6 +1930,7 @@ class KeyboardViewController: UIInputViewController { } setKeyPadding() + conditionallyShowDownloadDataBtn() } func setCommaAndPeriodKeysConditionally() { diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift index 236fd9e5..43778e4f 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift @@ -177,3 +177,8 @@ var pluralPromptAndCursor = "" var pluralPromptAndPlaceholder = "" var pluralPromptAndColorPlaceholder = NSMutableAttributedString() var alreadyPluralMsg = "" + +// MARK: Download Data Variables + +/// The message shown on the download data button when no language data has been downloaded. +var downloadDataMsg = "Please download language data" diff --git a/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift b/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift index 28f5f4fc..4812c060 100644 --- a/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift @@ -294,4 +294,5 @@ func setENKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Already plural" + downloadDataMsg = "Please download language data" } diff --git a/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift b/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift index 8be89a71..8882feca 100644 --- a/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift @@ -293,4 +293,5 @@ func setFRKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Déjà pluriel" + downloadDataMsg = "Veuillez télécharger les données linguistiques" } diff --git a/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift b/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift index de063eb9..4da5a232 100644 --- a/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift @@ -356,4 +356,5 @@ func setDEKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Schon Plural" + downloadDataMsg = "Bitte Sprachdaten herunterladen" } diff --git a/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift index 3e3f6319..5cc7cee4 100644 --- a/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift @@ -254,4 +254,5 @@ func setHEKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "כבר בצורת רבים" + downloadDataMsg = "אנא הורד נתוני שפה" } diff --git a/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift index 8acfbe95..57b04d32 100644 --- a/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift @@ -244,4 +244,5 @@ func setIDKeyboardLayout() { textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) + downloadDataMsg = "Unduh data bahasa" } diff --git a/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift index 8337b9f7..256ee789 100644 --- a/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift @@ -281,4 +281,5 @@ func setITKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Già plurale" + downloadDataMsg = "Scarica i dati della lingua" } diff --git a/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift index 2d7e8a74..9e08ac1e 100644 --- a/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift @@ -300,4 +300,5 @@ func setNBKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Allerede flertall" + downloadDataMsg = "Last ned språkdata" } diff --git a/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift index eeee3642..4d5f19d3 100644 --- a/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift @@ -279,4 +279,5 @@ func setPTKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Já plural" + downloadDataMsg = "Por favor baixe os dados do idioma" } diff --git a/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift index 40e17c4c..282238e8 100644 --- a/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift @@ -269,4 +269,5 @@ func setRUKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Уже во множ-ом" + downloadDataMsg = "Загрузите языковые данные" } diff --git a/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift index 200d9b7b..b33cdb99 100644 --- a/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift @@ -353,4 +353,5 @@ func setESKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Ya en plural" + downloadDataMsg = "Por favor descarga los datos del idioma" } diff --git a/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift index 3943138d..f326ba38 100644 --- a/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift @@ -349,4 +349,5 @@ func setSVKeyboardLayout() { withColor: UIColor(cgColor: commandBarPlaceholderColorCG) ) alreadyPluralMsg = "Redan plural" + downloadDataMsg = "Ladda ner språkdata" } diff --git a/Scribe/AppDelegate.swift b/Scribe/AppDelegate.swift index b00aa906..e86a60b8 100644 --- a/Scribe/AppDelegate.swift +++ b/Scribe/AppDelegate.swift @@ -82,6 +82,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { */ } + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Handle scribe:// URL scheme to navigate to the download data screen. + if url.scheme == "scribe" { + NotificationCenter.default.post(name: NSNotification.Name("NavigateToDownloadScreen"), object: nil) + return true + } + return false + } + func applicationDidBecomeActive(_: UIApplication) { /* Restart any tasks that were paused (or not yet started) while the application was inactive. diff --git a/Scribe/Info.plist b/Scribe/Info.plist index 633b5171..5c2f37fe 100644 --- a/Scribe/Info.plist +++ b/Scribe/Info.plist @@ -46,5 +46,16 @@ UIViewControllerBasedStatusBarAppearance + CFBundleURLTypes + + + CFBundleURLSchemes + + scribe + + CFBundleURLName + be.scri.scribe + + From 6b4e15f10fc4519fa8fe567c399b995a7dd77915 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:15:32 +0530 Subject: [PATCH 2/2] Fix circular reference compilation errors by using classes --- Keyboards/DataManager/DataContract.swift | 2 +- Keyboards/KeyboardsBase/NavigationStructure.swift | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Keyboards/DataManager/DataContract.swift b/Keyboards/DataManager/DataContract.swift index 0eb96a77..4ca89639 100644 --- a/Keyboards/DataManager/DataContract.swift +++ b/Keyboards/DataManager/DataContract.swift @@ -38,7 +38,7 @@ struct DeclensionSection: Codable { let declensionForms: [Int: DeclensionNode]? } -struct DeclensionNode: Codable { +class DeclensionNode: Codable { let label: String? let value: String? let displayValue: String? diff --git a/Keyboards/KeyboardsBase/NavigationStructure.swift b/Keyboards/KeyboardsBase/NavigationStructure.swift index 54f5d558..64ef35f3 100644 --- a/Keyboards/KeyboardsBase/NavigationStructure.swift +++ b/Keyboards/KeyboardsBase/NavigationStructure.swift @@ -6,15 +6,20 @@ import Foundation /// Represents a single option in the navigation. -enum NavigationNode { +indirect enum NavigationNode { case nextLevel(NavigationLevel, displayValue: String?) // navigate deeper, with optional display value case finalValue(String) // terminal node, insert this text } /// Represents a level in the navigation hierarchy. -struct NavigationLevel { +class NavigationLevel { let title: String // title for command bar let options: [(label: String, node: NavigationNode)] // buttons to display + + init(title: String, options: [(label: String, node: NavigationNode)]) { + self.title = title + self.options = options + } } /// Builds navigation trees for conjugations and declensions.