Add version view in About/Settings (#952)

Restored from v2. Refactor LogoView for reuse.
This commit is contained in:
Davide 2024-11-27 13:24:44 +01:00 committed by GitHub
parent 14aae4e02a
commit eac1e028b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 225 additions and 36 deletions

View File

@ -61,25 +61,44 @@ struct AboutCoordinator: View {
extension AboutCoordinator { extension AboutCoordinator {
func linkView(to route: AboutCoordinatorRoute) -> some View { func linkView(to route: AboutCoordinatorRoute) -> some View {
NavigationLink(title(for: route), value: route) NavigationLink(value: route) {
linkLabel(for: route)
}
} }
func title(for route: AboutCoordinatorRoute) -> String { func title(for route: AboutCoordinatorRoute) -> String {
switch route { switch route {
case .credits: case .credits:
return Strings.Views.About.Credits.title Strings.Views.About.Credits.title
case .diagnostics: case .diagnostics:
return Strings.Views.Diagnostics.title Strings.Views.Diagnostics.title
case .donate: case .donate:
return Strings.Views.Donate.title Strings.Views.Donate.title
case .links: case .links:
return Strings.Views.About.Links.title Strings.Views.About.Links.title
case .purchased: case .purchased:
return Strings.Views.Purchased.title Strings.Views.Purchased.title
case .version:
Strings.Views.About.title
}
}
@ViewBuilder
func linkLabel(for route: AboutCoordinatorRoute) -> some View {
switch route {
case .version:
Text(Strings.Global.Nouns.version)
#if os(iOS)
.themeTrailingValue(BundleConfiguration.mainVersionString)
#endif
default:
Text(title(for: route))
} }
} }
@ -106,6 +125,9 @@ extension AboutCoordinator {
PurchasedView() PurchasedView()
.navigationTitle(title(for: .purchased)) .navigationTitle(title(for: .purchased))
case .version:
VersionView()
default: default:
Text(Strings.Global.Nouns.noSelection) Text(Strings.Global.Nouns.noSelection)
.themeEmptyMessage() .themeEmptyMessage()

View File

@ -35,4 +35,6 @@ enum AboutCoordinatorRoute: Hashable {
case links case links
case purchased case purchased
case version
} }

View File

@ -65,18 +65,17 @@ private extension AboutContentView {
List { List {
PreferencesGroup(profileManager: profileManager) PreferencesGroup(profileManager: profileManager)
Group { Group {
linkContent(.version)
linkContent(.links) linkContent(.links)
linkContent(.credits) linkContent(.credits)
if !isRestricted { if !isRestricted {
linkContent(.donate) linkContent(.donate)
} }
} }
.themeSection(header: Strings.Views.About.Sections.resources) .themeSection(header: Strings.Views.About.title)
Section { Section {
linkContent(.purchased) linkContent(.purchased)
linkContent(.diagnostics) linkContent(.diagnostics)
Text(Strings.Global.Nouns.version)
.themeTrailingValue(BundleConfiguration.mainVersionString)
} }
} }
.navigationTitle(Strings.Global.Nouns.settings) .navigationTitle(Strings.Global.Nouns.settings)

View File

@ -31,6 +31,9 @@ import SwiftUI
struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View where LinkContent: View, AboutDestination: View, LogDestination: View { struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View where LinkContent: View, AboutDestination: View, LogDestination: View {
@EnvironmentObject
private var theme: Theme
@Environment(\.dismiss) @Environment(\.dismiss)
private var dismiss private var dismiss
@ -66,8 +69,9 @@ struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View whe
} }
} }
} }
.background(navigationRoute == .version ? theme.primaryColor : nil)
.onLoad { .onLoad {
navigationRoute = .links navigationRoute = .version
} }
} }
} }
@ -76,6 +80,7 @@ private extension AboutContentView {
var listView: some View { var listView: some View {
List(selection: $navigationRoute) { List(selection: $navigationRoute) {
Section { Section {
linkContent(.version)
linkContent(.links) linkContent(.links)
linkContent(.credits) linkContent(.credits)
if !isRestricted { if !isRestricted {

View File

@ -80,7 +80,7 @@ struct ProfileView: View, Routable, TunnelInstallationProviding {
} }
} }
.ignoresSafeArea(edges: .horizontal) .ignoresSafeArea(edges: .horizontal)
.background(theme.primaryColor.opacity(0.6).gradient) .background(theme.primaryColor)
.themeAnimation(on: showsSidePanel, category: .profiles) .themeAnimation(on: showsSidePanel, category: .profiles)
.defaultFocus($focusedField, .switchProfile) .defaultFocus($focusedField, .switchProfile)
.onChange(of: tunnel.status, onTunnelStatus) .onChange(of: tunnel.status, onTunnelStatus)

View File

@ -37,17 +37,17 @@ enum Detail {
case other case other
case purchased case purchased
case version
} }
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject
private var theme: Theme
let tunnel: ExtendedTunnel let tunnel: ExtendedTunnel
@Namespace
private var masterScope
@Namespace
private var detailScope
@FocusState @FocusState
private var focus: Detail? private var focus: Detail?
@ -63,6 +63,7 @@ struct SettingsView: View {
DetailView(detail: detail) DetailView(detail: detail)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.background(theme.primaryColor)
.onChange(of: focus) { .onChange(of: focus) {
guard focus != nil else { guard focus != nil else {
return return
@ -84,12 +85,18 @@ private extension SettingsView {
var creditsSection: some View { var creditsSection: some View {
Group { Group {
Button {
} label: {
Text(Strings.Global.Nouns.version)
.themeTrailingValue(BundleConfiguration.mainVersionString)
}
.focused($focus, equals: .version)
Button(Strings.Views.About.Credits.title) {} Button(Strings.Views.About.Credits.title) {}
.focused($focus, equals: .credits) .focused($focus, equals: .credits)
Button(Strings.Views.Donate.title) {} Button(Strings.Views.Donate.title) {}
.focused($focus, equals: .donate) .focused($focus, equals: .donate)
} }
.themeSection(header: Strings.Unlocalized.appName) .themeSection(header: Strings.Views.About.title)
} }
var diagnosticsSection: some View { var diagnosticsSection: some View {
@ -105,10 +112,8 @@ private extension SettingsView {
Group { Group {
Button(Strings.Views.Purchased.title) {} Button(Strings.Views.Purchased.title) {}
.focused($focus, equals: .purchased) .focused($focus, equals: .purchased)
Text(Strings.Global.Nouns.version)
.themeTrailingValue(BundleConfiguration.mainVersionString)
} }
.themeSection(header: Strings.Views.About.title) .themeSection()
} }
} }
@ -128,6 +133,9 @@ private struct DetailView: View {
PurchasedView() PurchasedView()
.themeList() .themeList()
case .version:
VersionView()
default: default:
VStack {} VStack {}
} }

View File

@ -40,8 +40,6 @@ public struct Credits: Decodable {
public let message: String public let message: String
} }
public let author: String
public let licenses: [License] public let licenses: [License]
public let notices: [Notice] public let notices: [Notice]

View File

@ -98,6 +98,8 @@ extension Strings {
public static let appleTV = "Apple TV" public static let appleTV = "Apple TV"
public static let authorName = "Davide De Rosa (keeshux)"
public static let ca = "CA" public static let ca = "CA"
public static let dns = "DNS" public static let dns = "DNS"

View File

@ -568,10 +568,6 @@ public enum Strings {
public static let web = Strings.tr("Localizable", "views.about.links.sections.web", fallback: "Web") public static let web = Strings.tr("Localizable", "views.about.links.sections.web", fallback: "Web")
} }
} }
public enum Sections {
/// Resources
public static let resources = Strings.tr("Localizable", "views.about.sections.resources", fallback: "Resources")
}
} }
public enum App { public enum App {
public enum Errors { public enum Errors {
@ -877,6 +873,14 @@ public enum Strings {
} }
} }
} }
public enum Version {
/// %@ is a project maintained by %@.
///
/// Source code is publicly available on GitHub under the GPLv3, you can find links in the home page.
public static func extra(_ p1: Any, _ p2: Any) -> String {
return Strings.tr("Localizable", "views.version.extra", String(describing: p1), String(describing: p2), fallback: "%@ is a project maintained by %@.\n\nSource code is publicly available on GitHub under the GPLv3, you can find links in the home page.")
}
}
public enum Vpn { public enum Vpn {
/// No servers /// No servers
public static let noServers = Strings.tr("Localizable", "views.vpn.no_servers", fallback: "No servers") public static let noServers = Strings.tr("Localizable", "views.vpn.no_servers", fallback: "No servers")

View File

@ -1,5 +1,4 @@
{ {
"author": "Davide De Rosa",
"licenses": [ "licenses": [
{ {
"name": "GenericJSON", "name": "GenericJSON",

View File

@ -26,7 +26,6 @@
// MARK: Views // MARK: Views
"views.about.title" = "About"; "views.about.title" = "About";
"views.about.sections.resources" = "Resources";
"views.about.links.title" = "Links"; "views.about.links.title" = "Links";
"views.about.links.sections.support" = "Support"; "views.about.links.sections.support" = "Support";
"views.about.links.sections.web" = "Web"; "views.about.links.sections.web" = "Web";
@ -129,6 +128,8 @@
"views.ui.purchase_required.purchase.help" = "Purchase required"; "views.ui.purchase_required.purchase.help" = "Purchase required";
"views.ui.purchase_required.restricted.help" = "Feature is restricted"; "views.ui.purchase_required.restricted.help" = "Feature is restricted";
"views.version.extra" = "%@ is a project maintained by %@.\n\nSource code is publicly available on GitHub under the GPLv3, you can find links in the home page.";
"views.vpn.category.any" = "All categories"; "views.vpn.category.any" = "All categories";
"views.vpn.preset" = "Preset"; "views.vpn.preset" = "Preset";
"views.vpn.no_servers" = "No servers"; "views.vpn.no_servers" = "No servers";

View File

@ -51,7 +51,11 @@ extension View {
} }
public func themeLockScreen() -> some View { public func themeLockScreen() -> some View {
modifier(ThemeLockScreenModifier(lockedContent: LogoView.init)) modifier(ThemeLockScreenModifier {
FullScreenView {
LogoImage()
}
})
} }
} }

View File

@ -31,6 +31,12 @@ extension Theme {
public convenience init() { public convenience init() {
self.init(dummy: Void()) self.init(dummy: Void())
} }
// public var primaryGradient: AnyGradient {
// primaryColor
// .opacity(0.6)
// .gradient
// }
} }
// MARK: - Shortcuts // MARK: - Shortcuts

View File

@ -0,0 +1,90 @@
//
// VersionView.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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 VersionView<Icon>: View where Icon: View {
@EnvironmentObject
private var theme: Theme
private let icon: () -> Icon
public init(icon: @escaping () -> Icon) {
self.icon = icon
}
public var body: some View {
ScrollView {
icon()
.padding(.top)
Spacer()
Text(title)
.font(.largeTitle)
Spacer()
Text(subtitle)
VStack {
Text(message)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.multilineTextAlignment(.center)
.padding()
}
}
.background(theme.primaryColor)
.foregroundStyle(.white)
}
}
extension VersionView where Icon == LogoImage {
public init() {
icon = {
LogoImage()
}
}
}
private extension VersionView {
var title: String {
Strings.Unlocalized.appName
}
var subtitle: String {
BundleConfiguration.mainVersionString
}
var message: String {
Strings.Views.Version.extra(Strings.Unlocalized.appName, Strings.Unlocalized.authorName)
}
}
#Preview {
VersionView {
ThemeImage(.cloudOn)
.font(.largeTitle)
}
.withMockEnvironment()
}

View File

@ -1,5 +1,5 @@
// //
// LogoView.swift // FullScreenView.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 3/20/23. // Created by Davide De Rosa on 3/20/23.
@ -25,22 +25,32 @@
import SwiftUI import SwiftUI
struct LogoView: View { public struct FullScreenView<Icon>: View where Icon: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme
var body: some View { @ViewBuilder
private let icon: () -> Icon
public init(icon: @escaping () -> Icon) {
self.icon = icon
}
public var body: some View {
ZStack { ZStack {
theme.primaryColor theme.primaryColor
.ignoresSafeArea() .ignoresSafeArea()
Image(theme.logoImage) icon()
} }
.ignoresSafeArea() .ignoresSafeArea()
} }
} }
#Preview { #Preview {
LogoView() FullScreenView {
.withMockEnvironment() ThemeImage(.cloudOn)
.foregroundStyle(.white)
}
.withMockEnvironment()
} }

View File

@ -0,0 +1,39 @@
//
// LogoImage.swift
// Passepartout
//
// Created by Davide De Rosa on 11/27/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
public struct LogoImage: View {
@EnvironmentObject
private var theme: Theme
public init() {
}
public var body: some View {
Image(theme.logoImage)
}
}