mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-31 04:52:05 +00:00
Add TV settings tab (#921)
Includes: - Credits - Donations - Diagnostics - Version Had to: - Wrap tab view into a NavigationStack for full-screen navigation - Take out navigation titles of about subviews - Customize donations view layout with modifier - Fix credits and debug log to support scrolling Closes #914
This commit is contained in:
parent
f13a292b4b
commit
bad9e8b58e
@ -23,9 +23,6 @@
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#if !os(tvOS)
|
||||
import AppUIMain
|
||||
#endif
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
@ -85,15 +85,19 @@ extension AboutCoordinator {
|
||||
switch item {
|
||||
case .credits:
|
||||
CreditsView()
|
||||
.navigationTitle(Strings.Views.About.Credits.title)
|
||||
|
||||
case .diagnostics:
|
||||
DiagnosticsView(profileManager: profileManager, tunnel: tunnel)
|
||||
.navigationTitle(Strings.Views.Diagnostics.title)
|
||||
|
||||
case .donate:
|
||||
DonateView()
|
||||
DonateView(modifier: DonateViewModifier())
|
||||
.navigationTitle(Strings.Views.Donate.title)
|
||||
|
||||
case .links:
|
||||
LinksView()
|
||||
.navigationTitle(Strings.Views.About.Links.title)
|
||||
|
||||
default:
|
||||
Text(Strings.Global.Nouns.noSelection)
|
||||
|
@ -0,0 +1,39 @@
|
||||
//
|
||||
// DonateViewModifier+iOS.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DonateViewModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
List {
|
||||
content
|
||||
.themeSection(footer: Strings.Views.Donate.Sections.Main.footer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,42 @@
|
||||
//
|
||||
// DonateViewModifier+macOS.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DonateViewModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
Form {
|
||||
Section {
|
||||
Text(Strings.Views.Donate.Sections.Main.footer)
|
||||
}
|
||||
content
|
||||
}
|
||||
.themeForm()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -88,7 +88,6 @@ struct DiagnosticsView: View {
|
||||
tunnelLogs = await availableTunnelLogs()
|
||||
}
|
||||
.themeForm()
|
||||
.navigationTitle(Strings.Views.Diagnostics.title)
|
||||
.alert(Strings.Views.Diagnostics.ReportIssue.title, isPresented: $isPresentingUnableToEmail) {
|
||||
Button(Strings.Global.Nouns.ok, role: .cancel) {
|
||||
isPresentingUnableToEmail = false
|
||||
@ -105,11 +104,7 @@ private extension DiagnosticsView {
|
||||
navLink(Strings.Views.Diagnostics.Rows.app, to: .app(title: Strings.Views.Diagnostics.Rows.app))
|
||||
navLink(Strings.Views.Diagnostics.Rows.tunnel, to: .tunnel(title: Strings.Views.Diagnostics.Rows.tunnel, url: nil))
|
||||
|
||||
Toggle(Strings.Views.Diagnostics.Rows.includePrivateData, isOn: $logsPrivateData)
|
||||
.onChange(of: logsPrivateData) {
|
||||
PassepartoutConfiguration.shared.logsAddresses = $0
|
||||
PassepartoutConfiguration.shared.logsModules = $0
|
||||
}
|
||||
LogsPrivateDataToggle()
|
||||
}
|
||||
.themeSection(header: Strings.Views.Diagnostics.Sections.live)
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ struct DebugLogContentView: View {
|
||||
var body: some View {
|
||||
TextEditor(text: .constant(lines.joined(separator: "\n")))
|
||||
.font(.caption)
|
||||
.monospaced()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,6 @@ struct DebugLogContentView: View {
|
||||
List {
|
||||
ForEach(Array(lines.enumerated()), id: \.offset) {
|
||||
Text($0.element)
|
||||
.monospaced()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,21 +42,24 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
|
||||
|
||||
public var body: some View {
|
||||
debugChanges()
|
||||
return TabView {
|
||||
profileView
|
||||
.tabItem {
|
||||
Text(Strings.Global.Nouns.profile)
|
||||
}
|
||||
return NavigationStack {
|
||||
TabView {
|
||||
profileView
|
||||
.tabItem {
|
||||
Text(Strings.Global.Nouns.profile)
|
||||
}
|
||||
|
||||
// searchView
|
||||
// .tabItem {
|
||||
// ThemeImage(.search)
|
||||
// }
|
||||
// searchView
|
||||
// .tabItem {
|
||||
// ThemeImage(.search)
|
||||
// }
|
||||
|
||||
settingsView
|
||||
.tabItem {
|
||||
ThemeImage(.settings)
|
||||
}
|
||||
settingsView
|
||||
.tabItem {
|
||||
ThemeImage(.settings)
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: AppCoordinatorRoute.self, destination: pushDestination)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,10 +76,41 @@ private extension AppCoordinator {
|
||||
// }
|
||||
|
||||
var settingsView: some View {
|
||||
SettingsView()
|
||||
SettingsView(tunnel: tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppCoordinator {
|
||||
|
||||
@ViewBuilder
|
||||
func pushDestination(_ item: AppCoordinatorRoute?) -> some View {
|
||||
switch item {
|
||||
case .appLog:
|
||||
DebugLogView(withAppParameters: Constants.shared.log) {
|
||||
DebugLogContentView(lines: $0)
|
||||
}
|
||||
|
||||
case .credits:
|
||||
CreditsView()
|
||||
.resized(width: 0.5)
|
||||
.themeList()
|
||||
|
||||
case .donate:
|
||||
DonateView(modifier: DonateViewModifier())
|
||||
|
||||
case .tunnelLog:
|
||||
DebugLogView(withTunnel: tunnel, parameters: Constants.shared.log) {
|
||||
DebugLogContentView(lines: $0)
|
||||
}
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#Preview {
|
||||
AppCoordinator(
|
||||
profileManager: .mock,
|
||||
|
@ -0,0 +1,36 @@
|
||||
//
|
||||
// AppCoordinatorRoute.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AppCoordinatorRoute: Hashable {
|
||||
case appLog
|
||||
|
||||
case credits
|
||||
|
||||
case donate
|
||||
|
||||
case tunnelLog
|
||||
}
|
@ -100,24 +100,24 @@ private extension ActiveProfileView {
|
||||
func detailView(for profile: Profile) -> some View {
|
||||
VStack(spacing: 10) {
|
||||
if let connectionType = profile.localizedDescription(optionalStyle: .connectionType) {
|
||||
DetailRowView(title: Strings.Global.Nouns.protocol) {
|
||||
ListRowView(title: Strings.Global.Nouns.protocol) {
|
||||
Text(connectionType)
|
||||
}
|
||||
}
|
||||
if let pair = profile.selectedProvider {
|
||||
if let metadata = providerManager.provider(withId: pair.selection.id) {
|
||||
DetailRowView(title: Strings.Global.Nouns.provider) {
|
||||
ListRowView(title: Strings.Global.Nouns.provider) {
|
||||
Text(metadata.description)
|
||||
}
|
||||
}
|
||||
if let entity = pair.selection.entity {
|
||||
DetailRowView(title: Strings.Global.Nouns.country) {
|
||||
ListRowView(title: Strings.Global.Nouns.country) {
|
||||
ThemeCountryText(entity.header.countryCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let otherList = profile.localizedDescription(optionalStyle: .nonConnectionTypes) {
|
||||
DetailRowView(title: otherList) {
|
||||
ListRowView(title: otherList) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
@ -171,29 +171,11 @@ private extension ActiveProfileView {
|
||||
|
||||
private extension ActiveProfileView {
|
||||
func onProviderEntityRequired(_ profile: Profile) {
|
||||
// FIXME: #788, TV missing provider entity
|
||||
// FIXME: #913, TV missing provider entity
|
||||
}
|
||||
|
||||
func onPurchaseRequired(_ features: Set<AppFeature>) {
|
||||
// FIXME: #788, TV purchase required
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private struct DetailRowView<Content>: View where Content: View {
|
||||
let title: String
|
||||
|
||||
@ViewBuilder
|
||||
let content: Content
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.fontWeight(.light)
|
||||
Spacer()
|
||||
content
|
||||
}
|
||||
// FIXME: #913, TV purchase required
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,8 +52,7 @@ struct ProfileListView: View {
|
||||
List {
|
||||
ForEach(previews, id: \.id, content: toggleButton(for:))
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
.scrollClipDisabled()
|
||||
.themeList()
|
||||
.themeProgress(if: false, isEmpty: !profileManager.hasProfiles) {
|
||||
Text(Strings.Views.App.Folders.noProfiles)
|
||||
.themeEmptyMessage()
|
||||
@ -105,11 +104,11 @@ private extension ProfileListView {
|
||||
|
||||
private extension ProfileListView {
|
||||
func onProviderEntityRequired(_ profile: Profile) {
|
||||
// FIXME: #788, TV missing provider entity
|
||||
// FIXME: #913, TV missing provider entity
|
||||
}
|
||||
|
||||
func onPurchaseRequired(_ features: Set<AppFeature>) {
|
||||
// FIXME: #788, TV purchase required
|
||||
// FIXME: #913, TV purchase required
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,45 @@
|
||||
//
|
||||
// DebugLogContentView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DebugLogContentView: View {
|
||||
let lines: [String]
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(Array(lines.enumerated()), id: \.offset) {
|
||||
Button($0.element) {
|
||||
//
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
//
|
||||
// DonateViewModifier.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DonateViewModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
VStack {
|
||||
Text(Strings.Views.Donate.Sections.Main.footer)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.bottom)
|
||||
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns) {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 150)
|
||||
}
|
||||
}
|
||||
|
||||
private extension DonateViewModifier {
|
||||
var columns: [GridItem] {
|
||||
[GridItem(.adaptive(minimum: 500))]
|
||||
}
|
||||
}
|
@ -23,18 +23,61 @@
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
// FIXME: #788, UI for TV
|
||||
import UILibrary
|
||||
|
||||
struct SettingsView: View {
|
||||
let tunnel: ExtendedTunnel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Settings")
|
||||
}
|
||||
listView
|
||||
.resized(width: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
private extension SettingsView {
|
||||
var listView: some View {
|
||||
List {
|
||||
creditsSection
|
||||
diagnosticsSection
|
||||
aboutSection
|
||||
}
|
||||
.themeList()
|
||||
}
|
||||
|
||||
var creditsSection: some View {
|
||||
Group {
|
||||
NavigationLink(Strings.Views.About.Credits.title, value: AppCoordinatorRoute.credits)
|
||||
NavigationLink(Strings.Views.Donate.title, value: AppCoordinatorRoute.donate)
|
||||
}
|
||||
.themeSection(header: Strings.Unlocalized.appName)
|
||||
}
|
||||
|
||||
var diagnosticsSection: some View {
|
||||
Group {
|
||||
NavigationLink(Strings.Views.Diagnostics.Rows.app, value: AppCoordinatorRoute.appLog)
|
||||
NavigationLink(Strings.Views.Diagnostics.Rows.tunnel, value: AppCoordinatorRoute.tunnelLog)
|
||||
LogsPrivateDataToggle()
|
||||
}
|
||||
.themeSection(header: Strings.Views.Diagnostics.title)
|
||||
}
|
||||
|
||||
var aboutSection: some View {
|
||||
Group {
|
||||
Text(Strings.Global.Nouns.version)
|
||||
.themeTrailingValue(BundleConfiguration.mainVersionString)
|
||||
}
|
||||
.themeSection(header: Strings.Views.About.title)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#Preview {
|
||||
SettingsView(tunnel: .mock)
|
||||
.themeNavigationStack()
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
//
|
||||
// ListRowView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListRowView<Content>: View where Content: View {
|
||||
let title: String
|
||||
|
||||
@ViewBuilder
|
||||
let content: Content
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.fontWeight(.light)
|
||||
Spacer()
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
@ -178,18 +178,15 @@ private extension GenericCreditsView {
|
||||
var translationsSection: some View {
|
||||
Section {
|
||||
ForEach(sortedLanguages, id: \.self) { code in
|
||||
HStack {
|
||||
Text(code.localizedAsLanguageCode ?? code)
|
||||
Spacer()
|
||||
credits.translations[code].map { authors in
|
||||
VStack(spacing: 4) {
|
||||
ForEach(authors, id: \.self) {
|
||||
Text($0)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
Button {
|
||||
//
|
||||
} label: {
|
||||
translationLabel(code)
|
||||
}
|
||||
#else
|
||||
translationLabel(code)
|
||||
#endif
|
||||
}
|
||||
} header: {
|
||||
translationsHeader.map(Text.init)
|
||||
@ -204,6 +201,21 @@ private extension GenericCreditsView {
|
||||
}
|
||||
.navigationTitle(content.name)
|
||||
}
|
||||
|
||||
func translationLabel(_ code: String) -> some View {
|
||||
HStack {
|
||||
Text(code.localizedAsLanguageCode ?? code)
|
||||
Spacer()
|
||||
credits.translations[code].map { authors in
|
||||
VStack(spacing: 4) {
|
||||
ForEach(authors, id: \.self) {
|
||||
Text($0)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
@ -50,6 +50,20 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
public func resized(width: CGFloat? = nil, height: CGFloat? = nil) -> some View {
|
||||
GeometryReader { geo in
|
||||
self
|
||||
.frame(
|
||||
width: width.map {
|
||||
$0 * geo.size.width
|
||||
},
|
||||
height: height.map {
|
||||
$0 * geo.size.height
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func setLater<T>(_ value: T?, millis: Int = 50, block: @escaping (T?) -> Void) {
|
||||
Task {
|
||||
block(nil)
|
||||
|
@ -662,10 +662,8 @@ public enum Strings {
|
||||
}
|
||||
public enum Sections {
|
||||
public enum Main {
|
||||
/// If you want to display gratitude for my work, here are a couple amounts you can donate instantly.
|
||||
///
|
||||
/// You will only be charged once per donation, and you can donate multiple times.
|
||||
public static let footer = Strings.tr("Localizable", "views.donate.sections.main.footer", fallback: "If you want to display gratitude for my work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times.")
|
||||
/// If you want to display gratitude for my work, here are a couple of amounts you can donate instantly. You will only be charged once per donation, and you can donate multiple times.
|
||||
public static let footer = Strings.tr("Localizable", "views.donate.sections.main.footer", fallback: "If you want to display gratitude for my work, here are a couple of amounts you can donate instantly. You will only be charged once per donation, and you can donate multiple times.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@
|
||||
"views.diagnostics.alerts.report_issue.email" = "The device is not configured to send e-mails.";
|
||||
|
||||
"views.donate.title" = "Make a donation";
|
||||
"views.donate.sections.main.footer" = "If you want to display gratitude for my work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times.";
|
||||
"views.donate.sections.main.footer" = "If you want to display gratitude for my work, here are a couple of amounts you can donate instantly. You will only be charged once per donation, and you can donate multiple times.";
|
||||
"views.donate.alerts.thank_you.message" = "This means a lot to me and I really hope you keep using and promoting this app.";
|
||||
|
||||
"views.migration.title" = "Migrate";
|
||||
|
@ -115,6 +115,15 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
public func themeList() -> some View {
|
||||
#if os(tvOS)
|
||||
listStyle(.grouped)
|
||||
.scrollClipDisabled()
|
||||
#else
|
||||
self
|
||||
#endif
|
||||
}
|
||||
|
||||
public func themeForm() -> some View {
|
||||
formStyle(.grouped)
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ public struct CreditsView: View {
|
||||
.localizedDescription
|
||||
}
|
||||
)
|
||||
.navigationTitle(Strings.Views.About.Credits.title)
|
||||
.themeForm()
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import CommonLibrary
|
||||
import CommonUtils
|
||||
import SwiftUI
|
||||
|
||||
public struct DonateView: View {
|
||||
public struct DonateView<Modifier>: View where Modifier: ViewModifier {
|
||||
|
||||
@EnvironmentObject
|
||||
private var iapManager: IAPManager
|
||||
@ -35,6 +35,8 @@ public struct DonateView: View {
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
private let modifier: Modifier
|
||||
|
||||
@State
|
||||
private var availableProducts: [InAppProduct] = []
|
||||
|
||||
@ -50,13 +52,15 @@ public struct DonateView: View {
|
||||
@StateObject
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
public init() {
|
||||
public init(modifier: Modifier) {
|
||||
self.modifier = modifier
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
donationsView
|
||||
productsRows
|
||||
.modifier(modifier)
|
||||
.themeProgress(if: isFetchingProducts)
|
||||
.navigationTitle(title)
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
.alert(
|
||||
title,
|
||||
isPresented: $isThankYouPresented,
|
||||
@ -75,27 +79,17 @@ private extension DonateView {
|
||||
Strings.Views.Donate.title
|
||||
}
|
||||
|
||||
var donationsView: some View {
|
||||
Form {
|
||||
#if os(macOS)
|
||||
Section {
|
||||
Text(Strings.Views.Donate.Sections.Main.footer)
|
||||
}
|
||||
#endif
|
||||
ForEach(availableProducts, id: \.productIdentifier) {
|
||||
PaywallProductView(
|
||||
iapManager: iapManager,
|
||||
style: .donation,
|
||||
product: $0,
|
||||
purchasingIdentifier: $purchasingIdentifier,
|
||||
onComplete: onComplete,
|
||||
onError: onError
|
||||
)
|
||||
}
|
||||
.themeSection(footer: Strings.Views.Donate.Sections.Main.footer)
|
||||
var productsRows: some View {
|
||||
ForEach(availableProducts, id: \.productIdentifier) {
|
||||
PaywallProductView(
|
||||
iapManager: iapManager,
|
||||
style: .donation,
|
||||
product: $0,
|
||||
purchasingIdentifier: $purchasingIdentifier,
|
||||
onComplete: onComplete,
|
||||
onError: onError
|
||||
)
|
||||
}
|
||||
.themeForm()
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
}
|
||||
|
||||
func thankYouActions() -> some View {
|
||||
@ -159,6 +153,6 @@ private extension DonateView {
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
DonateView()
|
||||
DonateView(modifier: EmptyModifier())
|
||||
.withMockEnvironment()
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ public struct LinksView: View {
|
||||
supportSection
|
||||
webSection
|
||||
}
|
||||
.navigationTitle(Strings.Views.About.Links.title)
|
||||
.themeForm()
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ public struct DebugLogView<Content>: View where Content: View {
|
||||
|
||||
public var body: some View {
|
||||
content(currentLines)
|
||||
.monospaced()
|
||||
.themeEmpty(if: currentLines.isEmpty, message: Strings.Global.Nouns.noContent)
|
||||
.toolbar(content: toolbarContent)
|
||||
.task {
|
||||
@ -50,7 +51,9 @@ private extension DebugLogView {
|
||||
|
||||
@ViewBuilder
|
||||
func toolbarContent() -> some View {
|
||||
#if !os(tvOS)
|
||||
copyButton
|
||||
#endif
|
||||
// if !currentLines.isEmpty {
|
||||
// shareButton
|
||||
// }
|
||||
|
@ -60,7 +60,7 @@ public struct PaywallProductView: View {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
if #available(iOS 17, macOS 14, *) {
|
||||
if #available(iOS 17, macOS 14, tvOS 17, *) {
|
||||
StoreKitProductView(
|
||||
style: style,
|
||||
product: product,
|
||||
|
@ -63,7 +63,10 @@ private extension ProductView {
|
||||
|
||||
@ViewBuilder
|
||||
func withPaywallStyle(_ paywallStyle: PaywallProductViewStyle) -> some View {
|
||||
#if !os(tvOS)
|
||||
#if os(tvOS)
|
||||
productViewStyle(.compact)
|
||||
.padding()
|
||||
#else
|
||||
switch paywallStyle {
|
||||
case .recurring:
|
||||
productViewStyle(.compact)
|
||||
@ -71,8 +74,6 @@ private extension ProductView {
|
||||
case .oneTime, .donation:
|
||||
productViewStyle(.compact)
|
||||
}
|
||||
#else
|
||||
productViewStyle(.compact)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
//
|
||||
// LogsPrivateDataToggle.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/23/24.
|
||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
public struct LogsPrivateDataToggle: View {
|
||||
|
||||
@AppStorage(AppPreference.logsPrivateData.key, store: .appGroup)
|
||||
private var logsPrivateData = false
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Toggle(Strings.Views.Diagnostics.Rows.includePrivateData, isOn: $logsPrivateData)
|
||||
.onChange(of: logsPrivateData) {
|
||||
PassepartoutConfiguration.shared.logsAddresses = $0
|
||||
PassepartoutConfiguration.shared.logsModules = $0
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user