parent
6f64edc95b
commit
c930225458
|
@ -97,7 +97,7 @@ struct Issue: Identifiable {
|
|||
.replacingOccurrences(of: "$osLine", with: osLine)
|
||||
.replacingOccurrences(of: "$deviceLine", with: deviceLine ?? "unknown")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
var flatString: String {
|
||||
let str = uuidString.replacingOccurrences(of: "-", with: "")
|
||||
|
|
|
@ -42,7 +42,7 @@ extension AboutView {
|
|||
Section {
|
||||
diagnosticsLink
|
||||
Text(Strings.Global.version)
|
||||
.withTrailingText(BundleConfiguration.mainVersionString)
|
||||
.themeTrailingValue(BundleConfiguration.mainVersionString)
|
||||
}
|
||||
}
|
||||
.navigationTitle(Strings.Global.settings)
|
||||
|
|
|
@ -124,11 +124,11 @@ private extension IPView.RouteView {
|
|||
VStack {
|
||||
route.destination.map {
|
||||
Text("Destination")
|
||||
.withTrailingText($0.rawValue)
|
||||
.themeTrailingValue($0.rawValue)
|
||||
}
|
||||
route.gateway.map {
|
||||
Text("Gateway")
|
||||
.withTrailingText($0.rawValue)
|
||||
.themeTrailingValue($0.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ private extension View {
|
|||
switch row {
|
||||
case .text(let caption, let value):
|
||||
Text(caption)
|
||||
.withTrailingText(value)
|
||||
.themeTrailingValue(value)
|
||||
|
||||
case .textList(let caption, let values):
|
||||
if !values.isEmpty {
|
||||
|
@ -109,7 +109,7 @@ private extension View {
|
|||
}
|
||||
} else {
|
||||
Text(caption)
|
||||
.withTrailingText(Strings.Global.empty)
|
||||
.themeTrailingValue(Strings.Global.empty)
|
||||
}
|
||||
|
||||
case .copiableText(let caption, let value, let multiline):
|
||||
|
|
|
@ -166,7 +166,7 @@ private extension ProviderContentModifier {
|
|||
guard let lastUpdate else {
|
||||
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 {
|
||||
let header: String?
|
||||
|
||||
|
|
|
@ -183,6 +183,10 @@ extension View {
|
|||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue