Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 117 additions & 3 deletions Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,23 @@
}
}
},
"Copy email address" : {
"comment" : "Button that copies the feedback email address when Mail is unavailable.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "E-Mail-Adresse kopieren"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copy email address"
}
}
}
},
"Could not create the project. Please try again." : {
"localizations" : {
"de" : {
Expand Down Expand Up @@ -512,13 +529,19 @@
}
}
},
"Text row highlight opacity" : {
"comment" : "A heading for the opacity of the row highlight for text-imported projects on the Reading Mode settings page.",
"Feedback email body %@ %@ %@" : {
"comment" : "Prefilled feedback email body. The first placeholder is the app version and build, the second is the iOS version, and the third is the device model.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Deckkraft der Text-Reihenmarkierung"
"value" : "Hallo,\n\nich hätte folgenden Wunsch für LoopLine:\n\n[Bitte beschreibe deine Idee.]\n\n---\nApp-Version: %1$@\niOS-Version: %2$@\nGerät: %3$@"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hello,\n\nI have the following suggestion for LoopLine:\n\n[Please describe your idea.]\n\n---\nApp version: %1$@\niOS version: %2$@\nDevice: %3$@"
}
}
}
Expand Down Expand Up @@ -649,6 +672,17 @@
}
}
},
"LoopLine – Product Improvement" : {
"comment" : "Subject line for an email suggesting an improvement to LoopLine.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "machine_translated",
"value" : "LoopLine – Produktverbesserung"
}
}
}
},
"Low" : {
"comment" : "A label for the lowest value of a slider.",
"isCommentAutoGenerated" : true,
Expand All @@ -661,6 +695,23 @@
}
}
},
"Mail is unavailable" : {
"comment" : "Alert title shown when the device cannot compose email.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mail ist nicht verfügbar"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mail is unavailable"
}
}
}
},
"Markup" : {
"comment" : "A button that toggles markup on and off.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -1011,6 +1062,24 @@
}
}
},
"Privacy Policy" : {
"comment" : "A button that opens a privacy policy.",
"isCommentAutoGenerated" : true,
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Datenschutzrichtlinie"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Privacy Policy"
}
}
}
},
"Progress" : {
"comment" : "Label for the progress value in the project detail view.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -1367,6 +1436,23 @@
}
}
},
"Set up Mail to send feedback, or email us at %@." : {
"comment" : "Message shown when the device cannot compose email. The placeholder is the feedback email address.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Richte Mail ein, um Feedback zu senden, oder schreibe uns an %1$@."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Set up Mail to send feedback, or email us at %@."
}
}
}
},
"Sets this as the current row" : {
"comment" : "A hint that describes the action of tapping a reading row to set it as the current row.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -1451,6 +1537,23 @@
}
}
},
"Suggest an improvement" : {
"comment" : "Button that opens a prefilled email for suggesting an app improvement.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbesserung vorschlagen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Suggest an improvement"
}
}
}
},
"Text" : {
"comment" : "The text label for a text-based source.",
"isCommentAutoGenerated" : true,
Expand All @@ -1475,6 +1578,17 @@
}
}
},
"Text row highlight opacity" : {
"comment" : "A heading for the opacity of the row highlight for text-imported projects on the Reading Mode settings page.",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Deckkraft der Text-Reihenmarkierung"
}
}
}
},
"The imported image could not be found." : {
"comment" : "A description of the error that occurs when the image is missing.",
"isCommentAutoGenerated" : true,
Expand Down
6 changes: 6 additions & 0 deletions LoopLine/Models/FeedbackConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

enum FeedbackConfiguration {
static let privacyPolicyURL = URL(string: "https://dominikmoreira.github.io/LoopLine/")
static let feedbackEmailAddress = "dev@jodoko.de"
}
8 changes: 8 additions & 0 deletions LoopLine/ViewModels/SettingsViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import Observation
import SwiftData
import UIKit

@MainActor
@Observable
Expand Down Expand Up @@ -29,6 +30,13 @@ final class SettingsViewModel {
}
}

