From 3439dd3e815722ec9e973555ceb8cf96d2bb5fff Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:21:44 +0530 Subject: [PATCH 1/8] feat: Add SwiftUI About tab for Scribe-Conjugate - Implement AboutTab with Community, Feedback and support, and Legal sections - Add AboutSectionView, AboutRowView, AboutTipCardView, AboutInfoView, ShareSheet - Use Conjugate-specific share text and dynamic version number from bundle - Add Bluesky row with butterfly icon asset - Add detail screens for Wikimedia, Privacy Policy, and Third-party licenses - Matches Figma design for Conjugate About tab Closes #607 --- .../Views/Tabs/About/AboutInfoView.swift | 178 +++++++++++++ Conjugate/Views/Tabs/About/AboutRowView.swift | 106 ++++++++ .../Views/Tabs/About/AboutSectionView.swift | 31 +++ Conjugate/Views/Tabs/About/AboutTab.swift | 250 +++++++++++++++++- .../Views/Tabs/About/AboutTipCardView.swift | 54 ++++ Conjugate/Views/Tabs/About/ShareSheet.swift | 18 ++ .../MenuIcons/Bluesky.imageset/Contents.json | 21 ++ .../MenuIcons/Bluesky.imageset/Image.png | Bin 0 -> 2720 bytes 8 files changed, 653 insertions(+), 5 deletions(-) create mode 100644 Conjugate/Views/Tabs/About/AboutInfoView.swift create mode 100644 Conjugate/Views/Tabs/About/AboutRowView.swift create mode 100644 Conjugate/Views/Tabs/About/AboutSectionView.swift create mode 100644 Conjugate/Views/Tabs/About/AboutTipCardView.swift create mode 100644 Conjugate/Views/Tabs/About/ShareSheet.swift create mode 100644 Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json create mode 100644 Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Image.png diff --git a/Conjugate/Views/Tabs/About/AboutInfoView.swift b/Conjugate/Views/Tabs/About/AboutInfoView.swift new file mode 100644 index 00000000..049bd462 --- /dev/null +++ b/Conjugate/Views/Tabs/About/AboutInfoView.swift @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * Detail information screens navigated to from the About tab. + */ + +import SwiftUI + +enum AboutInfoSection { + case wikimedia + case privacyPolicy + case licenses +} + +struct AboutInfoView: View { + let section: AboutInfoSection + + private var title: String { + switch section { + case .wikimedia: + return NSLocalizedString("i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: "") + case .privacyPolicy: + return NSLocalizedString("i18n._global.privacy_policy", value: "Privacy policy", comment: "") + case .licenses: + return NSLocalizedString("i18n.app.about.legal.third_party", value: "Third-party licenses", comment: "") + } + } + + private var caption: String { + switch section { + case .wikimedia: + return NSLocalizedString("i18n.app.about.community.wikimedia.caption", value: "How we work together", comment: "") + case .privacyPolicy: + return NSLocalizedString("i18n.app.about.legal.privacy_policy.caption", value: "Keeping you safe", comment: "") + case .licenses: + return NSLocalizedString("i18n.app.about.legal.third_party.caption", value: "Whose code we used", comment: "") + } + } + + private var bodyText: String { + switch section { + case .wikimedia: + return wikimediaBodyText + case .privacyPolicy: + return privacyPolicyBodyText + case .licenses: + return thirdPartyLicensesBodyText + } + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 16) { + Text(caption) + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.primary) + + Text(bodyText) + .font(.body) + .foregroundColor(.primary) + .tint(Color("linkBlue")) + } + .padding(20) + .background(Color("lightWhiteDarkBlack")) + .cornerRadius(12) + .padding(.horizontal, 20) + .padding(.top, 16) + } + .padding(.bottom, 32) + } + .background(Color("scribeAppBackground").ignoresSafeArea()) + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + } +} + +// MARK: - Body text + +private let wikimediaBodyText: String = { + let t1 = NSLocalizedString( + "i18n.app.about.community.wikimedia.text_1", + value: "Scribe would not be possible without countless contributions by Wikimedia contributors to the many projects that they support. Specifically Scribe makes use of data from the Wikidata Lexicographical data community, as well as data from Wikipedia for each language that Scribe supports.", + comment: "" + ) + let t2 = NSLocalizedString( + "i18n.app.about.community.wikimedia.text_2", + value: "Wikidata is a collaboratively edited multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely available data that anyone can use under a Creative Commons Public Domain license (CC0). Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form annotations, noun plurals, and many other features.", + comment: "" + ) + let t3 = NSLocalizedString( + "i18n.app.about.community.wikimedia.text_3", + value: "Wikipedia is a multilingual free online encyclopedia written and maintained by a community of volunteers through open collaboration and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions by deriving the most common words in a language as well as the most common words that follow them.", + comment: "" + ) + return [t1, t2, t3].joined(separator: "\n\n") +}() + +private let privacyPolicyBodyText: String = NSLocalizedString( + "i18n.app.about.legal.privacy_policy.text", + value: """ +Please note that the English version of this policy takes precedence over all other versions. + +The Scribe developers (SCRIBE) built the iOS application "Scribe-Conjugate" (SERVICE) as an open-source application. This SERVICE is provided by SCRIBE at no cost and is intended for use as is. + +This privacy policy (POLICY) is used to inform the reader of the policies for the access, tracking, collection, retention, use, and disclosure of personal information (USER INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE (USERS). + +USER INFORMATION is specifically defined as any information related to the USERS themselves or the devices they use to access the SERVICE. + +USER DATA is specifically defined as any text that is typed or actions that are done by the USERS while using the SERVICE. + +1. Policy Statement + +This SERVICE does not access, track, collect, retain, use, or disclose any USER INFORMATION or USER DATA. + +2. Do Not Track + +USERS contacting SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with a copy of this POLICY as well as a link to all source codes as proof that they are not being tracked. + +3. Third-Party Data + +This SERVICE makes use of third-party data. All data used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata, Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and lexeme namespaces is made available under the Creative Commons CC0 License; text in other namespaces is made available under the Creative Commons Attribution-Share Alike License." The policy detailing Wikidata data usage can be found at https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type of data used by the SERVICE, "… can be used under the terms of the Creative Commons Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides permission, "… free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction…" The policy detailing Unicode data usage can be found at https://www.unicode.org/license.txt. + +4. Third-Party Source Code + +This SERVICE was based on third-party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. + +5. Third-Party Services + +This SERVICE makes use of third-party services to manipulate some of the third-party data. Specifically, data has been translated using models from Hugging Face transformers. This service is covered by an Apache License 2.0, which states that it is available for commercial use, modification, distribution, patent use, and private use. The license for the aforementioned service can be found at https://github.com/huggingface/transformers/blob/master/LICENSE. + +6. Third-Party Links + +This SERVICE contains links to external websites. If USERS click on a third-party link, they will be directed to a website. Note that these external websites are not operated by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these websites. This SERVICE has no control over and assumes no responsibility for the content, privacy policies, or practices of any third-party sites or services. + +7. Third-Party Images + +This SERVICE contains images that are copyrighted by third-parties. Specifically, this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia Foundation, Inc. The terms by which the GitHub logo can be used are found on https://github.com/logos, and the terms for the Wikidata logo are found on the following Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. + +8. Content Notice + +This SERVICE allows USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in a way that the information is unavailable unless explicitly known. SCRIBE takes no responsibility for the access of such CONTENT. + +9. Changes + +This POLICY is subject to change. Updates to this POLICY will replace all prior instances, and if deemed material will further be clearly stated in the next applicable update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the latest information on our privacy practices and to familiarize themselves with any changes. + +10. Contact + +If you have any questions, concerns, or suggestions about this POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at scribe.langauge@gmail.com. The person responsible for such inquiries is Andrew Tavis McAllister. + +11. Effective Date + +This POLICY is effective as of the 24th of May, 2022. +""", + comment: "" +) + +private let thirdPartyLicensesBodyText: String = { + let intro = NSLocalizedString( + "i18n.app.about.legal.third_party.text", + value: "The Scribe developers (SCRIBE) built the iOS application \"Scribe-Conjugate\" (SERVICE) using third party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. This section lists the source code on which the SERVICE was based as well as the coinciding licenses of each.\n\nThe following is a list of all used source code, the main author or authors of the code, the license under which it was released at time of usage, and a link to the license.", + comment: "" + ) + let entry1 = NSLocalizedString( + "i18n.app.about.legal.third_party.entry_custom_keyboard", + value: "Custom Keyboard\n• Author: EthanSK\n• License: MIT\n• Link: https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE", + comment: "" + ) + let entry2 = NSLocalizedString( + "i18n.app.about.legal.third_party.entry_simple_keyboard", + value: "Simple Keyboard\n• Author: Simple Mobile Tools\n• License: GPL-3.0\n• Link: https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE", + comment: "" + ) + return "\(intro)\n\n1. \(entry1)\n\n2. \(entry2)" +}() + diff --git a/Conjugate/Views/Tabs/About/AboutRowView.swift b/Conjugate/Views/Tabs/About/AboutRowView.swift new file mode 100644 index 00000000..0356ac4f --- /dev/null +++ b/Conjugate/Views/Tabs/About/AboutRowView.swift @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * A single row in the About tab list. + */ + +import SwiftUI + +enum AboutRowTrailing { + case externalLink + case chevron + case reset + case none +} + +struct AboutRowView: View { + let icon: String + let isCustomImage: Bool + let title: String + var trailing: AboutRowTrailing = .none + let action: () -> Void + + init( + icon: String, + isCustomImage: Bool, + title: String, + hasExternalLink: Bool = false, + hasNestedNavigation: Bool = false, + isReset: Bool = false, + action: @escaping () -> Void + ) { + self.icon = icon + self.isCustomImage = isCustomImage + self.title = title + self.action = action + if hasExternalLink { + self.trailing = .externalLink + } else if hasNestedNavigation { + self.trailing = .chevron + } else if isReset { + self.trailing = .reset + } else { + self.trailing = .none + } + } + + var body: some View { + Button(action: action) { + HStack(spacing: 14) { + iconView + .frame(width: 28, height: 28) + .padding(.leading, 16) + + Text(title) + .foregroundColor(.primary) + .font(.body) + + Spacer() + + trailingView + .padding(.trailing, 16) + } + .frame(minHeight: 52) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + // MARK: - Icon + + @ViewBuilder + private var iconView: some View { + if isCustomImage { + Image(icon) + .resizable() + .scaledToFit() + } else { + Image(systemName: icon) + .resizable() + .scaledToFit() + .foregroundColor(.primary) + } + } + + // MARK: - Trailing + + @ViewBuilder + private var trailingView: some View { + switch trailing { + case .externalLink: + Image(systemName: "arrow.up.right.square") + .font(.system(size: 17)) + .foregroundColor(Color(.systemGray3)) + case .chevron: + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color(.systemGray3)) + case .reset: + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 17)) + .foregroundColor(Color(.systemGray3)) + case .none: + EmptyView() + } + } +} diff --git a/Conjugate/Views/Tabs/About/AboutSectionView.swift b/Conjugate/Views/Tabs/About/AboutSectionView.swift new file mode 100644 index 00000000..d880bed6 --- /dev/null +++ b/Conjugate/Views/Tabs/About/AboutSectionView.swift @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * A grouped section container used in the About tab. + */ + +import SwiftUI + +struct AboutSectionView: View { + let heading: String + @ViewBuilder let content: () -> Content + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(heading) + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.horizontal, 20) + .padding(.top, 24) + .padding(.bottom, 10) + + VStack(spacing: 0) { + content() + } + .background(Color("lightWhiteDarkBlack")) + .cornerRadius(12) + .padding(.horizontal, 20) + } + } +} diff --git a/Conjugate/Views/Tabs/About/AboutTab.swift b/Conjugate/Views/Tabs/About/AboutTab.swift index f3e878c1..7c1b0730 100644 --- a/Conjugate/Views/Tabs/About/AboutTab.swift +++ b/Conjugate/Views/Tabs/About/AboutTab.swift @@ -1,13 +1,253 @@ // SPDX-License-Identifier: GPL-3.0-or-later +/** + * The About tab for the Scribe-Conjugate app. + */ + +import StoreKit import SwiftUI struct AboutTab: View { - var body: some View { - AppNavigation { - Text("About") - .font(.largeTitle) - .navigationTitle("About") + @State private var showShareSheet = false + @State private var showAppHintsConfirmation = false + @State private var tipCardVisible: Bool = { + UserDefaults.standard.object(forKey: "aboutTipCardState") as? Bool ?? true + }() + + private var appVersion: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "—" + } + + var body: some View { + AppNavigation { + ZStack(alignment: .top) { + Color("scribeAppBackground") + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 0) { + if tipCardVisible { + AboutTipCardView( + infoText: NSLocalizedString( + "i18n.app.about.tip_card", + value: "Tap a row to learn more or take action.", + comment: "" + ), + isVisible: $tipCardVisible, + onDismiss: { + UserDefaults.standard.set(false, forKey: "aboutTipCardState") + } + ) + .padding(.horizontal, 20) + .padding(.top, 12) + .transition(.opacity.combined(with: .move(edge: .top))) + } + + // MARK: Community + + AboutSectionView( + heading: NSLocalizedString("i18n.app.about.community.title", value: "Community", comment: "") + ) { + AboutRowView( + icon: "github", + isCustomImage: true, + title: NSLocalizedString("i18n.app.about.community.github", value: "See the code on GitHub", comment: ""), + hasExternalLink: true + ) { openURL("https://github.com/scribe-org/Scribe-Conjugate") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "matrix", + isCustomImage: true, + title: NSLocalizedString("i18n.app.about.community.matrix", value: "Chat with the team on Matrix", comment: ""), + hasExternalLink: true + ) { openURL("https://matrix.to/#/#scribe_community:matrix.org", encoded: true) } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "mastodon", + isCustomImage: true, + title: NSLocalizedString("i18n.app.about.community.mastodon", value: "Follow us on Mastodon", comment: ""), + hasExternalLink: true + ) { openURL("https://wikis.world/@scribe") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "Bluesky", + isCustomImage: true, + title: NSLocalizedString("i18n.app.about.community.bluesky", value: "Follow us on Bluesky", comment: ""), + hasExternalLink: true + ) { openURL("https://bsky.app/profile/scribe-org.bsky.social") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "globe", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.community.visit_website", value: "Visit the Scribe website", comment: ""), + hasExternalLink: true + ) { openURL("https://scri.be/") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "square.and.arrow.up", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.community.share_conjugate", value: "Share Scribe Conjugate", comment: ""), + hasExternalLink: true + ) { showShareSheet = true } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "wikimedia", + isCustomImage: true, + title: NSLocalizedString("i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: ""), + hasNestedNavigation: true + ) {} + .overlay( + NavigationLink(destination: AboutInfoView(section: .wikimedia)) { Color.clear } + ) + } + + // MARK: Feedback and support + + AboutSectionView( + heading: NSLocalizedString("i18n.app.about.feedback.title", value: "Feedback and support", comment: "") + ) { + AboutRowView( + icon: "star.fill", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.feedback.rate_scribe", value: "Rate Scribe Conjugate", comment: ""), + hasExternalLink: true + ) { rateApp() } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "ladybug", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.feedback.bug_report", value: "Report a bug", comment: ""), + hasExternalLink: true + ) { openURL("https://github.com/scribe-org/Scribe-Conjugate/issues") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "envelope", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.feedback.send_email", value: "Send us an email", comment: ""), + hasExternalLink: true + ) { sendEmail() } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "bookmark.fill", + isCustomImage: false, + title: String( + format: NSLocalizedString("i18n.app.about.feedback.version", value: "Version %@", comment: ""), + appVersion + ), + hasExternalLink: true + ) { openURL("https://github.com/scribe-org/Scribe-Conjugate/releases") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "lightbulb", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.feedback.reset_app_hints", value: "Reset app hints", comment: ""), + isReset: true + ) { showAppHintsConfirmation = true } + } + + // MARK: Legal + + AboutSectionView( + heading: NSLocalizedString("i18n._global.legal", value: "Legal", comment: "") + ) { + AboutRowView( + icon: "lock.shield", + isCustomImage: false, + title: NSLocalizedString("i18n._global.privacy_policy", value: "Privacy policy", comment: ""), + hasNestedNavigation: true + ) {} + .overlay( + NavigationLink(destination: AboutInfoView(section: .privacyPolicy)) { Color.clear } + ) + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "doc.text", + isCustomImage: false, + title: NSLocalizedString("i18n.app.about.legal.third_party", value: "Third-party licenses", comment: ""), + hasNestedNavigation: true + ) {} + .overlay( + NavigationLink(destination: AboutInfoView(section: .licenses)) { Color.clear } + ) + } + + Spacer(minLength: 32) + } + .padding(.top, 8) + .animation(.easeInOut(duration: 0.2), value: tipCardVisible) } + } + .navigationTitle(NSLocalizedString("i18n.app.about.title", value: "About", comment: "")) + .navigationBarTitleDisplayMode(.large) + } + .sheet(isPresented: $showShareSheet) { + ShareSheet(items: [conjugateShareURL()]) } + .alert( + NSLocalizedString("i18n.app.about.feedback.reset_app_hints", value: "Reset app hints", comment: ""), + isPresented: $showAppHintsConfirmation + ) { + Button(NSLocalizedString("i18n._global.cancel", value: "Cancel", comment: ""), role: .cancel) {} + Button(NSLocalizedString("i18n._global.reset", value: "Reset", comment: "")) { + resetAppHints() + } + } message: { + Text(NSLocalizedString( + "i18n.app.about.feedback.reset_app_hints.message", + value: "This will reset all app hints to their default visible state.", + comment: "" + )) + } + } + + // MARK: - Actions + + private func openURL(_ urlString: String, encoded: Bool = false) { + let target = encoded + ? (urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString) + : urlString + guard let url = URL(string: target) else { return } + UIApplication.shared.open(url) + } + + private func rateApp() { + guard let scene = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else { return } + SKStoreReviewController.requestReview(in: scene) + } + + private func sendEmail() { + openURL("mailto:team@scri.be") + } + + private func resetAppHints() { + UserDefaults.standard.set(true, forKey: "aboutTipCardState") + withAnimation { tipCardVisible = true } + } + + private func conjugateShareURL() -> URL { + URL(string: "https://scri.be/")! + } } diff --git a/Conjugate/Views/Tabs/About/AboutTipCardView.swift b/Conjugate/Views/Tabs/About/AboutTipCardView.swift new file mode 100644 index 00000000..232f5b9e --- /dev/null +++ b/Conjugate/Views/Tabs/About/AboutTipCardView.swift @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * Tip card shown at the top of the About tab. + */ + +import SwiftUI + +struct AboutTipCardView: View { + let infoText: String + @Binding var isVisible: Bool + var onDismiss: (() -> Void)? + + private let cardCornerRadius: CGFloat = 10 + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: cardCornerRadius) + .fill(Color("lightWhiteDarkBlack")) + HStack(spacing: 12) { + Image(systemName: "lightbulb.max") + .resizable() + .scaledToFit() + .frame(width: 28, height: 28) + .foregroundColor(Color("scribeCTA")) + .padding(.leading, 12) + + Text(infoText) + .font(.subheadline) + .foregroundColor(.primary) + + Spacer() + + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isVisible = false + } + onDismiss?() + } label: { + Text("OK") + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(Color("scribeBlue")) + .cornerRadius(cardCornerRadius) + } + .padding(.trailing, 12) + } + } + .frame(maxWidth: .infinity) + .frame(height: 70) + .shadow(color: Color("keyShadow").opacity(0.4), radius: 5, x: 0, y: 2) + } +} + diff --git a/Conjugate/Views/Tabs/About/ShareSheet.swift b/Conjugate/Views/Tabs/About/ShareSheet.swift new file mode 100644 index 00000000..906d06a2 --- /dev/null +++ b/Conjugate/Views/Tabs/About/ShareSheet.swift @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * UIKit bridge for presenting the system share sheet in SwiftUI. + */ + +import SwiftUI +import UIKit + +struct ShareSheet: UIViewControllerRepresentable { + let items: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: items, applicationActivities: nil) + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} diff --git a/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json b/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json new file mode 100644 index 00000000..17f6fd94 --- /dev/null +++ b/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Image.png b/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Image.png new file mode 100644 index 0000000000000000000000000000000000000000..99badf838df3ea3dee31a538ea8a177aef36a0d7 GIT binary patch literal 2720 zcmV;R3Sae!P)mC`0000)WmrjOO-%qQ0000800D<-00aO40096102%-Q00003paB2_0000100961 zpaK8{000010001ZpaTE|000010001L0000062g%T000UHNklq6bK#)49g$Dg2_R!!Qfa7j)1@sFdPId1`I3;l}$_(0fXXUqDW9NWKp1~ zZ}!%t@7$TWGyRyUo;!6;Z_l&4s(w?C?w&ilb*i3Tk;AV^xhUn_;*w*jf0J@Y%8|1z zn=>WWz@ySWm|T$Zf|T=8I0oI70xs92d?#gzwwCrz0R2jiDX5jxE4#Pk2#u~ZymIAy zz()gsdRfZz9dE|?cBN1k8tH3^#7I=fc3!wPy4DA ztooD!jL8FTSIgzzI+}s5(@OVa@5-gVz`DJF!^={Rw--lg_{GI`NOe|j*7!|qxE<2i z2Hi_s#lUu?oQWVeIQ%E&mRj)7vJ+~(b$sEnaos#oGp%X>7pzvhdN%9`uKL-B9VbLb;0-*60WZg5JSnVgG2%jXX zkN%FaV6x{SXkR*E2$ou?A!PCG1KH65x)_4a1Mnk5^t9-MaBy7;S^K6DGUJlb!BBJo zfS(wuw?#h)N?hI~kadnBRj$(n;aTG1UrlEdA1BKz2jq$S8nbzlsRJpageEL_EJ9%1 zH3`^MNkDsCO(_us&Q<0oe+;gk=S0PC{wf$R)5`Qr8)x(&~x$K0wA0Bmg`fQ*xsm7G9kOb^f2 zsF

