Add version view in About/Settings (#952)
Restored from v2. Refactor LogoView for reuse.
This commit is contained in:
parent
14aae4e02a
commit
eac1e028b2
|
@ -61,25 +61,44 @@ struct AboutCoordinator: View {
|
|||
|
||||
extension AboutCoordinator {
|
||||
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 {
|
||||
switch route {
|
||||
case .credits:
|
||||
return Strings.Views.About.Credits.title
|
||||
Strings.Views.About.Credits.title
|
||||
|
||||
case .diagnostics:
|
||||
return Strings.Views.Diagnostics.title
|
||||
Strings.Views.Diagnostics.title
|
||||
|
||||
case .donate:
|
||||
return Strings.Views.Donate.title
|
||||
Strings.Views.Donate.title
|
||||
|
||||
case .links:
|
||||
return Strings.Views.About.Links.title
|
||||
Strings.Views.About.Links.title
|
||||
|
||||
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()
|
||||
.navigationTitle(title(for: .purchased))
|
||||
|
||||
case .version:
|
||||
VersionView()
|
||||
|
||||
default:
|
||||
Text(Strings.Global.Nouns.noSelection)
|
||||
.themeEmptyMessage()
|
||||
|
|
|
@ -35,4 +35,6 @@ enum AboutCoordinatorRoute: Hashable {
|
|||
case links
|
||||
|
||||
case purchased
|
||||
|
||||
case version
|
||||
}
|
||||
|
|
|
@ -65,18 +65,17 @@ private extension AboutContentView {
|
|||
List {
|
||||
PreferencesGroup(profileManager: profileManager)
|
||||
Group {
|
||||
linkContent(.version)
|
||||
linkContent(.links)
|
||||
linkContent(.credits)
|
||||
if !isRestricted {
|
||||
linkContent(.donate)
|
||||
}
|
||||
}
|
||||
.themeSection(header: Strings.Views.About.Sections.resources)
|
||||
.themeSection(header: Strings.Views.About.title)
|
||||
Section {
|
||||
linkContent(.purchased)
|
||||
linkContent(.diagnostics)
|
||||
Text(Strings.Global.Nouns.version)
|
||||
.themeTrailingValue(BundleConfiguration.mainVersionString)
|
||||
}
|
||||
}
|
||||
.navigationTitle(Strings.Global.Nouns.settings)
|
||||
|
|
|
@ -31,6 +31,9 @@ import SwiftUI
|
|||
|
||||
struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View where LinkContent: View, AboutDestination: View, LogDestination: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var theme: Theme
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
|
@ -66,8 +69,9 @@ struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View whe
|
|||
}
|
||||
}
|
||||
}
|
||||
.background(navigationRoute == .version ? theme.primaryColor : nil)
|
||||
.onLoad {
|
||||
navigationRoute = .links
|
||||
navigationRoute = .version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +80,7 @@ private extension AboutContentView {
|
|||
var listView: some View {
|
||||
List(selection: $navigationRoute) {
|
||||
Section {
|
||||
linkContent(.version)
|
||||
linkContent(.links)
|
||||
linkContent(.credits)
|
||||
if !isRestricted {
|
||||
|
|
|
@ -80,7 +80,7 @@ struct ProfileView: View, Routable, TunnelInstallationProviding {
|
|||
}
|
||||
}
|
||||
.ignoresSafeArea(edges: .horizontal)
|
||||
.background(theme.primaryColor.opacity(0.6).gradient)
|
||||
.background(theme.primaryColor)
|
||||
.themeAnimation(on: showsSidePanel, category: .profiles)
|
||||
.defaultFocus($focusedField, .switchProfile)
|
||||
.onChange(of: tunnel.status, onTunnelStatus)
|
||||
|
|
|
@ -37,17 +37,17 @@ enum Detail {
|
|||
case other
|
||||
|
||||
case purchased
|
||||
|
||||
case version
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var theme: Theme
|
||||
|
||||
let tunnel: ExtendedTunnel
|
||||
|
||||
@Namespace
|
||||
private var masterScope
|
||||
|
||||
@Namespace
|
||||
private var detailScope
|
||||
|
||||
@FocusState
|
||||
private var focus: Detail?
|
||||
|
||||
|
@ -63,6 +63,7 @@ struct SettingsView: View {
|
|||
DetailView(detail: detail)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(theme.primaryColor)
|
||||
.onChange(of: focus) {
|
||||
guard focus != nil else {
|
||||
return
|
||||
|
@ -84,12 +85,18 @@ private extension SettingsView {
|
|||
|
||||
var creditsSection: some View {
|
||||
Group {
|
||||
Button {
|
||||
} label: {
|
||||
Text(Strings.Global.Nouns.version)
|
||||
.themeTrailingValue(BundleConfiguration.mainVersionString)
|
||||
}
|
||||
.focused($focus, equals: .version)
|
||||
Button(Strings.Views.About.Credits.title) {}
|
||||
.focused($focus, equals: .credits)
|
||||
Button(Strings.Views.Donate.title) {}
|
||||
.focused($focus, equals: .donate)
|
||||
}
|
||||
.themeSection(header: Strings.Unlocalized.appName)
|
||||
.themeSection(header: Strings.Views.About.title)
|
||||
}
|
||||
|
||||
var diagnosticsSection: some View {
|
||||
|
@ -105,10 +112,8 @@ private extension SettingsView {
|
|||
Group {
|
||||
Button(Strings.Views.Purchased.title) {}
|
||||
.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()
|
||||
.themeList()
|
||||
|
||||
case .version:
|
||||
VersionView()
|
||||
|
||||
default:
|
||||
VStack {}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,6 @@ public struct Credits: Decodable {
|
|||
public let message: String
|
||||
}
|
||||
|
||||
public let author: String
|
||||
|
||||
public let licenses: [License]
|
||||
|
||||
public let notices: [Notice]
|
||||
|
|
|
@ -98,6 +98,8 @@ extension Strings {
|
|||
|
||||
public static let appleTV = "Apple TV"
|
||||
|
||||
public static let authorName = "Davide De Rosa (keeshux)"
|
||||
|
||||
public static let ca = "CA"
|
||||
|
||||
public static let dns = "DNS"
|
||||
|
|
|
@ -568,10 +568,6 @@ public enum Strings {
|
|||
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 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 {
|
||||
/// No servers
|
||||
public static let noServers = Strings.tr("Localizable", "views.vpn.no_servers", fallback: "No servers")
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"author": "Davide De Rosa",
|
||||
"licenses": [
|
||||
{
|
||||
"name": "GenericJSON",
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
// MARK: Views
|
||||
|
||||
"views.about.title" = "About";
|
||||
"views.about.sections.resources" = "Resources";
|
||||
"views.about.links.title" = "Links";
|
||||
"views.about.links.sections.support" = "Support";
|
||||
"views.about.links.sections.web" = "Web";
|
||||
|
@ -129,6 +128,8 @@
|
|||
"views.ui.purchase_required.purchase.help" = "Purchase required";
|
||||
"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.preset" = "Preset";
|
||||
"views.vpn.no_servers" = "No servers";
|
||||
|
|
|
@ -51,7 +51,11 @@ extension View {
|
|||
}
|
||||
|
||||
public func themeLockScreen() -> some View {
|
||||
modifier(ThemeLockScreenModifier(lockedContent: LogoView.init))
|
||||
modifier(ThemeLockScreenModifier {
|
||||
FullScreenView {
|
||||
LogoImage()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,12 @@ extension Theme {
|
|||
public convenience init() {
|
||||
self.init(dummy: Void())
|
||||
}
|
||||
|
||||
// public var primaryGradient: AnyGradient {
|
||||
// primaryColor
|
||||
// .opacity(0.6)
|
||||
// .gradient
|
||||
// }
|
||||
}
|
||||
|
||||
// MARK: - Shortcuts
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// LogoView.swift
|
||||
// FullScreenView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 3/20/23.
|
||||
|
@ -25,22 +25,32 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct LogoView: View {
|
||||
public struct FullScreenView<Icon>: View where Icon: View {
|
||||
|
||||
@EnvironmentObject
|
||||
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 {
|
||||
theme.primaryColor
|
||||
.ignoresSafeArea()
|
||||
Image(theme.logoImage)
|
||||
icon()
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LogoView()
|
||||
.withMockEnvironment()
|
||||
FullScreenView {
|
||||
ThemeImage(.cloudOn)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.withMockEnvironment()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue