parent
d7ebcb23ba
commit
bd6340ce77
|
@ -104,7 +104,6 @@
|
||||||
0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACF827C12E4800F85C4B /* CreditsView.swift */; };
|
0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACF827C12E4800F85C4B /* CreditsView.swift */; };
|
||||||
0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFA27C12E5300F85C4B /* VersionView.swift */; };
|
0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFA27C12E5300F85C4B /* VersionView.swift */; };
|
||||||
0E71ACFD27C1321A00F85C4B /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFC27C1321A00F85C4B /* ActivityView.swift */; };
|
0E71ACFD27C1321A00F85C4B /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFC27C1321A00F85C4B /* ActivityView.swift */; };
|
||||||
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577D62816A3B200081CBE /* DestructiveButton.swift */; };
|
|
||||||
0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577DE2817E22C00081CBE /* VPNToggle.swift */; };
|
0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577DE2817E22C00081CBE /* VPNToggle.swift */; };
|
||||||
0E7A8C0A2A1D410500780F4B /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */; };
|
0E7A8C0A2A1D410500780F4B /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */; };
|
||||||
0E7A8C0C2A1D4A6100780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0B2A1D4A6100780F4B /* PassepartoutLibrary */; };
|
0E7A8C0C2A1D4A6100780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0B2A1D4A6100780F4B /* PassepartoutLibrary */; };
|
||||||
|
@ -398,7 +397,6 @@
|
||||||
0E71ACF827C12E4800F85C4B /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = "<group>"; };
|
0E71ACF827C12E4800F85C4B /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = "<group>"; };
|
||||||
0E71ACFA27C12E5300F85C4B /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
|
0E71ACFA27C12E5300F85C4B /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
|
||||||
0E71ACFC27C1321A00F85C4B /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
|
0E71ACFC27C1321A00F85C4B /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
|
||||||
0E7577D62816A3B200081CBE /* DestructiveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveButton.swift; sourceTree = "<group>"; };
|
|
||||||
0E7577DE2817E22C00081CBE /* VPNToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggle.swift; sourceTree = "<group>"; };
|
0E7577DE2817E22C00081CBE /* VPNToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggle.swift; sourceTree = "<group>"; };
|
||||||
0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+OpenVPN.swift"; sourceTree = "<group>"; };
|
0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+OpenVPN.swift"; sourceTree = "<group>"; };
|
||||||
0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+Network.swift"; sourceTree = "<group>"; };
|
0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+Network.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -613,7 +611,6 @@
|
||||||
0EB4042D27CA136200378B1A /* AddingTextField.swift */,
|
0EB4042D27CA136200378B1A /* AddingTextField.swift */,
|
||||||
0EB3412F27C7761A00483410 /* Binding+Extensions.swift */,
|
0EB3412F27C7761A00483410 /* Binding+Extensions.swift */,
|
||||||
0E9ED48027FD9BAE003B2316 /* CopySavingButton.swift */,
|
0E9ED48027FD9BAE003B2316 /* CopySavingButton.swift */,
|
||||||
0E7577D62816A3B200081CBE /* DestructiveButton.swift */,
|
|
||||||
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */,
|
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */,
|
||||||
0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */,
|
0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */,
|
||||||
0E2C171A27CB5A3A007E8488 /* GenericCreditsView.swift */,
|
0E2C171A27CB5A3A007E8488 /* GenericCreditsView.swift */,
|
||||||
|
@ -1458,7 +1455,6 @@
|
||||||
0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */,
|
0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */,
|
||||||
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
||||||
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
||||||
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
|
||||||
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
||||||
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */,
|
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */,
|
||||||
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,
|
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,
|
||||||
|
|
|
@ -51,15 +51,19 @@ struct AddingTextField<Field: View, ActionLabel: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func doAdd() {
|
// MARK: -
|
||||||
|
|
||||||
|
private extension AddingTextField {
|
||||||
|
func doAdd() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
onAdd?()
|
onAdd?()
|
||||||
isAdding = true
|
isAdding = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func doCommit() {
|
func doCommit() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
onCommit?()
|
onCommit?()
|
||||||
isAdding = false
|
isAdding = false
|
||||||
|
|
|
@ -51,12 +51,20 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var canSave: Bool {
|
// MARK: -
|
||||||
|
|
||||||
|
private extension CopySavingButton {
|
||||||
|
var canSave: Bool {
|
||||||
isLoaded && (saveAnyway || copy != original)
|
isLoaded && (saveAnyway || copy != original)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadFromOriginal(once: Bool) {
|
// MARK: -
|
||||||
|
|
||||||
|
private extension CopySavingButton {
|
||||||
|
func loadFromOriginal(once: Bool) {
|
||||||
guard !once || !isLoaded else {
|
guard !once || !isLoaded else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -64,7 +72,7 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
|
||||||
isLoaded = true
|
isLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveToOriginal() {
|
func saveToOriginal() {
|
||||||
if copy != original {
|
if copy != original {
|
||||||
original = copy
|
original = copy
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
//
|
|
||||||
// DestructiveButton.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 4/25/22.
|
|
||||||
// Copyright (c) 2023 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 DestructiveButton<Label: View>: View {
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
let label: () -> Label
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(role: .destructive, action: action, label: label)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -64,14 +64,6 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
|
|
||||||
private let addedUUID = UUID()
|
private let addedUUID = UUID()
|
||||||
|
|
||||||
private var addedText: Binding<String> {
|
|
||||||
.init {
|
|
||||||
editedTextStrings[addedUUID] ?? ""
|
|
||||||
} set: {
|
|
||||||
editedTextStrings[addedUUID] = $0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Group {
|
return Group {
|
||||||
|
@ -90,8 +82,20 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
}
|
}
|
||||||
}.onChange(of: elements, perform: remapElements)
|
}.onChange(of: elements, perform: remapElements)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func existingRow(_ element: IdentifiableString) -> some View {
|
// MARK: -
|
||||||
|
|
||||||
|
private extension EditableTextList {
|
||||||
|
var addedText: Binding<String> {
|
||||||
|
.init {
|
||||||
|
editedTextStrings[addedUUID] ?? ""
|
||||||
|
} set: {
|
||||||
|
editedTextStrings[addedUUID] = $0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func existingRow(_ element: IdentifiableString) -> some View {
|
||||||
let editedText = binding(toEditedElement: element)
|
let editedText = binding(toEditedElement: element)
|
||||||
|
|
||||||
return textField(.init(isNewElement: false, text: editedText, onEditingChanged: {
|
return textField(.init(isNewElement: false, text: editedText, onEditingChanged: {
|
||||||
|
@ -104,7 +108,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var newRow: some View {
|
var newRow: some View {
|
||||||
AddingTextField(
|
AddingTextField(
|
||||||
onAdd: {
|
onAdd: {
|
||||||
addedText.wrappedValue = ""
|
addedText.wrappedValue = ""
|
||||||
|
@ -120,10 +124,8 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View model
|
private extension EditableTextList {
|
||||||
|
func remapElements(_ newElements: [String]) {
|
||||||
extension EditableTextList {
|
|
||||||
private func remapElements(_ newElements: [String]) {
|
|
||||||
var oldIdentifiableElements = identifiableElements
|
var oldIdentifiableElements = identifiableElements
|
||||||
var newIdentifiableElements: [IdentifiableString] = []
|
var newIdentifiableElements: [IdentifiableString] = []
|
||||||
|
|
||||||
|
@ -148,7 +150,20 @@ extension EditableTextList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addElement() {
|
func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
|
||||||
|
// print(">>> <-> \(element)")
|
||||||
|
.init {
|
||||||
|
editedTextStrings[element.id] ?? element.string
|
||||||
|
} set: {
|
||||||
|
editedTextStrings[element.id] = $0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
private extension EditableTextList {
|
||||||
|
func addElement() {
|
||||||
guard allowsDuplicates || !identifiableElements.contains(where: {
|
guard allowsDuplicates || !identifiableElements.contains(where: {
|
||||||
$0.string == addedText.wrappedValue
|
$0.string == addedText.wrappedValue
|
||||||
}) else {
|
}) else {
|
||||||
|
@ -159,16 +174,7 @@ extension EditableTextList {
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
|
func replaceElement(at id: UUID, with editedText: Binding<String>) {
|
||||||
// print(">>> <-> \(element)")
|
|
||||||
.init {
|
|
||||||
editedTextStrings[element.id] ?? element.string
|
|
||||||
} set: {
|
|
||||||
editedTextStrings[element.id] = $0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func replaceElement(at id: UUID, with editedText: Binding<String>) {
|
|
||||||
// print(">>> \(identifiableElements[id].string) -> \(editedText.wrappedValue)")
|
// print(">>> \(identifiableElements[id].string) -> \(editedText.wrappedValue)")
|
||||||
guard let i = identifiableElements.firstIndex(where: {
|
guard let i = identifiableElements.firstIndex(where: {
|
||||||
$0.id == id
|
$0.id == id
|
||||||
|
@ -188,21 +194,21 @@ extension EditableTextList {
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onDelete(offsets: IndexSet) {
|
func onDelete(offsets: IndexSet) {
|
||||||
var mapped = mapping(identifiableElements)
|
var mapped = mapping(identifiableElements)
|
||||||
mapped.remove(atOffsets: offsets)
|
mapped.remove(atOffsets: offsets)
|
||||||
identifiableElements = mapped
|
identifiableElements = mapped
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onMove(indexSet: IndexSet, to offset: Int) {
|
func onMove(indexSet: IndexSet, to offset: Int) {
|
||||||
var mapped = mapping(identifiableElements)
|
var mapped = mapping(identifiableElements)
|
||||||
mapped.move(fromOffsets: indexSet, toOffset: offset)
|
mapped.move(fromOffsets: indexSet, toOffset: offset)
|
||||||
identifiableElements = mapped
|
identifiableElements = mapped
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commit() {
|
func commit() {
|
||||||
// print(">>> identifiableElements = \(identifiableElements.map { "\($0.string) (\($0.id))" })")
|
// print(">>> identifiableElements = \(identifiableElements.map { "\($0.string) (\($0.id))" })")
|
||||||
elements = identifiableElements.map(\.string)
|
elements = identifiableElements.map(\.string)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,31 +26,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct GenericCreditsView: View {
|
struct GenericCreditsView: View {
|
||||||
struct License {
|
|
||||||
let name: String
|
|
||||||
|
|
||||||
let licenseName: String
|
|
||||||
|
|
||||||
let licenseURL: URL
|
|
||||||
|
|
||||||
init(_ name: String, _ licenseName: String, _ licenseURL: URL) {
|
|
||||||
self.name = name
|
|
||||||
self.licenseName = licenseName
|
|
||||||
self.licenseURL = licenseURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Notice {
|
|
||||||
let name: String
|
|
||||||
|
|
||||||
let noticeString: String
|
|
||||||
|
|
||||||
init(_ name: String, _ noticeString: String) {
|
|
||||||
self.name = name
|
|
||||||
self.noticeString = noticeString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var licensesHeader: String? = "Licenses"
|
var licensesHeader: String? = "Licenses"
|
||||||
|
|
||||||
var noticesHeader: String? = "Notices"
|
var noticesHeader: String? = "Notices"
|
||||||
|
@ -78,26 +53,80 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var sortedLicenses: [License] {
|
extension GenericCreditsView {
|
||||||
|
struct License {
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
let licenseName: String
|
||||||
|
|
||||||
|
let licenseURL: URL
|
||||||
|
|
||||||
|
init(_ name: String, _ licenseName: String, _ licenseURL: URL) {
|
||||||
|
self.name = name
|
||||||
|
self.licenseName = licenseName
|
||||||
|
self.licenseURL = licenseURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Notice {
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
let noticeString: String
|
||||||
|
|
||||||
|
init(_ name: String, _ noticeString: String) {
|
||||||
|
self.name = name
|
||||||
|
self.noticeString = noticeString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension GenericCreditsView {
|
||||||
|
struct LicenseView: View {
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
@Binding var content: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
content.map { unwrapped in
|
||||||
|
ScrollView {
|
||||||
|
Text(unwrapped)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if content == nil {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}.onAppear(perform: loadURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
private extension GenericCreditsView {
|
||||||
|
var sortedLicenses: [License] {
|
||||||
licenses.sorted {
|
licenses.sorted {
|
||||||
$0.name.lowercased() < $1.name.lowercased()
|
$0.name.lowercased() < $1.name.lowercased()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedNotices: [Notice] {
|
var sortedNotices: [Notice] {
|
||||||
notices.sorted {
|
notices.sorted {
|
||||||
$0.name.lowercased() < $1.name.lowercased()
|
$0.name.lowercased() < $1.name.lowercased()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedLanguages: [String] {
|
var sortedLanguages: [String] {
|
||||||
translations.keys.sorted {
|
translations.keys.sorted {
|
||||||
$0.localizedAsCountryCode < $1.localizedAsCountryCode
|
$0.localizedAsCountryCode < $1.localizedAsCountryCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var licensesSection: some View {
|
var licensesSection: some View {
|
||||||
Section(
|
Section(
|
||||||
header: licensesHeader.map(Text.init)
|
header: licensesHeader.map(Text.init)
|
||||||
) {
|
) {
|
||||||
|
@ -118,7 +147,7 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var noticesSection: some View {
|
var noticesSection: some View {
|
||||||
Section(
|
Section(
|
||||||
header: noticesHeader.map(Text.init)
|
header: noticesHeader.map(Text.init)
|
||||||
) {
|
) {
|
||||||
|
@ -128,7 +157,7 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var translationsSection: some View {
|
var translationsSection: some View {
|
||||||
Section(
|
Section(
|
||||||
header: translationsHeader.map(Text.init)
|
header: translationsHeader.map(Text.init)
|
||||||
) {
|
) {
|
||||||
|
@ -145,7 +174,7 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func noticeView(_ content: Notice) -> some View {
|
func noticeView(_ content: Notice) -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text(content.noticeString)
|
Text(content.noticeString)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
@ -155,46 +184,27 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GenericCreditsView {
|
|
||||||
struct LicenseView: View {
|
|
||||||
let url: URL
|
|
||||||
|
|
||||||
@Binding var content: String?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
content.map { unwrapped in
|
|
||||||
ScrollView {
|
|
||||||
Text(unwrapped)
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if content == nil {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}.onAppear(perform: loadURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadURL() {
|
|
||||||
guard content == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Task { @MainActor in
|
|
||||||
withAnimation {
|
|
||||||
do {
|
|
||||||
content = try String(contentsOf: url)
|
|
||||||
} catch {
|
|
||||||
content = AppError(error).localizedDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension String {
|
private extension String {
|
||||||
var localizedAsCountryCode: String {
|
var localizedAsCountryCode: String {
|
||||||
Locale.current.localizedString(forLanguageCode: self)?.capitalized ?? self
|
Locale.current.localizedString(forLanguageCode: self)?.capitalized ?? self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
private extension GenericCreditsView.LicenseView {
|
||||||
|
func loadURL() {
|
||||||
|
guard content == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task { @MainActor in
|
||||||
|
withAnimation {
|
||||||
|
do {
|
||||||
|
content = try String(contentsOf: url)
|
||||||
|
} catch {
|
||||||
|
content = AppError(error).localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -66,8 +66,31 @@ struct LockableView<Content: View, LockedContent: View>: View {
|
||||||
}
|
}
|
||||||
}.onChange(of: scenePhase, perform: onScenePhase)
|
}.onChange(of: scenePhase, perform: onScenePhase)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func onScenePhase(_ scenePhase: ScenePhase) {
|
// MARK: -
|
||||||
|
|
||||||
|
private final class Lock: ObservableObject {
|
||||||
|
enum State {
|
||||||
|
case none
|
||||||
|
|
||||||
|
case covered
|
||||||
|
|
||||||
|
case locked
|
||||||
|
}
|
||||||
|
|
||||||
|
static let shared = Lock()
|
||||||
|
|
||||||
|
@Published var state: State = .locked
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
private extension LockableView {
|
||||||
|
func onScenePhase(_ scenePhase: ScenePhase) {
|
||||||
switch scenePhase {
|
switch scenePhase {
|
||||||
case .active:
|
case .active:
|
||||||
unlockIfNeeded()
|
unlockIfNeeded()
|
||||||
|
@ -114,20 +137,3 @@ struct LockableView<Content: View, LockedContent: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class Lock: ObservableObject {
|
|
||||||
enum State {
|
|
||||||
case none
|
|
||||||
|
|
||||||
case covered
|
|
||||||
|
|
||||||
case locked
|
|
||||||
}
|
|
||||||
|
|
||||||
static let shared = Lock()
|
|
||||||
|
|
||||||
@Published var state: State = .locked
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,18 +27,6 @@ import MessageUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MailComposerView: UIViewControllerRepresentable {
|
struct MailComposerView: UIViewControllerRepresentable {
|
||||||
final class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
|
|
||||||
@Binding private var isPresented: Bool
|
|
||||||
|
|
||||||
init(_ view: MailComposerView) {
|
|
||||||
_isPresented = view._isPresented
|
|
||||||
}
|
|
||||||
|
|
||||||
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
|
||||||
isPresented = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Attachment {
|
struct Attachment {
|
||||||
let data: Data
|
let data: Data
|
||||||
|
|
||||||
|
@ -80,3 +68,17 @@ struct MailComposerView: UIViewControllerRepresentable {
|
||||||
Coordinator(self)
|
Coordinator(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MailComposerView {
|
||||||
|
final class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
|
||||||
|
@Binding private var isPresented: Bool
|
||||||
|
|
||||||
|
init(_ view: MailComposerView) {
|
||||||
|
_isPresented = view._isPresented
|
||||||
|
}
|
||||||
|
|
||||||
|
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,8 +49,12 @@ struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func pickerView() -> some View {
|
// MARK: -
|
||||||
|
|
||||||
|
private extension StyledPicker {
|
||||||
|
func pickerView() -> some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
ForEach(values, id: \.self) { value in
|
ForEach(values, id: \.self) { value in
|
||||||
|
|
|
@ -157,7 +157,7 @@ private extension OrganizerView.ProfileContextMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
var deleteButton: some View {
|
var deleteButton: some View {
|
||||||
DestructiveButton {
|
Button(role: .destructive) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
profileManager.removeProfiles(withIds: [header.id])
|
profileManager.removeProfiles(withIds: [header.id])
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,7 +229,7 @@ private extension ProfileView.MainMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
var deleteProfileButton: some View {
|
var deleteProfileButton: some View {
|
||||||
DestructiveButton {
|
Button(role: .destructive) {
|
||||||
alertType = .deleteProfile
|
alertType = .deleteProfile
|
||||||
isAlertPresented = true
|
isAlertPresented = true
|
||||||
} label: {
|
} label: {
|
||||||
|
|
Loading…
Reference in New Issue