#uWSjGGoUaCPNh5(S|}Otby)h!4|UUI{r}Hp%jaXQ+^fPp=YSH|8;c{gPBpX zN~%Ds{2d5mR>3WFh6UfZs=WnY#9|UJYm`GWSd4?M0}QY%iu2}!6-xCMMr`>CfUOOa znuKHx)xw)0EcXqyp4+60&c`hdliHYPv7ER(7SZ>IV|DJ)Z;@o>;G$JFGU~B1Yx<7y zYO%^PL|qJ%EN9VmO2CT7NV@3LbjDnxta75m>R7mj=i(4^zP$ZE6p>WXXHHbiP!fY7 zOA8Js9IgL*MJ^d)IunnPO;Y8kt?WyV_q)^%B6j#bH%+>Uf-E z+*REyx#(O0MwnFbm>8d1+*NR?3$L8Ji&la5Wdm!DMY+aZSNed{Z7L&(r+3a(jo zR;)27z`46<6^c5b`^4*NKi2k3EL{sOQMblz06&1+1|Sc>6vn(&6%`*r(W?VfJw5ZmS`yeSg>ow zSgyOT*G-{b!qEj7;eMQggbgyd?{U>xbi-}g9bWnt+35=VKgmAdnw%mr6w;VG#l%W6 zqe!% zC;KE?&s2`jyEZjISjB=@sL6u$LacadD=jM9RBDncthbixVz7JRp6rw>6Yhz5+k^%H zcejm`br#IM3@vi|*<_o{Q#q&LQa4f;fVp`Fxlx6qoah8DoUL=w$~_Hk!nd7OUduH@ zSln`}<*ESNy3wI#mh0=qb$sM{reQUJ{y&oPo)oq_Q6GS{L7t#PDGa0L3|qC5wnpcd zq}=S=P1hLQ)Go1kUy82MG+j3z@2>%@^jj&lGgMuh3k6vF0caM`r@WA^vZky7mBk)F z1>DZASgx(5SO8ZW2kfV$P~*yZ0NPe@R1n@J;@aGaxdY^;G!fSzFM$3oh4<3gR$a(u zIHdx(i8BN~yEw@<$=c(KdTQ|;uy{j;|7t_6zyYdm%Vt^{1K`(1q#Y?(_a+gE63cni zorX9ulrt2-Sn9r%O#`(Fqa!&1*w>mah;=+Qz$9I~;MNv@W66?ZJ6Q5nDJu*0v4lwl zFc!;u$1||qkV*!0Y=B9eK9xd@ovFVDuw*PcT8xefFj+8PgPpBbln_9GyC;P#JeayJ zHmvK2SUWE%uG3Pt^UMQWmx4}%X*;-u6LxpFV8+YWKj#E2+yqK>ioSg8S~b}>X|zs|YbfgLqQ@52r+>0gTr>%bS4Qg7>Dc#d62R_~ z9P6>U5f5f>@&!1@?^?Q6%L{F}5!05v$roTg){+xpvAAcY9No10fT|p8YhQqKvtZXf zt5S0NRD~D7Av0MlY2`57z;c`Ba{rKXUfHQWB?HxMGPJVbYr!;n*xIt4lUG|<`LgAl zrBiJ(wDK8Z!94jlq*$M_#CqNM>na2pm{X^=O5WCsKp_@PqGCOc5qq1!MSf(kZpGf# zi+~{(%>59r>Ud3~6?doo(`LQevt=tL`+rjz^{GQFSo?cNHoOMm+CZXuo%L$BR;?J% z^S(0aTgR|q`o@B>WGjSncBQnrXO~xtR*dCh!9lbgboYasb(Y1-G9q!ck8=K%>mQcF zgKB}uZN7=YNGyQiMF-+zgN)Vgc_;_|S#y2NO_Y-D@ ze5oM`xac7jz_bm>xXPEX31fD~(Yo<;~0&hwgjE1&gS1>GNUrL{32A75fBzJAnM;$58UB}8i9K8Q^ zASJb@spB`wYEo^(JrC~4^#F|mv21nW#6MgyfTAcAW`W=ne&L(#b5bDq14zD$1<#aG adHx?m0*TXoL~;iJ0000 Date: Tue, 24 Mar 2026 19:36:49 +0530 Subject: [PATCH 2/8] fix: Remove extra trailing newlines to pass SwiftLint --- Conjugate/Views/Tabs/About/AboutInfoView.swift | 1 - Conjugate/Views/Tabs/About/AboutTipCardView.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Conjugate/Views/Tabs/About/AboutInfoView.swift b/Conjugate/Views/Tabs/About/AboutInfoView.swift index 049bd462..353a7543 100644 --- a/Conjugate/Views/Tabs/About/AboutInfoView.swift +++ b/Conjugate/Views/Tabs/About/AboutInfoView.swift @@ -175,4 +175,3 @@ private let thirdPartyLicensesBodyText: String = { ) return "\(intro)\n\n1. \(entry1)\n\n2. \(entry2)" }() - diff --git a/Conjugate/Views/Tabs/About/AboutTipCardView.swift b/Conjugate/Views/Tabs/About/AboutTipCardView.swift index 232f5b9e..48845689 100644 --- a/Conjugate/Views/Tabs/About/AboutTipCardView.swift +++ b/Conjugate/Views/Tabs/About/AboutTipCardView.swift @@ -51,4 +51,3 @@ struct AboutTipCardView: View { .shadow(color: Color("keyShadow").opacity(0.4), radius: 5, x: 0, y: 2) } } - From 9fb119f8e9f3e8dabad43729e18c92198e8411fd Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:21:32 +0530 Subject: [PATCH 3/8] fix: remove DownloadDataScreen, DownloadStateManager and DynamicConjugationViewController from Conjugate target These files are Scribe keyboard app-only and were incorrectly included in the Conjugate app target, causing 'Cannot find type' build errors. None of them are used by the Conjugate app. --- Scribe.xcodeproj/project.pbxproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index 2ad3e9d9..c6f11466 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -3632,7 +3632,6 @@ buildActionMask = 2147483647; files = ( E97E65172F2CDD730070810A /* ESInterfaceVariables.swift in Sources */, - E9ED938A2F2A3C45008D7451 /* DynamicConjugationViewController.swift in Sources */, F786BAD42F1E8F70003F7505 /* ThirdPartyLicense.swift in Sources */, F786BAD52F1E8F70003F7505 /* (null) in Sources */, F786BAD62F1E8F70003F7505 /* AppTextStyling.swift in Sources */, @@ -3641,7 +3640,6 @@ F786BAD82F1E8F70003F7505 /* Conjugate.swift in Sources */, F786BAD92F1E8F70003F7505 /* SettingsViewController.swift in Sources */, F786BADA2F1E8F70003F7505 /* AboutTableData.swift in Sources */, - F786BADB2F1E8F70003F7505 /* DownloadStateManager.swift in Sources */, F786BADC2F1E8F70003F7505 /* IDInterfaceVariables.swift in Sources */, F786BADE2F1E8F70003F7505 /* ToolTipViewDatasource.swift in Sources */, F786BADF2F1E8F70003F7505 /* Translate.swift in Sources */, @@ -3671,7 +3669,6 @@ F786BAFD2F1E8F70003F7505 /* ToolTipViewTheme.swift in Sources */, F786BAFE2F1E8F70003F7505 /* RadioTableViewCell.swift in Sources */, F786BAFF2F1E8F70003F7505 /* CommandVariables.swift in Sources */, - F786BB002F1E8F70003F7505 /* DownloadDataScreen.swift in Sources */, F786BB012F1E8F70003F7505 /* Utilities.swift in Sources */, F786BB022F1E8F70003F7505 /* BaseTableViewController.swift in Sources */, E98034E52F45B88C006C1CDC /* ToastView.swift in Sources */, From d3a090e23db9045010c1c5ab99500f398fc08b97 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:59:29 +0530 Subject: [PATCH 4/8] fix: remove ToastView and SelectionViewTemplateViewController from Conjugate target Both files depend on DownloadStateManager which is Scribe-app-only, causing 'Cannot find type DownloadStateManager' build errors in the Conjugate target. --- Scribe.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index c6f11466..9ab3ef61 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -3671,7 +3671,6 @@ F786BAFF2F1E8F70003F7505 /* CommandVariables.swift in Sources */, F786BB012F1E8F70003F7505 /* Utilities.swift in Sources */, F786BB022F1E8F70003F7505 /* BaseTableViewController.swift in Sources */, - E98034E52F45B88C006C1CDC /* ToastView.swift in Sources */, F786BB032F1E8F70003F7505 /* DAInterfaceVariables.swift in Sources */, F786BB052F1E8F70003F7505 /* UIDeviceExtensions.swift in Sources */, E97E65222F2CDEC50070810A /* ESCommandVariables.swift in Sources */, @@ -3688,7 +3687,6 @@ F786BB102F1E8F70003F7505 /* CommandBar.swift in Sources */, F786BB122F1E8F70003F7505 /* ViewThemeable.swift in Sources */, F786BB132F1E8F70003F7505 /* ColorVariables.swift in Sources */, - F786BB142F1E8F70003F7505 /* SelectionViewTemplateViewController.swift in Sources */, F786BB152F1E8F70003F7505 /* AboutTableViewCell.swift in Sources */, F786BB162F1E8F70003F7505 /* InstallScreen.swift in Sources */, F786BB182F1E8F70003F7505 /* KeyboardKeys.swift in Sources */, From 2c3b31b5e9f6a7e1868ba23f68091f70d66ac222 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Tue, 24 Mar 2026 22:17:35 +0100 Subject: [PATCH 5/8] Indent using four spaces to match Swift standards --- .../Views/Tabs/About/AboutInfoView.swift | 259 ++++----- Conjugate/Views/Tabs/About/AboutRowView.swift | 160 +++--- .../Views/Tabs/About/AboutSectionView.swift | 36 +- Conjugate/Views/Tabs/About/AboutTab.swift | 514 ++++++++++-------- .../Views/Tabs/About/AboutTipCardView.swift | 82 +-- Conjugate/Views/Tabs/About/ShareSheet.swift | 10 +- 6 files changed, 562 insertions(+), 499 deletions(-) diff --git a/Conjugate/Views/Tabs/About/AboutInfoView.swift b/Conjugate/Views/Tabs/About/AboutInfoView.swift index 353a7543..547373c2 100644 --- a/Conjugate/Views/Tabs/About/AboutInfoView.swift +++ b/Conjugate/Views/Tabs/About/AboutInfoView.swift @@ -7,171 +7,186 @@ import SwiftUI enum AboutInfoSection { - case wikimedia - case privacyPolicy - case licenses + case wikimedia + case privacyPolicy + case licenses } struct AboutInfoView: View { - let section: AboutInfoSection - - private var title: String { - switch section { - case .wikimedia: - return NSLocalizedString("i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: "") - case .privacyPolicy: - return NSLocalizedString("i18n._global.privacy_policy", value: "Privacy policy", comment: "") - case .licenses: - return NSLocalizedString("i18n.app.about.legal.third_party", value: "Third-party licenses", comment: "") + let section: AboutInfoSection + + private var title: String { + switch section { + case .wikimedia: + return NSLocalizedString( + "i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: "") + case .privacyPolicy: + return NSLocalizedString( + "i18n._global.privacy_policy", value: "Privacy policy", comment: "") + case .licenses: + return NSLocalizedString( + "i18n.app.about.legal.third_party", value: "Third-party licenses", comment: "") + } } - } - - private var caption: String { - switch section { - case .wikimedia: - return NSLocalizedString("i18n.app.about.community.wikimedia.caption", value: "How we work together", comment: "") - case .privacyPolicy: - return NSLocalizedString("i18n.app.about.legal.privacy_policy.caption", value: "Keeping you safe", comment: "") - case .licenses: - return NSLocalizedString("i18n.app.about.legal.third_party.caption", value: "Whose code we used", comment: "") + + private var caption: String { + switch section { + case .wikimedia: + return NSLocalizedString( + "i18n.app.about.community.wikimedia.caption", value: "How we work together", + comment: "") + case .privacyPolicy: + return NSLocalizedString( + "i18n.app.about.legal.privacy_policy.caption", value: "Keeping you safe", + comment: "") + case .licenses: + return NSLocalizedString( + "i18n.app.about.legal.third_party.caption", value: "Whose code we used", comment: "" + ) + } } - } - - private var bodyText: String { - switch section { - case .wikimedia: - return wikimediaBodyText - case .privacyPolicy: - return privacyPolicyBodyText - case .licenses: - return thirdPartyLicensesBodyText + + private var bodyText: String { + switch section { + case .wikimedia: + return wikimediaBodyText + case .privacyPolicy: + return privacyPolicyBodyText + case .licenses: + return thirdPartyLicensesBodyText + } } - } - - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - VStack(alignment: .leading, spacing: 16) { - Text(caption) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text(bodyText) - .font(.body) - .foregroundColor(.primary) - .tint(Color("linkBlue")) + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 16) { + Text(caption) + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.primary) + + Text(bodyText) + .font(.body) + .foregroundColor(.primary) + .tint(Color("linkBlue")) + } + .padding(20) + .background(Color("lightWhiteDarkBlack")) + .cornerRadius(12) + .padding(.horizontal, 20) + .padding(.top, 16) + } + .padding(.bottom, 32) } - .padding(20) - .background(Color("lightWhiteDarkBlack")) - .cornerRadius(12) - .padding(.horizontal, 20) - .padding(.top, 16) - } - .padding(.bottom, 32) + .background(Color("scribeAppBackground").ignoresSafeArea()) + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) } - .background(Color("scribeAppBackground").ignoresSafeArea()) - .navigationTitle(title) - .navigationBarTitleDisplayMode(.inline) - } } // MARK: - Body text private let wikimediaBodyText: String = { - let t1 = NSLocalizedString( - "i18n.app.about.community.wikimedia.text_1", - value: "Scribe would not be possible without countless contributions by Wikimedia contributors to the many projects that they support. Specifically Scribe makes use of data from the Wikidata Lexicographical data community, as well as data from Wikipedia for each language that Scribe supports.", - comment: "" - ) - let t2 = NSLocalizedString( - "i18n.app.about.community.wikimedia.text_2", - value: "Wikidata is a collaboratively edited multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely available data that anyone can use under a Creative Commons Public Domain license (CC0). Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form annotations, noun plurals, and many other features.", - comment: "" - ) - let t3 = NSLocalizedString( - "i18n.app.about.community.wikimedia.text_3", - value: "Wikipedia is a multilingual free online encyclopedia written and maintained by a community of volunteers through open collaboration and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions by deriving the most common words in a language as well as the most common words that follow them.", - comment: "" - ) - return [t1, t2, t3].joined(separator: "\n\n") + let t1 = NSLocalizedString( + "i18n.app.about.community.wikimedia.text_1", + value: + "Scribe would not be possible without countless contributions by Wikimedia contributors to the many projects that they support. Specifically Scribe makes use of data from the Wikidata Lexicographical data community, as well as data from Wikipedia for each language that Scribe supports.", + comment: "" + ) + let t2 = NSLocalizedString( + "i18n.app.about.community.wikimedia.text_2", + value: + "Wikidata is a collaboratively edited multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely available data that anyone can use under a Creative Commons Public Domain license (CC0). Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form annotations, noun plurals, and many other features.", + comment: "" + ) + let t3 = NSLocalizedString( + "i18n.app.about.community.wikimedia.text_3", + value: + "Wikipedia is a multilingual free online encyclopedia written and maintained by a community of volunteers through open collaboration and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions by deriving the most common words in a language as well as the most common words that follow them.", + comment: "" + ) + return [t1, t2, t3].joined(separator: "\n\n") }() private let privacyPolicyBodyText: String = NSLocalizedString( - "i18n.app.about.legal.privacy_policy.text", - value: """ -Please note that the English version of this policy takes precedence over all other versions. + "i18n.app.about.legal.privacy_policy.text", + value: """ + Please note that the English version of this policy takes precedence over all other versions. -The Scribe developers (SCRIBE) built the iOS application "Scribe-Conjugate" (SERVICE) as an open-source application. This SERVICE is provided by SCRIBE at no cost and is intended for use as is. + The Scribe developers (SCRIBE) built the iOS application "Scribe-Conjugate" (SERVICE) as an open-source application. This SERVICE is provided by SCRIBE at no cost and is intended for use as is. -This privacy policy (POLICY) is used to inform the reader of the policies for the access, tracking, collection, retention, use, and disclosure of personal information (USER INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE (USERS). + This privacy policy (POLICY) is used to inform the reader of the policies for the access, tracking, collection, retention, use, and disclosure of personal information (USER INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE (USERS). -USER INFORMATION is specifically defined as any information related to the USERS themselves or the devices they use to access the SERVICE. + USER INFORMATION is specifically defined as any information related to the USERS themselves or the devices they use to access the SERVICE. -USER DATA is specifically defined as any text that is typed or actions that are done by the USERS while using the SERVICE. + USER DATA is specifically defined as any text that is typed or actions that are done by the USERS while using the SERVICE. -1. Policy Statement + 1. Policy Statement -This SERVICE does not access, track, collect, retain, use, or disclose any USER INFORMATION or USER DATA. + This SERVICE does not access, track, collect, retain, use, or disclose any USER INFORMATION or USER DATA. -2. Do Not Track + 2. Do Not Track -USERS contacting SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with a copy of this POLICY as well as a link to all source codes as proof that they are not being tracked. + USERS contacting SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with a copy of this POLICY as well as a link to all source codes as proof that they are not being tracked. -3. Third-Party Data + 3. Third-Party Data -This SERVICE makes use of third-party data. All data used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata, Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and lexeme namespaces is made available under the Creative Commons CC0 License; text in other namespaces is made available under the Creative Commons Attribution-Share Alike License." The policy detailing Wikidata data usage can be found at https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type of data used by the SERVICE, "… can be used under the terms of the Creative Commons Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides permission, "… free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction…" The policy detailing Unicode data usage can be found at https://www.unicode.org/license.txt. + This SERVICE makes use of third-party data. All data used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata, Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and lexeme namespaces is made available under the Creative Commons CC0 License; text in other namespaces is made available under the Creative Commons Attribution-Share Alike License." The policy detailing Wikidata data usage can be found at https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type of data used by the SERVICE, "… can be used under the terms of the Creative Commons Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides permission, "… free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction…" The policy detailing Unicode data usage can be found at https://www.unicode.org/license.txt. -4. Third-Party Source Code + 4. Third-Party Source Code -This SERVICE was based on third-party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. + This SERVICE was based on third-party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. -5. Third-Party Services + 5. Third-Party Services -This SERVICE makes use of third-party services to manipulate some of the third-party data. Specifically, data has been translated using models from Hugging Face transformers. This service is covered by an Apache License 2.0, which states that it is available for commercial use, modification, distribution, patent use, and private use. The license for the aforementioned service can be found at https://github.com/huggingface/transformers/blob/master/LICENSE. + This SERVICE makes use of third-party services to manipulate some of the third-party data. Specifically, data has been translated using models from Hugging Face transformers. This service is covered by an Apache License 2.0, which states that it is available for commercial use, modification, distribution, patent use, and private use. The license for the aforementioned service can be found at https://github.com/huggingface/transformers/blob/master/LICENSE. -6. Third-Party Links + 6. Third-Party Links -This SERVICE contains links to external websites. If USERS click on a third-party link, they will be directed to a website. Note that these external websites are not operated by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these websites. This SERVICE has no control over and assumes no responsibility for the content, privacy policies, or practices of any third-party sites or services. + This SERVICE contains links to external websites. If USERS click on a third-party link, they will be directed to a website. Note that these external websites are not operated by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these websites. This SERVICE has no control over and assumes no responsibility for the content, privacy policies, or practices of any third-party sites or services. -7. Third-Party Images + 7. Third-Party Images -This SERVICE contains images that are copyrighted by third-parties. Specifically, this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia Foundation, Inc. The terms by which the GitHub logo can be used are found on https://github.com/logos, and the terms for the Wikidata logo are found on the following Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. + This SERVICE contains images that are copyrighted by third-parties. Specifically, this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia Foundation, Inc. The terms by which the GitHub logo can be used are found on https://github.com/logos, and the terms for the Wikidata logo are found on the following Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. -8. Content Notice + 8. Content Notice -This SERVICE allows USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in a way that the information is unavailable unless explicitly known. SCRIBE takes no responsibility for the access of such CONTENT. + This SERVICE allows USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in a way that the information is unavailable unless explicitly known. SCRIBE takes no responsibility for the access of such CONTENT. -9. Changes + 9. Changes -This POLICY is subject to change. Updates to this POLICY will replace all prior instances, and if deemed material will further be clearly stated in the next applicable update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the latest information on our privacy practices and to familiarize themselves with any changes. + This POLICY is subject to change. Updates to this POLICY will replace all prior instances, and if deemed material will further be clearly stated in the next applicable update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the latest information on our privacy practices and to familiarize themselves with any changes. -10. Contact + 10. Contact -If you have any questions, concerns, or suggestions about this POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at scribe.langauge@gmail.com. The person responsible for such inquiries is Andrew Tavis McAllister. + If you have any questions, concerns, or suggestions about this POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at scribe.langauge@gmail.com. The person responsible for such inquiries is Andrew Tavis McAllister. -11. Effective Date + 11. Effective Date -This POLICY is effective as of the 24th of May, 2022. -""", - comment: "" + This POLICY is effective as of the 24th of May, 2022. + """, + comment: "" ) private let thirdPartyLicensesBodyText: String = { - let intro = NSLocalizedString( - "i18n.app.about.legal.third_party.text", - value: "The Scribe developers (SCRIBE) built the iOS application \"Scribe-Conjugate\" (SERVICE) using third party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. This section lists the source code on which the SERVICE was based as well as the coinciding licenses of each.\n\nThe following is a list of all used source code, the main author or authors of the code, the license under which it was released at time of usage, and a link to the license.", - comment: "" - ) - let entry1 = NSLocalizedString( - "i18n.app.about.legal.third_party.entry_custom_keyboard", - value: "Custom Keyboard\n• Author: EthanSK\n• License: MIT\n• Link: https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE", - comment: "" - ) - let entry2 = NSLocalizedString( - "i18n.app.about.legal.third_party.entry_simple_keyboard", - value: "Simple Keyboard\n• Author: Simple Mobile Tools\n• License: GPL-3.0\n• Link: https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE", - comment: "" - ) - return "\(intro)\n\n1. \(entry1)\n\n2. \(entry2)" + let intro = NSLocalizedString( + "i18n.app.about.legal.third_party.text", + value: + "The Scribe developers (SCRIBE) built the iOS application \"Scribe-Conjugate\" (SERVICE) using third party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. This section lists the source code on which the SERVICE was based as well as the coinciding licenses of each.\n\nThe following is a list of all used source code, the main author or authors of the code, the license under which it was released at time of usage, and a link to the license.", + comment: "" + ) + let entry1 = NSLocalizedString( + "i18n.app.about.legal.third_party.entry_custom_keyboard", + value: + "Custom Keyboard\n• Author: EthanSK\n• License: MIT\n• Link: https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE", + comment: "" + ) + let entry2 = NSLocalizedString( + "i18n.app.about.legal.third_party.entry_simple_keyboard", + value: + "Simple Keyboard\n• Author: Simple Mobile Tools\n• License: GPL-3.0\n• Link: https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE", + comment: "" + ) + return "\(intro)\n\n1. \(entry1)\n\n2. \(entry2)" }() diff --git a/Conjugate/Views/Tabs/About/AboutRowView.swift b/Conjugate/Views/Tabs/About/AboutRowView.swift index 0356ac4f..b89d3c7a 100644 --- a/Conjugate/Views/Tabs/About/AboutRowView.swift +++ b/Conjugate/Views/Tabs/About/AboutRowView.swift @@ -7,100 +7,100 @@ import SwiftUI enum AboutRowTrailing { - case externalLink - case chevron - case reset - case none + case externalLink + case chevron + case reset + case none } struct AboutRowView: View { - let icon: String - let isCustomImage: Bool - let title: String - var trailing: AboutRowTrailing = .none - let action: () -> Void + let icon: String + let isCustomImage: Bool + let title: String + var trailing: AboutRowTrailing = .none + let action: () -> Void - init( - icon: String, - isCustomImage: Bool, - title: String, - hasExternalLink: Bool = false, - hasNestedNavigation: Bool = false, - isReset: Bool = false, - action: @escaping () -> Void - ) { - self.icon = icon - self.isCustomImage = isCustomImage - self.title = title - self.action = action - if hasExternalLink { - self.trailing = .externalLink - } else if hasNestedNavigation { - self.trailing = .chevron - } else if isReset { - self.trailing = .reset - } else { - self.trailing = .none + init( + icon: String, + isCustomImage: Bool, + title: String, + hasExternalLink: Bool = false, + hasNestedNavigation: Bool = false, + isReset: Bool = false, + action: @escaping () -> Void + ) { + self.icon = icon + self.isCustomImage = isCustomImage + self.title = title + self.action = action + if hasExternalLink { + self.trailing = .externalLink + } else if hasNestedNavigation { + self.trailing = .chevron + } else if isReset { + self.trailing = .reset + } else { + self.trailing = .none + } } - } - var body: some View { - Button(action: action) { - HStack(spacing: 14) { - iconView - .frame(width: 28, height: 28) - .padding(.leading, 16) + var body: some View { + Button(action: action) { + HStack(spacing: 14) { + iconView + .frame(width: 28, height: 28) + .padding(.leading, 16) - Text(title) - .foregroundColor(.primary) - .font(.body) + Text(title) + .foregroundColor(.primary) + .font(.body) - Spacer() + Spacer() - trailingView - .padding(.trailing, 16) - } - .frame(minHeight: 52) - .contentShape(Rectangle()) + trailingView + .padding(.trailing, 16) + } + .frame(minHeight: 52) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) } - .buttonStyle(.plain) - } - // MARK: - Icon + // MARK: - Icon - @ViewBuilder - private var iconView: some View { - if isCustomImage { - Image(icon) - .resizable() - .scaledToFit() - } else { - Image(systemName: icon) - .resizable() - .scaledToFit() - .foregroundColor(.primary) + @ViewBuilder + private var iconView: some View { + if isCustomImage { + Image(icon) + .resizable() + .scaledToFit() + } else { + Image(systemName: icon) + .resizable() + .scaledToFit() + .foregroundColor(.primary) + } } - } - // MARK: - Trailing + // MARK: - Trailing - @ViewBuilder - private var trailingView: some View { - switch trailing { - case .externalLink: - Image(systemName: "arrow.up.right.square") - .font(.system(size: 17)) - .foregroundColor(Color(.systemGray3)) - case .chevron: - Image(systemName: "chevron.right") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color(.systemGray3)) - case .reset: - Image(systemName: "arrow.counterclockwise") - .font(.system(size: 17)) - .foregroundColor(Color(.systemGray3)) - case .none: - EmptyView() + @ViewBuilder + private var trailingView: some View { + switch trailing { + case .externalLink: + Image(systemName: "arrow.up.right.square") + .font(.system(size: 17)) + .foregroundColor(Color(.systemGray3)) + case .chevron: + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color(.systemGray3)) + case .reset: + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 17)) + .foregroundColor(Color(.systemGray3)) + case .none: + EmptyView() + } } - } } diff --git a/Conjugate/Views/Tabs/About/AboutSectionView.swift b/Conjugate/Views/Tabs/About/AboutSectionView.swift index d880bed6..ed77e0db 100644 --- a/Conjugate/Views/Tabs/About/AboutSectionView.swift +++ b/Conjugate/Views/Tabs/About/AboutSectionView.swift @@ -7,25 +7,25 @@ import SwiftUI struct AboutSectionView: View { - let heading: String - @ViewBuilder let content: () -> Content + let heading: String + @ViewBuilder let content: () -> Content - var body: some View { - VStack(alignment: .leading, spacing: 0) { - Text(heading) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.primary) - .padding(.horizontal, 20) - .padding(.top, 24) - .padding(.bottom, 10) + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(heading) + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.horizontal, 20) + .padding(.top, 24) + .padding(.bottom, 10) - VStack(spacing: 0) { - content() - } - .background(Color("lightWhiteDarkBlack")) - .cornerRadius(12) - .padding(.horizontal, 20) + VStack(spacing: 0) { + content() + } + .background(Color("lightWhiteDarkBlack")) + .cornerRadius(12) + .padding(.horizontal, 20) + } } - } } diff --git a/Conjugate/Views/Tabs/About/AboutTab.swift b/Conjugate/Views/Tabs/About/AboutTab.swift index 7c1b0730..07d67665 100644 --- a/Conjugate/Views/Tabs/About/AboutTab.swift +++ b/Conjugate/Views/Tabs/About/AboutTab.swift @@ -8,246 +8,294 @@ import StoreKit import SwiftUI struct AboutTab: View { - @State private var showShareSheet = false - @State private var showAppHintsConfirmation = false - @State private var tipCardVisible: Bool = { - UserDefaults.standard.object(forKey: "aboutTipCardState") as? Bool ?? true - }() - - private var appVersion: String { - Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "—" - } - - var body: some View { - AppNavigation { - ZStack(alignment: .top) { - Color("scribeAppBackground") - .ignoresSafeArea() - - ScrollView { - VStack(spacing: 0) { - if tipCardVisible { - AboutTipCardView( - infoText: NSLocalizedString( - "i18n.app.about.tip_card", - value: "Tap a row to learn more or take action.", - comment: "" - ), - isVisible: $tipCardVisible, - onDismiss: { - UserDefaults.standard.set(false, forKey: "aboutTipCardState") + @State private var showShareSheet = false + @State private var showAppHintsConfirmation = false + @State private var tipCardVisible: Bool = { + UserDefaults.standard.object(forKey: "aboutTipCardState") as? Bool ?? true + }() + + private var appVersion: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "—" + } + + var body: some View { + AppNavigation { + ZStack(alignment: .top) { + Color("scribeAppBackground") + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 0) { + if tipCardVisible { + AboutTipCardView( + infoText: NSLocalizedString( + "i18n.app.about.tip_card", + value: "Tap a row to learn more or take action.", + comment: "" + ), + isVisible: $tipCardVisible, + onDismiss: { + UserDefaults.standard.set(false, forKey: "aboutTipCardState") + } + ) + .padding(.horizontal, 20) + .padding(.top, 12) + .transition(.opacity.combined(with: .move(edge: .top))) + } + + // MARK: Community + + AboutSectionView( + heading: NSLocalizedString( + "i18n.app.about.community.title", value: "Community", comment: "") + ) { + AboutRowView( + icon: "github", + isCustomImage: true, + title: NSLocalizedString( + "i18n.app.about.community.github", + value: "See the code on GitHub", comment: ""), + hasExternalLink: true + ) { openURL("https://github.com/scribe-org/Scribe-Conjugate") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "matrix", + isCustomImage: true, + title: NSLocalizedString( + "i18n.app.about.community.matrix", + value: "Chat with the team on Matrix", comment: ""), + hasExternalLink: true + ) { + openURL( + "https://matrix.to/#/#scribe_community:matrix.org", + encoded: true) + } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "mastodon", + isCustomImage: true, + title: NSLocalizedString( + "i18n.app.about.community.mastodon", + value: "Follow us on Mastodon", comment: ""), + hasExternalLink: true + ) { openURL("https://wikis.world/@scribe") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "Bluesky", + isCustomImage: true, + title: NSLocalizedString( + "i18n.app.about.community.bluesky", + value: "Follow us on Bluesky", comment: ""), + hasExternalLink: true + ) { openURL("https://bsky.app/profile/scribe-org.bsky.social") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "globe", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.community.visit_website", + value: "Visit the Scribe website", comment: ""), + hasExternalLink: true + ) { openURL("https://scri.be/") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "square.and.arrow.up", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.community.share_conjugate", + value: "Share Scribe Conjugate", comment: ""), + hasExternalLink: true + ) { showShareSheet = true } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "wikimedia", + isCustomImage: true, + title: NSLocalizedString( + "i18n.app.about.community.wikimedia", + value: "Wikimedia and Scribe", comment: ""), + hasNestedNavigation: true + ) {} + .overlay( + NavigationLink(destination: AboutInfoView(section: .wikimedia)) { + Color.clear + } + ) + } + + // MARK: Feedback and support + + AboutSectionView( + heading: NSLocalizedString( + "i18n.app.about.feedback.title", value: "Feedback and support", + comment: "") + ) { + AboutRowView( + icon: "star.fill", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.feedback.rate_scribe", + value: "Rate Scribe Conjugate", comment: ""), + hasExternalLink: true + ) { rateApp() } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "ladybug", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.feedback.bug_report", value: "Report a bug", + comment: ""), + hasExternalLink: true + ) { openURL("https://github.com/scribe-org/Scribe-Conjugate/issues") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "envelope", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.feedback.send_email", value: "Send us an email", + comment: ""), + hasExternalLink: true + ) { sendEmail() } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "bookmark.fill", + isCustomImage: false, + title: String( + format: NSLocalizedString( + "i18n.app.about.feedback.version", value: "Version %@", + comment: ""), + appVersion + ), + hasExternalLink: true + ) { openURL("https://github.com/scribe-org/Scribe-Conjugate/releases") } + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "lightbulb", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.feedback.reset_app_hints", + value: "Reset app hints", comment: ""), + isReset: true + ) { showAppHintsConfirmation = true } + } + + // MARK: Legal + + AboutSectionView( + heading: NSLocalizedString( + "i18n._global.legal", value: "Legal", comment: "") + ) { + AboutRowView( + icon: "lock.shield", + isCustomImage: false, + title: NSLocalizedString( + "i18n._global.privacy_policy", value: "Privacy policy", + comment: ""), + hasNestedNavigation: true + ) {} + .overlay( + NavigationLink(destination: AboutInfoView(section: .privacyPolicy)) { Color.clear } + ) + + Divider().padding(.leading, 54) + + AboutRowView( + icon: "doc.text", + isCustomImage: false, + title: NSLocalizedString( + "i18n.app.about.legal.third_party", + value: "Third-party licenses", comment: ""), + hasNestedNavigation: true + ) {} + .overlay( + NavigationLink(destination: AboutInfoView(section: .licenses)) { + Color.clear + } + ) + } + + Spacer(minLength: 32) + } + .padding(.top, 8) + .animation(.easeInOut(duration: 0.2), value: tipCardVisible) } - ) - .padding(.horizontal, 20) - .padding(.top, 12) - .transition(.opacity.combined(with: .move(edge: .top))) } - - // MARK: Community - - AboutSectionView( - heading: NSLocalizedString("i18n.app.about.community.title", value: "Community", comment: "") - ) { - AboutRowView( - icon: "github", - isCustomImage: true, - title: NSLocalizedString("i18n.app.about.community.github", value: "See the code on GitHub", comment: ""), - hasExternalLink: true - ) { openURL("https://github.com/scribe-org/Scribe-Conjugate") } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "matrix", - isCustomImage: true, - title: NSLocalizedString("i18n.app.about.community.matrix", value: "Chat with the team on Matrix", comment: ""), - hasExternalLink: true - ) { openURL("https://matrix.to/#/#scribe_community:matrix.org", encoded: true) } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "mastodon", - isCustomImage: true, - title: NSLocalizedString("i18n.app.about.community.mastodon", value: "Follow us on Mastodon", comment: ""), - hasExternalLink: true - ) { openURL("https://wikis.world/@scribe") } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "Bluesky", - isCustomImage: true, - title: NSLocalizedString("i18n.app.about.community.bluesky", value: "Follow us on Bluesky", comment: ""), - hasExternalLink: true - ) { openURL("https://bsky.app/profile/scribe-org.bsky.social") } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "globe", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.community.visit_website", value: "Visit the Scribe website", comment: ""), - hasExternalLink: true - ) { openURL("https://scri.be/") } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "square.and.arrow.up", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.community.share_conjugate", value: "Share Scribe Conjugate", comment: ""), - hasExternalLink: true - ) { showShareSheet = true } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "wikimedia", - isCustomImage: true, - title: NSLocalizedString("i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: ""), - hasNestedNavigation: true - ) {} - .overlay( - NavigationLink(destination: AboutInfoView(section: .wikimedia)) { Color.clear } - ) + .navigationTitle(NSLocalizedString("i18n.app.about.title", value: "About", comment: "")) + .navigationBarTitleDisplayMode(.large) + } + .sheet(isPresented: $showShareSheet) { + ShareSheet(items: [conjugateShareURL()]) + } + .alert( + NSLocalizedString( + "i18n.app.about.feedback.reset_app_hints", value: "Reset app hints", comment: ""), + isPresented: $showAppHintsConfirmation + ) { + Button( + NSLocalizedString("i18n._global.cancel", value: "Cancel", comment: ""), + role: .cancel + ) {} + Button(NSLocalizedString("i18n._global.reset", value: "Reset", comment: "")) { + resetAppHints() } + } message: { + Text( + NSLocalizedString( + "i18n.app.about.feedback.reset_app_hints.message", + value: "This will reset all app hints to their default visible state.", + comment: "" + )) + } + } - // MARK: Feedback and support - - AboutSectionView( - heading: NSLocalizedString("i18n.app.about.feedback.title", value: "Feedback and support", comment: "") - ) { - AboutRowView( - icon: "star.fill", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.feedback.rate_scribe", value: "Rate Scribe Conjugate", comment: ""), - hasExternalLink: true - ) { rateApp() } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "ladybug", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.feedback.bug_report", value: "Report a bug", comment: ""), - hasExternalLink: true - ) { openURL("https://github.com/scribe-org/Scribe-Conjugate/issues") } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "envelope", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.feedback.send_email", value: "Send us an email", comment: ""), - hasExternalLink: true - ) { sendEmail() } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "bookmark.fill", - isCustomImage: false, - title: String( - format: NSLocalizedString("i18n.app.about.feedback.version", value: "Version %@", comment: ""), - appVersion - ), - hasExternalLink: true - ) { openURL("https://github.com/scribe-org/Scribe-Conjugate/releases") } - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "lightbulb", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.feedback.reset_app_hints", value: "Reset app hints", comment: ""), - isReset: true - ) { showAppHintsConfirmation = true } - } + // MARK: - Actions - // MARK: Legal - - AboutSectionView( - heading: NSLocalizedString("i18n._global.legal", value: "Legal", comment: "") - ) { - AboutRowView( - icon: "lock.shield", - isCustomImage: false, - title: NSLocalizedString("i18n._global.privacy_policy", value: "Privacy policy", comment: ""), - hasNestedNavigation: true - ) {} - .overlay( - NavigationLink(destination: AboutInfoView(section: .privacyPolicy)) { Color.clear } - ) - - Divider().padding(.leading, 54) - - AboutRowView( - icon: "doc.text", - isCustomImage: false, - title: NSLocalizedString("i18n.app.about.legal.third_party", value: "Third-party licenses", comment: ""), - hasNestedNavigation: true - ) {} - .overlay( - NavigationLink(destination: AboutInfoView(section: .licenses)) { Color.clear } - ) - } + private func openURL(_ urlString: String, encoded: Bool = false) { + let target = + encoded + ? (urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString) + : urlString + guard let url = URL(string: target) else { return } + UIApplication.shared.open(url) + } - Spacer(minLength: 32) - } - .padding(.top, 8) - .animation(.easeInOut(duration: 0.2), value: tipCardVisible) - } - } - .navigationTitle(NSLocalizedString("i18n.app.about.title", value: "About", comment: "")) - .navigationBarTitleDisplayMode(.large) + private func rateApp() { + guard + let scene = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene + else { return } + SKStoreReviewController.requestReview(in: scene) } - .sheet(isPresented: $showShareSheet) { - ShareSheet(items: [conjugateShareURL()]) + + private func sendEmail() { + openURL("mailto:team@scri.be") } - .alert( - NSLocalizedString("i18n.app.about.feedback.reset_app_hints", value: "Reset app hints", comment: ""), - isPresented: $showAppHintsConfirmation - ) { - Button(NSLocalizedString("i18n._global.cancel", value: "Cancel", comment: ""), role: .cancel) {} - Button(NSLocalizedString("i18n._global.reset", value: "Reset", comment: "")) { - resetAppHints() - } - } message: { - Text(NSLocalizedString( - "i18n.app.about.feedback.reset_app_hints.message", - value: "This will reset all app hints to their default visible state.", - comment: "" - )) + + private func resetAppHints() { + UserDefaults.standard.set(true, forKey: "aboutTipCardState") + withAnimation { tipCardVisible = true } + } + + private func conjugateShareURL() -> URL { + URL(string: "https://scri.be/")! } - } - - // MARK: - Actions - - private func openURL(_ urlString: String, encoded: Bool = false) { - let target = encoded - ? (urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString) - : urlString - guard let url = URL(string: target) else { return } - UIApplication.shared.open(url) - } - - private func rateApp() { - guard let scene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else { return } - SKStoreReviewController.requestReview(in: scene) - } - - private func sendEmail() { - openURL("mailto:team@scri.be") - } - - private func resetAppHints() { - UserDefaults.standard.set(true, forKey: "aboutTipCardState") - withAnimation { tipCardVisible = true } - } - - private func conjugateShareURL() -> URL { - URL(string: "https://scri.be/")! - } } diff --git a/Conjugate/Views/Tabs/About/AboutTipCardView.swift b/Conjugate/Views/Tabs/About/AboutTipCardView.swift index 48845689..7f983bdb 100644 --- a/Conjugate/Views/Tabs/About/AboutTipCardView.swift +++ b/Conjugate/Views/Tabs/About/AboutTipCardView.swift @@ -7,47 +7,47 @@ import SwiftUI struct AboutTipCardView: View { - let infoText: String - @Binding var isVisible: Bool - var onDismiss: (() -> Void)? - - private let cardCornerRadius: CGFloat = 10 - - var body: some View { - ZStack { - RoundedRectangle(cornerRadius: cardCornerRadius) - .fill(Color("lightWhiteDarkBlack")) - HStack(spacing: 12) { - Image(systemName: "lightbulb.max") - .resizable() - .scaledToFit() - .frame(width: 28, height: 28) - .foregroundColor(Color("scribeCTA")) - .padding(.leading, 12) - - Text(infoText) - .font(.subheadline) - .foregroundColor(.primary) - - Spacer() - - Button { - withAnimation(.easeInOut(duration: 0.2)) { - isVisible = false - } - onDismiss?() - } label: { - Text("OK") - .foregroundColor(.white) - .frame(width: 40, height: 40) - .background(Color("scribeBlue")) - .cornerRadius(cardCornerRadius) + let infoText: String + @Binding var isVisible: Bool + var onDismiss: (() -> Void)? + + private let cardCornerRadius: CGFloat = 10 + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: cardCornerRadius) + .fill(Color("lightWhiteDarkBlack")) + HStack(spacing: 12) { + Image(systemName: "lightbulb.max") + .resizable() + .scaledToFit() + .frame(width: 28, height: 28) + .foregroundColor(Color("scribeCTA")) + .padding(.leading, 12) + + Text(infoText) + .font(.subheadline) + .foregroundColor(.primary) + + Spacer() + + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isVisible = false + } + onDismiss?() + } label: { + Text("OK") + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(Color("scribeBlue")) + .cornerRadius(cardCornerRadius) + } + .padding(.trailing, 12) + } } - .padding(.trailing, 12) - } + .frame(maxWidth: .infinity) + .frame(height: 70) + .shadow(color: Color("keyShadow").opacity(0.4), radius: 5, x: 0, y: 2) } - .frame(maxWidth: .infinity) - .frame(height: 70) - .shadow(color: Color("keyShadow").opacity(0.4), radius: 5, x: 0, y: 2) - } } diff --git a/Conjugate/Views/Tabs/About/ShareSheet.swift b/Conjugate/Views/Tabs/About/ShareSheet.swift index 906d06a2..8a85e162 100644 --- a/Conjugate/Views/Tabs/About/ShareSheet.swift +++ b/Conjugate/Views/Tabs/About/ShareSheet.swift @@ -8,11 +8,11 @@ import SwiftUI import UIKit struct ShareSheet: UIViewControllerRepresentable { - let items: [Any] + let items: [Any] - func makeUIViewController(context: Context) -> UIActivityViewController { - UIActivityViewController(activityItems: items, applicationActivities: nil) - } + func makeUIViewController(context: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: items, applicationActivities: nil) + } - func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} } From 5b602b4a7b7a3b4d361cccae376f4fe188e20730 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:59:22 +0530 Subject: [PATCH 6/8] fix: assign Bluesky image to all scale slots in asset catalog --- Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json b/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json index 17f6fd94..8a8552b5 100644 --- a/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json +++ b/Scribe/Assets.xcassets/MenuIcons/Bluesky.imageset/Contents.json @@ -6,10 +6,12 @@ "scale" : "1x" }, { + "filename" : "Image.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "Image.png", "idiom" : "universal", "scale" : "3x" } From ba5131bea952fc3d47ca0d769b055a3969a27dce Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:53:20 +0530 Subject: [PATCH 7/8] fix: address review comments on About tab subpages and Bluesky icon - Fix Bluesky icon not visible in dark mode by adding invertIconInDarkMode parameter to AboutRowView; applies colorInvert() when colorScheme == .dark, avoiding the need for a separate dark-mode asset - Fix subpage navigation headers (Third-party licenses, Wikimedia and Scribe, Privacy policy) appearing inside the view by changing navigationBarTitleDisplayMode from .inline to .large so titles render above the content in the nav bar - Replace tooltip text 'Tap a row to learn more or take action.' with the shared Scribe i18n key i18n.app.about.app_hint_tooltip, aligning with the string used in the Keyboard app: 'Here's where you can learn more about Scribe and its community.' --- Conjugate/Views/Tabs/About/AboutInfoView.swift | 2 +- Conjugate/Views/Tabs/About/AboutRowView.swift | 12 +++++++++++- Conjugate/Views/Tabs/About/AboutTab.swift | 7 ++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Conjugate/Views/Tabs/About/AboutInfoView.swift b/Conjugate/Views/Tabs/About/AboutInfoView.swift index 547373c2..5b6593af 100644 --- a/Conjugate/Views/Tabs/About/AboutInfoView.swift +++ b/Conjugate/Views/Tabs/About/AboutInfoView.swift @@ -81,7 +81,7 @@ struct AboutInfoView: View { } .background(Color("scribeAppBackground").ignoresSafeArea()) .navigationTitle(title) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) } } diff --git a/Conjugate/Views/Tabs/About/AboutRowView.swift b/Conjugate/Views/Tabs/About/AboutRowView.swift index b89d3c7a..4f7d69ad 100644 --- a/Conjugate/Views/Tabs/About/AboutRowView.swift +++ b/Conjugate/Views/Tabs/About/AboutRowView.swift @@ -18,8 +18,11 @@ struct AboutRowView: View { let isCustomImage: Bool let title: String var trailing: AboutRowTrailing = .none + var invertIconInDarkMode: Bool = false let action: () -> Void + @Environment(\.colorScheme) private var colorScheme + init( icon: String, isCustomImage: Bool, @@ -27,11 +30,13 @@ struct AboutRowView: View { hasExternalLink: Bool = false, hasNestedNavigation: Bool = false, isReset: Bool = false, + invertIconInDarkMode: Bool = false, action: @escaping () -> Void ) { self.icon = icon self.isCustomImage = isCustomImage self.title = title + self.invertIconInDarkMode = invertIconInDarkMode self.action = action if hasExternalLink { self.trailing = .externalLink @@ -71,9 +76,14 @@ struct AboutRowView: View { @ViewBuilder private var iconView: some View { if isCustomImage { - Image(icon) + let image = Image(icon) .resizable() .scaledToFit() + if invertIconInDarkMode && colorScheme == .dark { + image.colorInvert() + } else { + image + } } else { Image(systemName: icon) .resizable() diff --git a/Conjugate/Views/Tabs/About/AboutTab.swift b/Conjugate/Views/Tabs/About/AboutTab.swift index 07d67665..54b8f329 100644 --- a/Conjugate/Views/Tabs/About/AboutTab.swift +++ b/Conjugate/Views/Tabs/About/AboutTab.swift @@ -29,8 +29,8 @@ struct AboutTab: View { if tipCardVisible { AboutTipCardView( infoText: NSLocalizedString( - "i18n.app.about.tip_card", - value: "Tap a row to learn more or take action.", + "i18n.app.about.app_hint_tooltip", + value: "Here's where you can learn more about Scribe and its community.", comment: "" ), isVisible: $tipCardVisible, @@ -92,7 +92,8 @@ struct AboutTab: View { title: NSLocalizedString( "i18n.app.about.community.bluesky", value: "Follow us on Bluesky", comment: ""), - hasExternalLink: true + hasExternalLink: true, + invertIconInDarkMode: true ) { openURL("https://bsky.app/profile/scribe-org.bsky.social") } Divider().padding(.leading, 54) From 94e02bc5dfd727966ec600a25478759c05734eee Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:25:12 +0530 Subject: [PATCH 8/8] fix: register About tab subviews and ShareSheet in Conjugate target --- Scribe.xcodeproj/project.pbxproj | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index dd575d99..348ec524 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -786,6 +786,11 @@ F786BB3B2F1E8F70003F7505 /* Russian.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1671A60275A1E8700A7C118 /* Russian.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F7A17EBA2F6A8C180040B09B /* AppNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EB62F6A8BFE0040B09B /* AppNavigation.swift */; }; F7A17EBB2F6A8C1C0040B09B /* AboutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EAC2F6A8BFE0040B09B /* AboutTab.swift */; }; + FA001AAB2F9A000000000001 /* AboutTipCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA001AAA2F9A000000000001 /* AboutTipCardView.swift */; }; + FA001AAB2F9A000000000002 /* AboutSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA001AAA2F9A000000000002 /* AboutSectionView.swift */; }; + FA001AAB2F9A000000000003 /* AboutRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA001AAA2F9A000000000003 /* AboutRowView.swift */; }; + FA001AAB2F9A000000000004 /* AboutInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA001AAA2F9A000000000004 /* AboutInfoView.swift */; }; + FA001AAB2F9A000000000005 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA001AAA2F9A000000000005 /* ShareSheet.swift */; }; F7A17EBC2F6A8C200040B09B /* ConjugateTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EAE2F6A8BFE0040B09B /* ConjugateTab.swift */; }; F7A17EBD2F6A8C230040B09B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EB42F6A8BFE0040B09B /* ContentView.swift */; }; /* End PBXBuildFile section */ @@ -1163,6 +1168,11 @@ F786BB422F1E8F70003F7505 /* ConjugateApp-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "ConjugateApp-Info.plist"; path = "/Users/gauthammohanraj/Developer/Scribe-iOS/ConjugateApp-Info.plist"; sourceTree = ""; }; F7A17EAB2F6A8BFE0040B09B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContentView.swift; path = Conjugate/ContentView.swift; sourceTree = ""; }; F7A17EAC2F6A8BFE0040B09B /* AboutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTab.swift; sourceTree = ""; }; + FA001AAA2F9A000000000001 /* AboutTipCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTipCardView.swift; sourceTree = ""; }; + FA001AAA2F9A000000000002 /* AboutSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSectionView.swift; sourceTree = ""; }; + FA001AAA2F9A000000000003 /* AboutRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutRowView.swift; sourceTree = ""; }; + FA001AAA2F9A000000000004 /* AboutInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInfoView.swift; sourceTree = ""; }; + FA001AAA2F9A000000000005 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; F7A17EAE2F6A8BFE0040B09B /* ConjugateTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConjugateTab.swift; sourceTree = ""; }; F7A17EB02F6A8BFE0040B09B /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; F7A17EB22F6A8BFE0040B09B /* ConjugateTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConjugateTab.swift; sourceTree = ""; }; @@ -2108,6 +2118,11 @@ isa = PBXGroup; children = ( F7A17EAC2F6A8BFE0040B09B /* AboutTab.swift */, + FA001AAA2F9A000000000001 /* AboutTipCardView.swift */, + FA001AAA2F9A000000000002 /* AboutSectionView.swift */, + FA001AAA2F9A000000000003 /* AboutRowView.swift */, + FA001AAA2F9A000000000004 /* AboutInfoView.swift */, + FA001AAA2F9A000000000005 /* ShareSheet.swift */, ); path = About; sourceTree = ""; @@ -3616,6 +3631,11 @@ F7A17EBD2F6A8C230040B09B /* ContentView.swift in Sources */, F7A17EBC2F6A8C200040B09B /* ConjugateTab.swift in Sources */, F7A17EBB2F6A8C1C0040B09B /* AboutTab.swift in Sources */, + FA001AAB2F9A000000000001 /* AboutTipCardView.swift in Sources */, + FA001AAB2F9A000000000002 /* AboutSectionView.swift in Sources */, + FA001AAB2F9A000000000003 /* AboutRowView.swift in Sources */, + FA001AAB2F9A000000000004 /* AboutInfoView.swift in Sources */, + FA001AAB2F9A000000000005 /* ShareSheet.swift in Sources */, F7A17EBA2F6A8C180040B09B /* AppNavigation.swift in Sources */, F725CADE2F6A72BC00A8C950 /* ConjugateApp.swift in Sources */, F725CAE72F6A783400A8C950 /* SettingsTab.swift in Sources */,