parent
d7ebcb23ba
commit
bd6340ce77
|
@ -104,7 +104,6 @@
|
|||
0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACF827C12E4800F85C4B /* CreditsView.swift */; };
|
||||
0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFA27C12E5300F85C4B /* VersionView.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 */; };
|
||||
0E7A8C0A2A1D410500780F4B /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -613,7 +611,6 @@
|
|||
0EB4042D27CA136200378B1A /* AddingTextField.swift */,
|
||||
0EB3412F27C7761A00483410 /* Binding+Extensions.swift */,
|
||||
0E9ED48027FD9BAE003B2316 /* CopySavingButton.swift */,
|
||||
0E7577D62816A3B200081CBE /* DestructiveButton.swift */,
|
||||
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */,
|
||||
0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */,
|
||||
0E2C171A27CB5A3A007E8488 /* GenericCreditsView.swift */,
|
||||
|
@ -1458,7 +1455,6 @@
|
|||
0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */,
|
||||
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
||||
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
||||
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
||||
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
||||
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.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 {
|
||||
onAdd?()
|
||||
isAdding = true
|
||||
}
|
||||
}
|
||||
|
||||
private func doCommit() {
|
||||
func doCommit() {
|
||||
withAnimation {
|
||||
onCommit?()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadFromOriginal(once: Bool) {
|
||||
// MARK: -
|
||||
|
||||
private extension CopySavingButton {
|
||||
func loadFromOriginal(once: Bool) {
|
||||
guard !once || !isLoaded else {
|
||||
return
|
||||
}
|
||||
|
@ -64,7 +72,7 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
|
|||
isLoaded = true
|
||||
}
|
||||
|
||||
private func saveToOriginal() {
|
||||
func saveToOriginal() {
|
||||
if copy != original {
|
||||
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 var addedText: Binding<String> {
|
||||
.init {
|
||||
editedTextStrings[addedUUID] ?? ""
|
||||
} set: {
|
||||
editedTextStrings[addedUUID] = $0
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
debugChanges()
|
||||
return Group {
|
||||
|
@ -90,8 +82,20 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
|||
}
|
||||
}.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)
|
||||
|
||||
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(
|
||||
onAdd: {
|
||||
addedText.wrappedValue = ""
|
||||
|
@ -120,10 +124,8 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
extension EditableTextList {
|
||||
private func remapElements(_ newElements: [String]) {
|
||||
private extension EditableTextList {
|
||||
func remapElements(_ newElements: [String]) {
|
||||
var oldIdentifiableElements = identifiableElements
|
||||
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: {
|
||||
$0.string == addedText.wrappedValue
|
||||
}) else {
|
||||
|
@ -159,16 +174,7 @@ extension EditableTextList {
|
|||
commit()
|
||||
}
|
||||
|
||||
private func binding(toEditedElement element: IdentifiableString) -> 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>) {
|
||||
func replaceElement(at id: UUID, with editedText: Binding<String>) {
|
||||
// print(">>> \(identifiableElements[id].string) -> \(editedText.wrappedValue)")
|
||||
guard let i = identifiableElements.firstIndex(where: {
|
||||
$0.id == id
|
||||
|
@ -188,21 +194,21 @@ extension EditableTextList {
|
|||
commit()
|
||||
}
|
||||
|
||||
private func onDelete(offsets: IndexSet) {
|
||||
func onDelete(offsets: IndexSet) {
|
||||
var mapped = mapping(identifiableElements)
|
||||
mapped.remove(atOffsets: offsets)
|
||||
identifiableElements = mapped
|
||||
commit()
|
||||
}
|
||||
|
||||
private func onMove(indexSet: IndexSet, to offset: Int) {
|
||||
func onMove(indexSet: IndexSet, to offset: Int) {
|
||||
var mapped = mapping(identifiableElements)
|
||||
mapped.move(fromOffsets: indexSet, toOffset: offset)
|
||||
identifiableElements = mapped
|
||||
commit()
|
||||
}
|
||||
|
||||
private func commit() {
|
||||
func commit() {
|
||||
// print(">>> identifiableElements = \(identifiableElements.map { "\($0.string) (\($0.id))" })")
|
||||
elements = identifiableElements.map(\.string)
|
||||
}
|
||||
|
|
|
@ -26,31 +26,6 @@
|
|||
import SwiftUI
|
||||
|
||||
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 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 {
|
||||
$0.name.lowercased() < $1.name.lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
private var sortedNotices: [Notice] {
|
||||
var sortedNotices: [Notice] {
|
||||
notices.sorted {
|
||||
$0.name.lowercased() < $1.name.lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
private var sortedLanguages: [String] {
|
||||
var sortedLanguages: [String] {
|
||||
translations.keys.sorted {
|
||||
$0.localizedAsCountryCode < $1.localizedAsCountryCode
|
||||
}
|
||||
}
|
||||
|
||||
private var licensesSection: some View {
|
||||
var licensesSection: some View {
|
||||
Section(
|
||||
header: licensesHeader.map(Text.init)
|
||||
) {
|
||||
|
@ -118,7 +147,7 @@ struct GenericCreditsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var noticesSection: some View {
|
||||
var noticesSection: some View {
|
||||
Section(
|
||||
header: noticesHeader.map(Text.init)
|
||||
) {
|
||||
|
@ -128,7 +157,7 @@ struct GenericCreditsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var translationsSection: some View {
|
||||
var translationsSection: some View {
|
||||
Section(
|
||||
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 {
|
||||
Text(content.noticeString)
|
||||
.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 {
|
||||
var localizedAsCountryCode: String {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
case .active:
|
||||
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
|
||||
|
||||
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 {
|
||||
let data: Data
|
||||
|
||||
|
@ -80,3 +68,17 @@ struct MailComposerView: UIViewControllerRepresentable {
|
|||
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 {
|
||||
Section {
|
||||
ForEach(values, id: \.self) { value in
|
||||
|
|
|
@ -157,7 +157,7 @@ private extension OrganizerView.ProfileContextMenu {
|
|||
}
|
||||
|
||||
var deleteButton: some View {
|
||||
DestructiveButton {
|
||||
Button(role: .destructive) {
|
||||
withAnimation {
|
||||
profileManager.removeProfiles(withIds: [header.id])
|
||||
}
|
||||
|
|
|
@ -229,7 +229,7 @@ private extension ProfileView.MainMenu {
|
|||
}
|
||||
|
||||
var deleteProfileButton: some View {
|
||||
DestructiveButton {
|
||||
Button(role: .destructive) {
|
||||
alertType = .deleteProfile
|
||||
isAlertPresented = true
|
||||
} label: {
|
||||
|
|
Loading…
Reference in New Issue