parent
6f64edc95b
commit
c930225458
|
@ -97,7 +97,7 @@ struct Issue: Identifiable {
|
||||||
.replacingOccurrences(of: "$osLine", with: osLine)
|
.replacingOccurrences(of: "$osLine", with: osLine)
|
||||||
.replacingOccurrences(of: "$deviceLine", with: deviceLine ?? "unknown")
|
.replacingOccurrences(of: "$deviceLine", with: deviceLine ?? "unknown")
|
||||||
.replacingOccurrences(of: "$providerName", with: providerName ?? "none")
|
.replacingOccurrences(of: "$providerName", with: providerName ?? "none")
|
||||||
.replacingOccurrences(of: "$providerLastUpdate", with: providerLastUpdate?.timestamp ?? "unknown")
|
.replacingOccurrences(of: "$providerLastUpdate", with: providerLastUpdate?.localizedDescription(style: .timestamp) ?? "unknown")
|
||||||
.replacingOccurrences(of: "$purchasedProducts", with: purchasedProducts.map(\.rawValue).description)
|
.replacingOccurrences(of: "$purchasedProducts", with: purchasedProducts.map(\.rawValue).description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,26 @@ extension TimeInterval: StyledLocalizableEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Date: StyledLocalizableEntity {
|
||||||
|
public enum Style {
|
||||||
|
case timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
public func localizedDescription(style: Style) -> String {
|
||||||
|
switch style {
|
||||||
|
case .timestamp:
|
||||||
|
return Self.timestampFormatter.string(from: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let timestampFormatter: DateFormatter = {
|
||||||
|
let fmt = DateFormatter()
|
||||||
|
fmt.dateStyle = .medium
|
||||||
|
fmt.timeStyle = .medium
|
||||||
|
return fmt
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
extension UUID {
|
extension UUID {
|
||||||
var flatString: String {
|
var flatString: String {
|
||||||
let str = uuidString.replacingOccurrences(of: "-", with: "")
|
let str = uuidString.replacingOccurrences(of: "-", with: "")
|
||||||
|
|
|
@ -42,7 +42,7 @@ extension AboutView {
|
||||||
Section {
|
Section {
|
||||||
diagnosticsLink
|
diagnosticsLink
|
||||||
Text(Strings.Global.version)
|
Text(Strings.Global.version)
|
||||||
.withTrailingText(BundleConfiguration.mainVersionString)
|
.themeTrailingValue(BundleConfiguration.mainVersionString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(Strings.Global.settings)
|
.navigationTitle(Strings.Global.settings)
|
||||||
|
|
|
@ -124,11 +124,11 @@ private extension IPView.RouteView {
|
||||||
VStack {
|
VStack {
|
||||||
route.destination.map {
|
route.destination.map {
|
||||||
Text("Destination")
|
Text("Destination")
|
||||||
.withTrailingText($0.rawValue)
|
.themeTrailingValue($0.rawValue)
|
||||||
}
|
}
|
||||||
route.gateway.map {
|
route.gateway.map {
|
||||||
Text("Gateway")
|
Text("Gateway")
|
||||||
.withTrailingText($0.rawValue)
|
.themeTrailingValue($0.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ private extension View {
|
||||||
switch row {
|
switch row {
|
||||||
case .text(let caption, let value):
|
case .text(let caption, let value):
|
||||||
Text(caption)
|
Text(caption)
|
||||||
.withTrailingText(value)
|
.themeTrailingValue(value)
|
||||||
|
|
||||||
case .textList(let caption, let values):
|
case .textList(let caption, let values):
|
||||||
if !values.isEmpty {
|
if !values.isEmpty {
|
||||||
|
@ -109,7 +109,7 @@ private extension View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(caption)
|
Text(caption)
|
||||||
.withTrailingText(Strings.Global.empty)
|
.themeTrailingValue(Strings.Global.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .copiableText(let caption, let value, let multiline):
|
case .copiableText(let caption, let value, let multiline):
|
||||||
|
|
|
@ -166,7 +166,7 @@ private extension ProviderContentModifier {
|
||||||
guard let lastUpdate else {
|
guard let lastUpdate else {
|
||||||
return providerManager.isLoading ? Strings.Providers.LastUpdated.loading : nil
|
return providerManager.isLoading ? Strings.Providers.LastUpdated.loading : nil
|
||||||
}
|
}
|
||||||
return Strings.Providers.lastUpdated(lastUpdate.timestamp)
|
return Strings.Providers.lastUpdated(lastUpdate.localizedDescription(style: .timestamp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -242,6 +242,26 @@ struct ThemeAnimationModifier<T>: ViewModifier where T: Equatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ThemeTrailingValueModifier: ViewModifier {
|
||||||
|
let value: CustomStringConvertible?
|
||||||
|
|
||||||
|
let truncationMode: Text.TruncationMode
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
LabeledContent {
|
||||||
|
if let value {
|
||||||
|
Spacer()
|
||||||
|
Text(value.description)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(truncationMode)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ThemeSectionWithHeaderFooterModifier: ViewModifier {
|
struct ThemeSectionWithHeaderFooterModifier: ViewModifier {
|
||||||
let header: String?
|
let header: String?
|
||||||
|
|
||||||
|
|
|
@ -183,6 +183,10 @@ extension View {
|
||||||
modifier(ThemeAnimationModifier(value: value, category: category))
|
modifier(ThemeAnimationModifier(value: value, category: category))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func themeTrailingValue(_ value: CustomStringConvertible?, truncationMode: Text.TruncationMode = .tail) -> some View {
|
||||||
|
modifier(ThemeTrailingValueModifier(value: value, truncationMode: truncationMode))
|
||||||
|
}
|
||||||
|
|
||||||
public func themeSection(header: String? = nil, footer: String? = nil) -> some View {
|
public func themeSection(header: String? = nil, footer: String? = nil) -> some View {
|
||||||
modifier(ThemeSectionWithHeaderFooterModifier(header: header, footer: footer))
|
modifier(ThemeSectionWithHeaderFooterModifier(header: header, footer: footer))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
//
|
|
||||||
// Dates+Extensions.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 8/18/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
|
|
||||||
|
|
||||||
private let timestampFormatter: DateFormatter = {
|
|
||||||
let fmt = DateFormatter()
|
|
||||||
fmt.dateStyle = .medium
|
|
||||||
fmt.timeStyle = .medium
|
|
||||||
return fmt
|
|
||||||
}()
|
|
||||||
|
|
||||||
private let componentsFormatter: DateComponentsFormatter = {
|
|
||||||
let fmt = DateComponentsFormatter()
|
|
||||||
fmt.unitsStyle = .full
|
|
||||||
return fmt
|
|
||||||
}()
|
|
||||||
|
|
||||||
extension Date {
|
|
||||||
public var timestamp: String {
|
|
||||||
timestampFormatter.string(from: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimeInterval {
|
|
||||||
public var localizedDescription: String {
|
|
||||||
guard let str = componentsFormatter.string(from: self) else {
|
|
||||||
fatalError("Could not format a TimeInterval?")
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimeInterval {
|
|
||||||
public var dispatchTimeInterval: DispatchTimeInterval {
|
|
||||||
.nanoseconds(Int(self * Double(NSEC_PER_SEC)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -38,21 +38,6 @@ extension View {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withTrailingText(_ text: String?, truncationMode: Text.TruncationMode = .tail) -> some View {
|
|
||||||
LabeledContent {
|
|
||||||
if let text {
|
|
||||||
Spacer()
|
|
||||||
let trailing = Text(text)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(truncationMode)
|
|
||||||
trailing
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewModifier {
|
extension ViewModifier {
|
||||||
|
|
Loading…
Reference in New Issue