var feedbackMailBody: String {
String(
localized: "Feedback email body \(appVersion) \(UIDevice.current.systemVersion) \(UIDevice.current.model)",
comment: "Prefilled feedback email body. The first placeholder is the app version and build, the second is the iOS version, and the third is the device model."
)
}

private func save(_ modelContext: ModelContext) {
try? modelContext.save()
}
Expand Down
40 changes: 40 additions & 0 deletions LoopLine/Views/FeedbackMailComposer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import MessageUI
import SwiftUI

struct FeedbackMailComposer: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let recipient: String
let subject: String
let messageBody: String

func makeUIViewController(context: Context) -> MFMailComposeViewController {
let composer = MFMailComposeViewController()
composer.mailComposeDelegate = context.coordinator
composer.setToRecipients([recipient])
composer.setSubject(subject)
composer.setMessageBody(messageBody, isHTML: false)
return composer
}

func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator(isPresented: $isPresented)
}

final class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
private var isPresented: Binding<Bool>

init(isPresented: Binding<Bool>) {
self.isPresented = isPresented
}

func mailComposeController(
_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?
) {
isPresented.wrappedValue = false
}
}
}
68 changes: 68 additions & 0 deletions LoopLine/Views/SettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Observation
import MessageUI
import SwiftData
import SwiftUI
import UIKit

struct SettingsView: View {
@Environment(\.modelContext) private var modelContext
Expand Down Expand Up @@ -28,8 +30,11 @@ struct SettingsView: View {

private struct SettingsForm: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.openURL) private var openURL
@Bindable var settings: AppSettings
let viewModel: SettingsViewModel
@State private var isShowingMailComposer = false
@State private var isShowingMailUnavailableAlert = false

var body: some View {
ScrollView {
Expand All @@ -42,6 +47,25 @@ private struct SettingsForm: View {
.padding(.bottom, 32)
}
.background(LoopLineTheme.appBackground.ignoresSafeArea())
.sheet(isPresented: $isShowingMailComposer) {
FeedbackMailComposer(
isPresented: $isShowingMailComposer,
recipient: FeedbackConfiguration.feedbackEmailAddress,
subject: String(
localized: "LoopLine – Product Improvement",
comment: "Subject line for an email suggesting an improvement to LoopLine."
),
messageBody: viewModel.feedbackMailBody
)
}
.alert("Mail is unavailable", isPresented: $isShowingMailUnavailableAlert) {
Button("Copy email address") {
UIPasteboard.general.string = FeedbackConfiguration.feedbackEmailAddress
}
Button("OK", role: .cancel) {}
} message: {
Text("Set up Mail to send feedback, or email us at \(FeedbackConfiguration.feedbackEmailAddress).")
}
}

private var readingModeSection: some View {
Expand Down Expand Up @@ -114,6 +138,26 @@ private struct SettingsForm: View {
Text("Version \(viewModel.appVersion)")
.font(.subheadline)
.foregroundStyle(LoopLineTheme.secondaryText)

Divider()
.padding(.leading, 16)

SettingsNavigationRow(title: "Privacy Policy") {
if let privacyPolicyURL = FeedbackConfiguration.privacyPolicyURL {
openURL(privacyPolicyURL)
}
}

Divider()
.padding(.leading, 16)

SettingsNavigationRow(title: "Suggest an improvement") {
if MFMailComposeViewController.canSendMail() {
isShowingMailComposer = true
} else {
isShowingMailUnavailableAlert = true
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
Expand Down Expand Up @@ -147,6 +191,30 @@ private struct SettingsToggleRow: View {
}
}

private struct SettingsNavigationRow: View {
let title: LocalizedStringResource
let action: () -> Void

var body: some View {
Button(action: action) {
HStack(spacing: 12) {
Text(title)
.font(.body)
.foregroundStyle(LoopLineTheme.primaryText)

Spacer()

Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(LoopLineTheme.secondaryText)
}
.frame(maxWidth: .infinity, minHeight: 44)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}

#Preview {
let container = PreviewModelContainer.make()
container.mainContext.insert(AppSettings())
Expand Down
Loading
Loading