diff --git a/CHANGELOG.md b/CHANGELOG.md index 036396cb..dd682ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Dot as a legal character in host profile title. [#22](https://github.com/keeshux/passepartout-ios/issues/22) +- Host profiles can now be renamed. [#24](https://github.com/keeshux/passepartout-ios/issues/24) + ### Fixed - Incorrect compression warnings when importing host configurations. [#20](https://github.com/keeshux/passepartout-ios/pull/20) diff --git a/Passepartout-iOS/Global/Macros.swift b/Passepartout-iOS/Global/Macros.swift index d173fe66..5ebc08fe 100644 --- a/Passepartout-iOS/Global/Macros.swift +++ b/Passepartout-iOS/Global/Macros.swift @@ -36,15 +36,16 @@ class Macros { } extension UIAlertController { - func addDefaultAction(_ title: String, handler: @escaping () -> Void) { + @discardableResult func addDefaultAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction { let action = UIAlertAction(title: title, style: .default) { (action) in handler() } addAction(action) preferredAction = action + return action } - func addCancelAction(_ title: String, handler: (() -> Void)? = nil) { + @discardableResult func addCancelAction(_ title: String, handler: (() -> Void)? = nil) -> UIAlertAction { let action = UIAlertAction(title: title, style: .cancel) { (action) in handler?() } @@ -52,20 +53,23 @@ extension UIAlertController { if actions.count == 1 { preferredAction = action } + return action } - func addAction(_ title: String, handler: @escaping () -> Void) { + @discardableResult func addAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction { let action = UIAlertAction(title: title, style: .default) { (action) in handler() } addAction(action) + return action } - func addDestructiveAction(_ title: String, handler: @escaping () -> Void) { + @discardableResult func addDestructiveAction(_ title: String, handler: @escaping () -> Void) -> UIAlertAction { let action = UIAlertAction(title: title, style: .destructive) { (action) in handler() } addAction(action) preferredAction = action + return action } } diff --git a/Passepartout-iOS/Global/Theme.swift b/Passepartout-iOS/Global/Theme.swift index 13fe5af8..f6dfb81b 100644 --- a/Passepartout-iOS/Global/Theme.swift +++ b/Passepartout-iOS/Global/Theme.swift @@ -125,6 +125,17 @@ extension UIButton { } } +extension UITextField { + func applyProfileId(_ theme: Theme) { + placeholder = L10n.Global.Host.TitleInput.placeholder + clearButtonMode = .always + keyboardType = .asciiCapable + returnKeyType = .done + autocapitalizationType = .none + autocorrectionType = .no + } +} + // XXX: status bar is broken extension MFMailComposeViewController { func apply(_ theme: Theme) { diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift index 49699eb2..bebb2427 100644 --- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift @@ -70,10 +70,6 @@ class OrganizerViewController: UITableViewController, TableModelHost { // MARK: UIViewController - deinit { - NotificationCenter.default.removeObserver(self) - } - override func awakeFromNib() { super.awakeFromNib() @@ -94,8 +90,6 @@ class OrganizerViewController: UITableViewController, TableModelHost { } service.delegate = self - - NotificationCenter.default.addObserver(self, selector: #selector(wizardDidCreate(notification:)), name: .WizardDidCreate, object: nil) } override func viewDidAppear(_ animated: Bool) { @@ -428,26 +422,9 @@ extension OrganizerViewController { // MARK: - extension OrganizerViewController: ConnectionServiceDelegate { - func connectionService(didDeactivate profile: ConnectionProfile) { - tableView.reloadData() - } - - func connectionService(didActivate profile: ConnectionProfile) { - tableView.reloadData() - } -} - -extension OrganizerViewController { - @objc private func wizardDidCreate(notification: Notification) { - guard let profile = notification.userInfo?[WizardCreationKey.profile] as? ConnectionProfile, - let credentials = notification.userInfo?[WizardCreationKey.credentials] as? Credentials else { - - fatalError("WizardDidCreate notification must post profile and credentials") - } - - service.addOrReplaceProfile(profile, credentials: credentials) + func connectionService(didAdd profile: ConnectionProfile) { TransientStore.shared.serialize() // add - + reloadModel() tableView.reloadData() @@ -467,4 +444,24 @@ extension OrganizerViewController { } perform(segue: StoryboardSegue.Organizer.selectProfileSegueIdentifier, sender: profile) } + + func connectionService(didRename oldProfile: ConnectionProfile, to newProfile: ConnectionProfile) { + TransientStore.shared.serialize() // rename + + reloadModel() + tableView.reloadData() + } + + func connectionService(didRemoveProfileWithKey key: ConnectionService.ProfileKey) { + reloadModel() + tableView.reloadData() + } + + func connectionService(didDeactivate profile: ConnectionProfile) { + tableView.reloadData() + } + + func connectionService(didActivate profile: ConnectionProfile) { + tableView.reloadData() + } } diff --git a/Passepartout-iOS/Scenes/Organizer/WizardHostViewController.swift b/Passepartout-iOS/Scenes/Organizer/WizardHostViewController.swift index 7c165371..804df87c 100644 --- a/Passepartout-iOS/Scenes/Organizer/WizardHostViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/WizardHostViewController.swift @@ -51,6 +51,7 @@ class WizardHostViewController: UITableViewController, TableModelHost { lazy var model: TableModel = { let model: TableModel = TableModel() model.add(.meta) + model.setFooter(L10n.Global.Host.TitleInput.message, for: .meta) if !existingHosts.isEmpty { model.add(.existing) model.setHeader(L10n.Wizards.Host.Sections.Existing.header, for: .existing) @@ -130,9 +131,10 @@ class WizardHostViewController: UITableViewController, TableModelHost { guard let profile = createdProfile else { fatalError("No profile created?") } + let service = TransientStore.shared.service if let url = parsedFile?.url { do { - let savedURL = try TransientStore.shared.service.save(configurationURL: url, for: profile) + let savedURL = try service.save(configurationURL: url, for: profile) log.debug("Associated .ovpn configuration file to profile '\(profile.id)': \(savedURL)") // can now delete imported file @@ -141,12 +143,8 @@ class WizardHostViewController: UITableViewController, TableModelHost { log.error("Could not associate .ovpn configuration file to profile: \(e)") } } - dismiss(animated: true) { - NotificationCenter.default.post(name: .WizardDidCreate, object: nil, userInfo: [ - WizardCreationKey.profile: profile, - WizardCreationKey.credentials: credentials - ]) + service.addOrReplaceProfile(profile, credentials: credentials) } } @@ -188,6 +186,10 @@ extension WizardHostViewController { return model.header(for: section) } + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return model.footer(for: section) + } + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return model.count(for: section) } @@ -199,9 +201,7 @@ extension WizardHostViewController { cell.caption = L10n.Wizards.Host.Cells.TitleInput.caption cell.captionWidth = 100.0 cell.allowedCharset = .filename - cell.field.placeholder = L10n.Wizards.Host.Cells.TitleInput.placeholder - cell.field.clearButtonMode = .always - cell.field.returnKeyType = .done + cell.field.applyProfileId(Theme.current) cell.delegate = self return cell diff --git a/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift b/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift index 715eada3..3aa2a3bd 100644 --- a/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift @@ -52,11 +52,9 @@ class WizardProviderViewController: UITableViewController { guard let profile = createdProfile else { fatalError("No profile created?") } + let service = TransientStore.shared.service dismiss(animated: true) { - NotificationCenter.default.post(name: .WizardDidCreate, object: nil, userInfo: [ - WizardCreationKey.profile: profile, - WizardCreationKey.credentials: credentials - ]) + service.addOrReplaceProfile(profile, credentials: credentials) } } diff --git a/Passepartout-iOS/Scenes/ServiceViewController.swift b/Passepartout-iOS/Scenes/ServiceViewController.swift index e3b27951..488e0f6f 100644 --- a/Passepartout-iOS/Scenes/ServiceViewController.swift +++ b/Passepartout-iOS/Scenes/ServiceViewController.swift @@ -35,9 +35,12 @@ class ServiceViewController: UIViewController, TableModelHost { @IBOutlet private weak var labelWelcome: UILabel! + @IBOutlet private weak var itemEdit: UIBarButtonItem! + var profile: ConnectionProfile? { didSet { title = profile?.id + navigationItem.rightBarButtonItem = (profile?.context == .host) ? itemEdit : nil reloadModel() updateViewsIfNeeded() } @@ -47,6 +50,8 @@ class ServiceViewController: UIViewController, TableModelHost { private lazy var vpn = GracefulVPN(service: service) + private weak var pendingRenameAction: UIAlertAction? + private var lastInfrastructureUpdate: Date? // MARK: Table @@ -179,7 +184,7 @@ class ServiceViewController: UIViewController, TableModelHost { viewWelcome?.isHidden = (profile != nil) } - @IBAction private func activate() { + private func activateProfile() { service.activateProfile(uncheckedProfile) TransientStore.shared.serialize() // activate @@ -189,6 +194,28 @@ class ServiceViewController: UIViewController, TableModelHost { vpn.disconnect(completionHandler: nil) } + @IBAction private func renameProfile() { + let alert = Macros.alert(L10n.Service.Alerts.Rename.title, L10n.Global.Host.TitleInput.message) + alert.addTextField { (field) in + field.text = self.profile?.id + field.applyProfileId(Theme.current) + field.delegate = self + } + pendingRenameAction = alert.addDefaultAction(L10n.Global.ok) { + guard let newId = alert.textFields?.first?.text else { + return + } + self.doRenameCurrentProfile(to: newId) + } + alert.addCancelAction(L10n.Global.cancel) + pendingRenameAction?.isEnabled = false + present(alert, animated: true, completion: nil) + } + + private func doRenameCurrentProfile(to newId: String) { + profile = service.renameProfile(uncheckedHostProfile, to: newId) + } + private func toggleVpnService(cell: ToggleTableViewCell) { if cell.isOn { guard !service.needsCredentials(for: uncheckedProfile) else { @@ -713,7 +740,7 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog private func handle(row: RowType, cell: UITableViewCell) -> Bool { switch row { case .useProfile: - activate() + activateProfile() case .reconnect: confirmVpnReconnection() @@ -931,6 +958,25 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog // MARK: - +extension ServiceViewController: UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard string.rangeOfCharacter(from: CharacterSet.filename.inverted) == nil else { + return false + } + if let text = textField.text { + let replacement = (text as NSString).replacingCharacters(in: range, with: string) + pendingRenameAction?.isEnabled = (replacement != uncheckedProfile.id) + } + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return true + } +} + +// MARK: - + extension ServiceViewController: TrustedNetworksModelDelegate { func trustedNetworksCouldDisconnect(_: TrustedNetworksModel) -> Bool { return (service.preferences.trustPolicy == .disconnect) && (vpn.status != .disconnected) diff --git a/Passepartout-iOS/en.lproj/Main.storyboard b/Passepartout-iOS/en.lproj/Main.storyboard index 393a3dfd..e4506154 100644 --- a/Passepartout-iOS/en.lproj/Main.storyboard +++ b/Passepartout-iOS/en.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -215,8 +215,15 @@ - + + + + + + + + diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 83d59e4d..558e911a 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -43,7 +43,6 @@ 0E89DFC5213DF7AE00741BA1 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFC4213DF7AE00741BA1 /* Preferences.swift */; }; 0E89DFC8213E8FC500741BA1 /* SessionProxy+Communication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */; }; 0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; }; - 0E89DFD0213F223400741BA1 /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCF213F223400741BA1 /* Wizard.swift */; }; 0E8D97E221388B52006FB4A0 /* InfrastructurePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */; }; 0E8D97E521389277006FB4A0 /* pia.json in Resources */ = {isa = PBXBuildFile; fileRef = 0E8D97E421389276006FB4A0 /* pia.json */; }; 0EA068F4218475F800C320AD /* ParsedFile+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */; }; @@ -166,7 +165,6 @@ 0E89DFC4213DF7AE00741BA1 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProxy+Communication.swift"; sourceTree = ""; }; 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardProviderViewController.swift; sourceTree = ""; }; - 0E89DFCF213F223400741BA1 /* Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wizard.swift; sourceTree = ""; }; 0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructurePreset.swift; sourceTree = ""; }; 0E8D97E421389276006FB4A0 /* pia.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pia.json; sourceTree = ""; }; 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsedFile+Alerts.swift"; sourceTree = ""; }; @@ -399,7 +397,6 @@ 0E2B494120FD16540094784C /* TransientStore.swift */, 0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */, 0EBE3A8F213C6F4000BFA2F5 /* TrustPolicy.swift */, - 0E89DFCF213F223400741BA1 /* Wizard.swift */, ); path = Model; sourceTree = ""; @@ -845,7 +842,6 @@ 0E05C5D520D1645F006EE732 /* SettingTableViewCell.swift in Sources */, 0EBE3A84213C6ADE00BFA2F5 /* InfrastructureFactory.swift in Sources */, 0E4FD7DE20D3E49A002221FF /* StandardVPNProvider.swift in Sources */, - 0E89DFD0213F223400741BA1 /* Wizard.swift in Sources */, 0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */, 0EBE3AA1213DC1A100BFA2F5 /* ConnectionService.swift in Sources */, 0E1D72B2213BFFCF00BA1586 /* ProviderPresetViewController.swift in Sources */, diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index 43cd719f..e53761bb 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -26,6 +26,8 @@ "global.ok" = "OK"; "global.cancel" = "Cancel"; "global.next" = "Next"; +"global.host.title_input.message" = "Legal characters are alphanumerics plus dash (-), underscore (_) and dot (.)."; +"global.host.title_input.placeholder" = "My Profile"; "reddit.title" = "Reddit"; "reddit.message" = "Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project."; @@ -49,7 +51,6 @@ "account.suggestion_footer.infrastructure.pia" = "Use your website credentials. Your username is usually numeric with a \"p\" prefix."; "wizards.host.cells.title_input.caption" = "Title"; -"wizards.host.cells.title_input.placeholder" = "My Profile"; "wizards.host.sections.existing.header" = "Existing profiles"; "wizards.host.alerts.existing.message" = "A host profile with the same title already exists. Replace it?"; @@ -105,6 +106,7 @@ "service.cells.debug_log.caption" = "Debug log"; "service.cells.report_issue.caption" = "Report connectivity issue"; +"service.alerts.rename.title" = "Rename profile"; "service.alerts.credentials_needed.message" = "You need to enter account credentials first."; "service.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?"; "service.alerts.trusted.no_network.message" = "You are not connected to any Wi-Fi network."; diff --git a/Passepartout/Sources/Model/ConnectionProfile.swift b/Passepartout/Sources/Model/ConnectionProfile.swift index c4978ea8..1ece6b5f 100644 --- a/Passepartout/Sources/Model/ConnectionProfile.swift +++ b/Passepartout/Sources/Model/ConnectionProfile.swift @@ -43,6 +43,8 @@ protocol ConnectionProfile: class, EndpointDataSource { var requiresCredentials: Bool { get } func generate(from configuration: TunnelKitProvider.Configuration, preferences: Preferences) throws -> TunnelKitProvider.Configuration + + func with(newId: String) -> ConnectionProfile } extension ConnectionProfile { @@ -70,4 +72,11 @@ extension ConnectionProfile { } try keychain.set(password: password, for: key, label: key) } + + func removePassword(in keychain: Keychain) { + guard let key = passwordKey else { + return + } + keychain.removePassword(for: key) + } } diff --git a/Passepartout/Sources/Model/ConnectionService+Configurations.swift b/Passepartout/Sources/Model/ConnectionService+Configurations.swift index 5d559f7c..2efed886 100644 --- a/Passepartout/Sources/Model/ConnectionService+Configurations.swift +++ b/Passepartout/Sources/Model/ConnectionService+Configurations.swift @@ -53,7 +53,7 @@ extension ConnectionService { return configurationURL(for: ProfileKey(profile)) } - private func targetConfigurationURL(for key: ProfileKey) -> URL { + func targetConfigurationURL(for key: ProfileKey) -> URL { return contextURL(key).appendingPathComponent(key.id).appendingPathExtension("ovpn") } diff --git a/Passepartout/Sources/Model/ConnectionService.swift b/Passepartout/Sources/Model/ConnectionService.swift index 345e0378..9e9b5bc8 100644 --- a/Passepartout/Sources/Model/ConnectionService.swift +++ b/Passepartout/Sources/Model/ConnectionService.swift @@ -31,6 +31,12 @@ import SwiftyBeaver private let log = SwiftyBeaver.self protocol ConnectionServiceDelegate: class { + func connectionService(didAdd profile: ConnectionProfile) + + func connectionService(didRename oldProfile: ConnectionProfile, to newProfile: ConnectionProfile) + + func connectionService(didRemoveProfileWithKey key: ConnectionService.ProfileKey) + func connectionService(didActivate profile: ConnectionProfile) func connectionService(didDeactivate profile: ConnectionProfile) @@ -224,7 +230,7 @@ class ConnectionService: Codable { } } - func saveProfiles() throws { + func saveProfiles() { let encoder = JSONEncoder() let fm = FileManager.default @@ -337,18 +343,69 @@ class ConnectionService: Codable { } // serialize immediately - try? saveProfiles() + saveProfiles() + + delegate?.connectionService(didAdd: profile) + } + + func renameProfile(_ key: ProfileKey, to newId: String) -> ConnectionProfile? { + precondition(newId != key.id) + + // WARNING: can be a placeholder + guard let oldProfile = cache[key] else { + return nil + } + + let fm = FileManager.default + let temporaryDelegate = delegate + delegate = nil + + // 1. add renamed profile + let newProfile = oldProfile.with(newId: newId) + let newKey = ProfileKey(newProfile) + let sameCredentials = credentials(for: oldProfile) + addOrReplaceProfile(newProfile, credentials: sameCredentials) + + // 2. rename .ovpn (if present) + if let cfgFrom = configurationURL(for: key) { + let cfgTo = targetConfigurationURL(for: newKey) + try? fm.removeItem(at: cfgTo) + try? fm.moveItem(at: cfgFrom, to: cfgTo) + } + + // 3. remove old entry + removeProfile(key) + + // 4. replace active key (if active) + if key == activeProfileKey { + activeProfileKey = newKey + } + + // serialize immediately + saveProfiles() + + delegate = temporaryDelegate + delegate?.connectionService(didRename: oldProfile, to: newProfile) + + return newProfile + } + + func renameProfile(_ profile: ConnectionProfile, to id: String) -> ConnectionProfile? { + return renameProfile(ProfileKey(profile), to: id) } func removeProfile(_ key: ProfileKey) { - guard let i = cache.index(forKey: key) else { + guard let profile = cache[key] else { return } - cache.remove(at: i) + cache.removeValue(forKey: key) + removeCredentials(for: profile) pendingRemoval.insert(key) if cache.isEmpty { activeProfileKey = nil } + + delegate?.connectionService(didRemoveProfileWithKey: key) } func containsProfile(_ key: ProfileKey) -> Bool { @@ -402,6 +459,10 @@ class ConnectionService: Codable { try profile.setPassword(credentials?.password, in: keychain) } + func removeCredentials(for profile: ConnectionProfile) { + profile.removePassword(in: keychain) + } + // MARK: VPN func vpnConfiguration() throws -> NetworkExtensionVPNConfiguration { @@ -479,7 +540,7 @@ private class PlaceholderConnectionProfile: ConnectionProfile { let id: String - var username: String? + var username: String? = nil var requiresCredentials: Bool = false @@ -487,6 +548,10 @@ private class PlaceholderConnectionProfile: ConnectionProfile { fatalError("Generating configuration from a PlaceholderConnectionProfile") } + func with(newId: String) -> ConnectionProfile { + return PlaceholderConnectionProfile(ConnectionService.ProfileKey(context, newId)) + } + var mainAddress: String = "" var addresses: [String] = [] diff --git a/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift b/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift index 591cdebe..154d014f 100644 --- a/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift +++ b/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift @@ -65,6 +65,13 @@ class HostConnectionProfile: ConnectionProfile, Codable, Equatable { return builder.build() } + + func with(newId: String) -> ConnectionProfile { + let profile = HostConnectionProfile(title: newId, hostname: hostname) + profile.username = username + profile.parameters = parameters + return profile + } } extension HostConnectionProfile { diff --git a/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift b/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift index 7343571d..fa577cc7 100644 --- a/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift +++ b/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift @@ -139,6 +139,10 @@ class ProviderConnectionProfile: ConnectionProfile, Codable, Equatable { } return builder.build() } + + func with(newId: String) -> ConnectionProfile { + fatalError("Cannot rename a ProviderConnectionProfile") + } } extension ProviderConnectionProfile { diff --git a/Passepartout/Sources/Model/TransientStore.swift b/Passepartout/Sources/Model/TransientStore.swift index c855d0d0..0f9187a3 100644 --- a/Passepartout/Sources/Model/TransientStore.swift +++ b/Passepartout/Sources/Model/TransientStore.swift @@ -79,6 +79,6 @@ class TransientStore { func serialize() { try? JSONEncoder().encode(service).write(to: TransientStore.serviceURL) - try? service.saveProfiles() + service.saveProfiles() } } diff --git a/Passepartout/Sources/Model/Wizard.swift b/Passepartout/Sources/Model/Wizard.swift deleted file mode 100644 index cad6dceb..00000000 --- a/Passepartout/Sources/Model/Wizard.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Wizard.swift -// Passepartout -// -// Created by Davide De Rosa on 9/4/18. -// Copyright (c) 2018 Davide De Rosa. All rights reserved. -// -// https://github.com/keeshux -// -// 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 . -// - -import Foundation - -extension Notification.Name { - static let WizardDidCreate = Notification.Name("WizardDidCreate") -} - -enum WizardCreationKey: String { - case profile - - case credentials -} diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index 2ef727f9..e7f290da 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -243,6 +243,14 @@ internal enum L10n { internal static let next = L10n.tr("Localizable", "global.next") /// OK internal static let ok = L10n.tr("Localizable", "global.ok") + internal enum Host { + internal enum TitleInput { + /// Legal characters are alphanumerics plus dash (-), underscore (_) and dot (.). + internal static let message = L10n.tr("Localizable", "global.host.title_input.message") + /// My Profile + internal static let placeholder = L10n.tr("Localizable", "global.host.title_input.placeholder") + } + } } internal enum ImportedHosts { @@ -415,6 +423,10 @@ internal enum L10n { /// Do you want to reconnect to the VPN? internal static let message = L10n.tr("Localizable", "service.alerts.reconnect_vpn.message") } + internal enum Rename { + /// Rename profile + internal static let title = L10n.tr("Localizable", "service.alerts.rename.title") + } internal enum TestConnectivity { /// Connectivity internal static let title = L10n.tr("Localizable", "service.alerts.test_connectivity.title") @@ -655,8 +667,6 @@ internal enum L10n { internal enum TitleInput { /// Title internal static let caption = L10n.tr("Localizable", "wizards.host.cells.title_input.caption") - /// My Profile - internal static let placeholder = L10n.tr("Localizable", "wizards.host.cells.title_input.placeholder") } } internal enum Sections { diff --git a/Passepartout/Sources/Utils.swift b/Passepartout/Sources/Utils.swift index 89091858..f091988e 100644 --- a/Passepartout/Sources/Utils.swift +++ b/Passepartout/Sources/Utils.swift @@ -206,7 +206,7 @@ extension CharacterSet { static let filename: CharacterSet = { var chars: CharacterSet = .decimalDigits let english = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - let symbols = "-_" + let symbols = "-_." chars.formUnion(CharacterSet(charactersIn: english)) chars.formUnion(CharacterSet(charactersIn: english.lowercased())) chars.formUnion(CharacterSet(charactersIn: symbols))