Add minimal TV app

Closes #315
This commit is contained in:
Davide De Rosa 2023-12-16 20:58:54 +01:00
parent 47c6b02c4d
commit 5c5697762b
No known key found for this signature in database
GPG Key ID: A48836171C759F5E
261 changed files with 2155 additions and 223 deletions

7
.env.tvos Normal file
View File

@ -0,0 +1,7 @@
INFO_PLIST_ROOT="Passepartout/App"
MATCH_PLATFORM="tvos"
GYM_SCHEME="Passepartout"
DELIVER_PLATFORM="appletvos"
DELIVER_METADATA_PATH="Passepartout/App/fastlane/tvos/metadata"
DELIVER_SCREENSHOTS_PATH="Passepartout/App/fastlane/tvos/screenshots"
CHANGELOG="CHANGELOG.md"

View File

@ -17,7 +17,7 @@ env:
jobs:
build_upload:
name: Distribute Private Beta
runs-on: macos-12
runs-on: macos-13
environment:
name: private_beta
strategy:
@ -50,7 +50,7 @@ jobs:
go-version: "^1.17"
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
xcode-version: "15.1"
- name: Create keychain
uses: ./.github/actions/create-keychain
with:

View File

@ -16,7 +16,7 @@ env:
jobs:
build_upload:
name: Upload to ASC
runs-on: macos-12
runs-on: macos-13
strategy:
fail-fast: true
matrix:
@ -48,7 +48,7 @@ jobs:
go-version: "^1.17"
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
xcode-version: "15.1"
- name: Store app version
id: app_version
if: ${{ matrix.use_version }}

View File

@ -18,7 +18,7 @@ concurrency:
jobs:
run_tests:
name: Run tests
runs-on: macos-12
runs-on: macos-13
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
@ -26,7 +26,7 @@ jobs:
submodules: true
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
xcode-version: "15.1"
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- App for tvOS. [#315](https://github.com/passepartoutvpn/passepartout-apple/issues/315)
- WireGuard: Show data count. [#312](https://github.com/passepartoutvpn/passepartout-apple/issues/312)
### Changed

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
@ -19,7 +19,7 @@
0E0838FA2877325A00A34EC0 /* LightProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0838F92877325A00A34EC0 /* LightProviderManager.swift */; };
0E0838FB2877325A00A34EC0 /* LightProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0838F92877325A00A34EC0 /* LightProviderManager.swift */; };
0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0838FC2877334300A34EC0 /* DefaultLightProviderManager.swift */; };
0E09E35D2834172800BE1BAE /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 0E09E35C2834172800BE1BAE /* Credits.rtf */; };
0E09E35D2834172800BE1BAE /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 0E09E35C2834172800BE1BAE /* Credits.rtf */; platformFilter = maccatalyst; };
0E0BD27327B2EA2C00583AC5 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27227B2EA2C00583AC5 /* MainView.swift */; };
0E0BD27627B2EB2200583AC5 /* DonateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27527B2EB2200583AC5 /* DonateView.swift */; };
0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */; };
@ -30,6 +30,7 @@
0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6529C84CF60022E884 /* LogoView.swift */; };
0E1AD5CE2A268645002AE6E6 /* Errors+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */; };
0E1B5F5C29C506AD00FE7D18 /* DiagnosticsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1B5F5B29C506AC00FE7D18 /* DiagnosticsSection.swift */; };
0E1DC1BF2B3618EE008B755E /* ProfileView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1DC1BE2B3618EE008B755E /* ProfileView+TV.swift */; };
0E1F5628287F0ECB00F8ADD7 /* ProviderProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */; };
0E1F562B287F0EF100F8ADD7 /* ProviderProfileItem+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5629287F0EEE00F8ADD7 /* ProviderProfileItem+ViewModel.swift */; };
0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */; };
@ -47,6 +48,9 @@
0E2E0B762B335AAB00E3204A /* UpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */; };
0E2E0B772B335AAB00E3204A /* UpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */; };
0E2E0B782B335AAB00E3204A /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */; };
0E330F532B30469700930C7C /* MockProfileRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E330F522B30469700930C7C /* MockProfileRepository.swift */; };
0E330F552B30946600930C7C /* ActiveProfileView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E330F542B30946600930C7C /* ActiveProfileView+TV.swift */; };
0E330F572B30952300930C7C /* ProfilesList+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E330F562B30952300930C7C /* ProfilesList+TV.swift */; };
0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */; };
0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */; };
0E34A2CF27CADA6300C73B67 /* GenericVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */; };
@ -108,6 +112,7 @@
0E7A8C0C2A1D4A6100780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0B2A1D4A6100780F4B /* PassepartoutLibrary */; };
0E7A8C0F2A1D54DE00780F4B /* Picker+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */; };
0E7A8C102A1D54DE00780F4B /* Picker+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */; };
0E859B832B2EE08700F80D92 /* OrganizerView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E859B822B2EE08700F80D92 /* OrganizerView+TV.swift */; };
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */; };
0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */; };
0E92D7C927F1042A0033CB7B /* ProfileView+Extra.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */; };
@ -184,10 +189,12 @@
0ED89C1727DE0E05008B36D6 /* IntentEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */; };
0ED89C1C27DE3ABC008B36D6 /* ShortcutsView+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */; };
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */; };
0EDDEC7D28D0DC140017802E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */; };
0EDE02C227F61C79000FBE3C /* EditableTextList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */; };
0EE11CD2280D8317003BE431 /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE11CD1280D8317003BE431 /* SettingsButton.swift */; };
0EE562782B2EE3EC000C52F6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EE562772B2EE3EC000C52F6 /* LaunchScreen.storyboard */; platformFilters = (ios, maccatalyst, ); };
0EE79B2F2B2ED99500C1220C /* MainView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE79B2E2B2ED99500C1220C /* MainView+TV.swift */; };
0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */; };
0EED5B9D2B3700AB009D1E97 /* TunnelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF656402B36C00E00CEFC96 /* TunnelError.swift */; };
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0FAF527DD0211007EB181 /* PaywallView.swift */; };
0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA591122733DD4E0096F796 /* IntentDispatcher.swift */; };
0EF0FAF927DD212C007EB181 /* IntentActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0FAF827DD212C007EB181 /* IntentActivity.swift */; };
@ -195,6 +202,10 @@
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; };
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; };
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; };
0EF6563E2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */; };
0EF6563F2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */; };
0EF656422B36C01200CEFC96 /* TunnelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF656402B36C00E00CEFC96 /* TunnelError.swift */; };
0EF656432B36C01200CEFC96 /* TunnelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF656402B36C00E00CEFC96 /* TunnelError.swift */; };
0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; };
A38D607728AFCFD20005C271 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38D607628AFCFD20005C271 /* SettingsView.swift */; };
A3A7CC462878DC8300172D7D /* ProviderServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A7CC452878DC8300172D7D /* ProviderServerItem.swift */; };
@ -248,6 +259,13 @@
remoteGlobalIDString = 0ECF71F327B6D9CD00CDB528;
remoteInfo = WireGuardGo;
};
0EE79B342B2EDB9C00C1220C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0E57F63020C83FC5008323CF /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0EE79B302B2EDB5D00C1220C;
remoteInfo = WireGuardGoTV;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -311,6 +329,7 @@
0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Errors+L10n.swift"; sourceTree = "<group>"; };
0E1B5F5B29C506AC00FE7D18 /* DiagnosticsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsSection.swift; sourceTree = "<group>"; };
0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0E1DC1BE2B3618EE008B755E /* ProfileView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+TV.swift"; sourceTree = "<group>"; };
0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProfileItem.swift; sourceTree = "<group>"; };
0E1F5629287F0EEE00F8ADD7 /* ProviderProfileItem+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderProfileItem+ViewModel.swift"; sourceTree = "<group>"; };
0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
@ -329,6 +348,9 @@
0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeManagerStrategy.swift; sourceTree = "<group>"; };
0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeManager.swift; sourceTree = "<group>"; };
0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = "<group>"; };
0E330F522B30469700930C7C /* MockProfileRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfileRepository.swift; sourceTree = "<group>"; };
0E330F542B30946600930C7C /* ActiveProfileView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveProfileView+TV.swift"; sourceTree = "<group>"; };
0E330F562B30952300930C7C /* ProfilesList+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilesList+TV.swift"; sourceTree = "<group>"; };
0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenVPN+L10n.swift"; sourceTree = "<group>"; };
0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Core+L10n.swift"; sourceTree = "<group>"; };
0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericVersionView.swift; sourceTree = "<group>"; };
@ -391,6 +413,7 @@
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>"; };
0E859B822B2EE08700F80D92 /* OrganizerView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+TV.swift"; sourceTree = "<group>"; };
0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostViewModel.swift; sourceTree = "<group>"; };
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Configuration.swift"; sourceTree = "<group>"; };
0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Extra.swift"; sourceTree = "<group>"; };
@ -493,13 +516,14 @@
0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShortcutsView+Add.swift"; sourceTree = "<group>"; };
0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentAddView.swift; sourceTree = "<group>"; };
0EDCEF692B337BEB0023A7FF /* PassepartoutLibrary.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PassepartoutLibrary.xctestplan; sourceTree = "<group>"; };
0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextList.swift; sourceTree = "<group>"; };
0EDE8DBF20C86910004C739C /* PassepartoutOpenVPNTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutOpenVPNTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0EDE8DC320C86910004C739C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0EDE8DD220C86978004C739C /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; };
0EDE8DE220C86A13004C739C /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
0EE11CD1280D8317003BE431 /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; };
0EE562772B2EE3EC000C52F6 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
0EE79B2E2B2ED99500C1220C /* MainView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainView+TV.swift"; sourceTree = "<group>"; };
0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNProtocolType+FileExtensions.swift"; sourceTree = "<group>"; };
0EF0FAF527DD0211007EB181 /* PaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallView.swift; sourceTree = "<group>"; };
0EF0FAF827DD212C007EB181 /* IntentActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentActivity.swift; sourceTree = "<group>"; };
@ -507,6 +531,8 @@
0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = "<group>"; };
0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = "<group>"; };
0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = "<group>"; };
0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEPacketTunnelProvider+Expiration.swift"; sourceTree = "<group>"; };
0EF656402B36C00E00CEFC96 /* TunnelError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelError.swift; sourceTree = "<group>"; };
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = "<group>"; };
A373484D29DC4F4500D1613C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
A373484E29DC504000D1613C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -572,6 +598,7 @@
0E021D9B284E68580077EF5D /* AppContext.swift */,
0E293856285A73BC002A6E0E /* AppContext+Shared.swift */,
0E021D9A284E68580077EF5D /* CoreContext.swift */,
0E330F522B30469700930C7C /* MockProfileRepository.swift */,
);
path = Context;
sourceTree = "<group>";
@ -630,6 +657,7 @@
0E35C0AE280EF8A80071FA35 /* Views */ = {
isa = PBXGroup;
children = (
0EE79B2D2B2ED96D00C1220C /* TV */,
0E44689B27B11B5300A14CE4 /* AboutView.swift */,
0ECF71ED27B6A99300CDB528 /* AccountView.swift */,
0E039278281890B100827C10 /* AddHostView.swift */,
@ -673,6 +701,7 @@
0E3CD482280DAE92007075C0 /* ProfileView+MainMenu.swift */,
0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */,
0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */,
0E1DC1BE2B3618EE008B755E /* ProfileView+TV.swift */,
0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */,
0E71ACF027C1073800F85C4B /* ProviderLocationView.swift */,
0E71ACEE27C106B400F85C4B /* ProviderPresetView.swift */,
@ -889,7 +918,7 @@
0E57F64720C83FC7008323CF /* Info.plist */,
0E0C072B236087A100155AAC /* InfoPlist.strings */,
0E9E5AE227B44CF1008C95DA /* Localizable.strings */,
0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */,
0EE562772B2EE3EC000C52F6 /* LaunchScreen.storyboard */,
0E3FC6852867A3F9009B851C /* AppDelegate.swift */,
0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */,
0E0F4C5929C761850022E884 /* SceneDelegate.swift */,
@ -975,6 +1004,8 @@
0ED2B33D27D3C53400FD8EA9 /* WireGuard */,
0ED31C3B20CF39510027975F /* Tunnel.entitlements */,
0EDE8DC320C86910004C739C /* Info.plist */,
0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */,
0EF656402B36C00E00CEFC96 /* TunnelError.swift */,
);
path = Tunnel;
sourceTree = "<group>";
@ -987,6 +1018,17 @@
name = Packages;
sourceTree = "<group>";
};
0EE79B2D2B2ED96D00C1220C /* TV */ = {
isa = PBXGroup;
children = (
0E330F542B30946600930C7C /* ActiveProfileView+TV.swift */,
0EE79B2E2B2ED99500C1220C /* MainView+TV.swift */,
0E859B822B2EE08700F80D92 /* OrganizerView+TV.swift */,
0E330F562B30952300930C7C /* ProfilesList+TV.swift */,
);
path = TV;
sourceTree = "<group>";
};
374B9F085E1148C37CF9117A /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -1014,6 +1056,20 @@
passBuildSettingsInEnvironment = 1;
productName = PassepartoutWireGuard;
};
0EE79B302B2EDB5D00C1220C /* WireGuardGoTV */ = {
isa = PBXLegacyTarget;
buildArgumentsString = "$(ACTION)";
buildConfigurationList = 0EE79B312B2EDB5D00C1220C /* Build configuration list for PBXLegacyTarget "WireGuardGoTV" */;
buildPhases = (
);
buildToolPath = "$(PROJECT_DIR)/Passepartout/App/Scripts/build_wireguard_go_bridge.sh";
buildWorkingDirectory = "";
dependencies = (
);
name = WireGuardGoTV;
passBuildSettingsInEnvironment = 1;
productName = PassepartoutWireGuard;
};
/* End PBXLegacyTarget section */
/* Begin PBXNativeTarget section */
@ -1052,9 +1108,10 @@
dependencies = (
0ECB78E7285F5CC400B0E460 /* PBXTargetDependency */,
0E41BDAB286713F6006346B4 /* PBXTargetDependency */,
0ECF71FC27B6DA6700CDB528 /* PBXTargetDependency */,
0EB2B14A2733FB6F007705AB /* PBXTargetDependency */,
0ED2B36227D3C99100FD8EA9 /* PBXTargetDependency */,
0ECF71FC27B6DA6700CDB528 /* PBXTargetDependency */,
0EE79B352B2EDB9C00C1220C /* PBXTargetDependency */,
);
name = Passepartout;
packageProductDependencies = (
@ -1210,8 +1267,9 @@
0ECB78D9285F52F700B0E460 /* PassepartoutMac */,
0E41BD96286711C3006346B4 /* PassepartoutLauncher */,
0EDE8DBE20C86910004C739C /* OpenVPNTunnel */,
0ECF71F327B6D9CD00CDB528 /* WireGuardGo */,
0ED2B33E27D3C77800FD8EA9 /* WireGuardTunnel */,
0ECF71F327B6D9CD00CDB528 /* WireGuardGo */,
0EE79B302B2EDB5D00C1220C /* WireGuardGoTV */,
);
};
/* End PBXProject section */
@ -1229,8 +1287,8 @@
buildActionMask = 2147483647;
files = (
0E6059CB27FCC5DE003F4063 /* Flags.xcassets in Resources */,
0EE562782B2EE3EC000C52F6 /* LaunchScreen.storyboard in Resources */,
0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */,
0EDDEC7D28D0DC140017802E /* LaunchScreen.storyboard in Resources */,
0E6059CC27FCC5DE003F4063 /* Providers.xcassets in Resources */,
0E6059CD27FCC5DE003F4063 /* Assets.xcassets in Resources */,
0E9E5AEF27B44CF1008C95DA /* Localizable.strings in Resources */,
@ -1376,6 +1434,7 @@
0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Extensions.swift in Sources */,
0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */,
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
0EED5B9D2B3700AB009D1E97 /* TunnelError.swift in Sources */,
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */,
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
@ -1411,6 +1470,7 @@
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */,
0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */,
0E330F552B30946600930C7C /* ActiveProfileView+TV.swift in Sources */,
0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */,
0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */,
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Restricted.swift in Sources */,
@ -1421,6 +1481,7 @@
0E96D30228720067005EFBCF /* LightVPNManager.swift in Sources */,
0ED89C1727DE0E05008B36D6 /* IntentEditView.swift in Sources */,
0E70589B28377DC40075D1D2 /* VPNStatusText.swift in Sources */,
0EE79B2F2B2ED99500C1220C /* MainView+TV.swift in Sources */,
0E71ACE927C1055300F85C4B /* NetworkSettingsView.swift in Sources */,
0EB34BCA27C6A70200B126DA /* OnDemandView.swift in Sources */,
0E0838FA2877325A00A34EC0 /* LightProviderManager.swift in Sources */,
@ -1432,6 +1493,7 @@
0EB17EBA27D2560300D473B5 /* PassepartoutProviders+Extensions.swift in Sources */,
0E3B7FDA27E51A0200C66F13 /* ProfileView+Provider.swift in Sources */,
0E2E0B6F2B335A8E00E3204A /* AppPreference.swift in Sources */,
0E859B832B2EE08700F80D92 /* OrganizerView+TV.swift in Sources */,
0E5468062867AEC500F74D1C /* MacMenu.swift in Sources */,
0E71ACE327C0F2E400F85C4B /* Providers+L10n.swift in Sources */,
0E2E0B752B335AAB00E3204A /* IntentsManager.swift in Sources */,
@ -1457,6 +1519,7 @@
0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */,
0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */,
0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */,
0E330F572B30952300930C7C /* ProfilesList+TV.swift in Sources */,
0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */,
0E2E0B772B335AAB00E3204A /* UpgradeManager.swift in Sources */,
0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */,
@ -1471,6 +1534,7 @@
0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */,
0E2E0B782B335AAB00E3204A /* PersistenceManager.swift in Sources */,
0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */,
0E330F532B30469700930C7C /* MockProfileRepository.swift in Sources */,
0E3CD483280DAE92007075C0 /* ProfileView+MainMenu.swift in Sources */,
0E71ACEB27C1060D00F85C4B /* EndpointView.swift in Sources */,
0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */,
@ -1478,6 +1542,7 @@
0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */,
0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */,
0EF2212B27E667EA001D0BD7 /* AddProviderView+Name.swift in Sources */,
0E1DC1BF2B3618EE008B755E /* ProfileView+TV.swift in Sources */,
0E065F112813269500062CAF /* WelcomeView.swift in Sources */,
0E2DE71F27DCD0290067B9E1 /* TunnelKit+L10n.swift in Sources */,
0E49F6BF27D764AF00385834 /* EndpointAdvancedView.swift in Sources */,
@ -1500,9 +1565,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0EF6563F2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */,
0ED2B35B27D3C94F00FD8EA9 /* PacketTunnelProvider.swift in Sources */,
0ED30DDD27EA35230057D8A3 /* Constants.swift in Sources */,
0ED1A5FD2B2B98CC00A0EA90 /* Constants+Tunnel.swift in Sources */,
0EF656432B36C01200CEFC96 /* TunnelError.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1510,9 +1577,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0EF6563E2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */,
0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */,
0EB17EA727D226B400D473B5 /* Constants.swift in Sources */,
0ED30DDB27EA351C0057D8A3 /* Constants+Tunnel.swift in Sources */,
0EF656422B36C01200CEFC96 /* TunnelError.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1538,6 +1607,10 @@
};
0ECF71FC27B6DA6700CDB528 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (
ios,
maccatalyst,
);
target = 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */;
targetProxy = 0ECF71FB27B6DA6700CDB528 /* PBXContainerItemProxy */;
};
@ -1551,6 +1624,14 @@
target = 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */;
targetProxy = 0ED2B36A27D3CAB100FD8EA9 /* PBXContainerItemProxy */;
};
0EE79B352B2EDB9C00C1220C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (
tvos,
);
target = 0EE79B302B2EDB5D00C1220C /* WireGuardGoTV */;
targetProxy = 0EE79B342B2EDB9C00C1220C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -1730,7 +1811,8 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
};
name = Debug;
};
@ -1791,7 +1873,8 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
@ -1801,6 +1884,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
"ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]" = TV;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Passepartout/App/App.entitlements;
@ -1814,11 +1898,12 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID)";
PRODUCT_NAME = Passepartout;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout tvos";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout catalyst";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = targeted;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@ -1827,6 +1912,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
"ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]" = TV;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Passepartout/App/App.entitlements;
@ -1840,10 +1926,11 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID)";
PRODUCT_NAME = Passepartout;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout tvos";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout catalyst";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@ -1959,8 +2046,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).WireGuardTunnel";
PRODUCT_NAME = PassepartoutWireGuardTunnel;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel tvos";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel catalyst";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
};
name = Debug;
@ -1979,8 +2068,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).WireGuardTunnel";
PRODUCT_NAME = PassepartoutWireGuardTunnel;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel tvos";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel catalyst";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
};
name = Release;
@ -1999,8 +2090,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).OpenVPNTunnel";
PRODUCT_NAME = PassepartoutOpenVPNTunnel;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel tvos";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel catalyst";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
};
name = Debug;
@ -2019,12 +2112,51 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).OpenVPNTunnel";
PRODUCT_NAME = PassepartoutOpenVPNTunnel;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel";
"PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel tvos";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel catalyst";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
};
name = Release;
};
0EE79B322B2EDB5D00C1220C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUGGING_SYMBOLS = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = DTDYD63ZX9;
GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_CFLAGS = "";
OTHER_LDFLAGS = "";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
};
name = Debug;
};
0EE79B332B2EDB5D00C1220C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = DTDYD63ZX9;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CFLAGS = "";
OTHER_LDFLAGS = "";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -2091,6 +2223,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0EE79B312B2EDB5D00C1220C /* Build configuration list for PBXLegacyTarget "WireGuardGoTV" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0EE79B322B2EDB5D00C1220C /* Debug */,
0EE79B332B2EDB5D00C1220C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */

View File

@ -1,70 +1,67 @@
{
"object": {
"pins": [
{
"package": "DTFoundation",
"repositoryURL": "https://github.com/Cocoanetics/DTFoundation.git",
"state": {
"branch": null,
"revision": "76062513434421cb6c8a1ae1d4f8368a7ebc2da3",
"version": "1.7.18"
}
},
{
"package": "GenericJSON",
"repositoryURL": "https://github.com/zoul/generic-json-swift",
"state": {
"branch": null,
"revision": "0a06575f4038b504e78ac330913d920f1630f510",
"version": "2.0.2"
}
},
{
"package": "Kvitto",
"repositoryURL": "https://github.com/Cocoanetics/Kvitto",
"state": {
"branch": null,
"revision": "88888674d772ddcf19671159ed0022cb0bc37be2",
"version": "1.0.6"
}
},
{
"package": "openssl-apple",
"repositoryURL": "https://github.com/passepartoutvpn/openssl-apple",
"state": {
"branch": null,
"revision": "026702febcaebcbf9ea68f2fa66b017eba998cdf",
"version": "3.2.105"
}
},
{
"package": "SwiftyBeaver",
"repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver",
"state": {
"branch": null,
"revision": "12b5acf96d98f91d50de447369bd18df74600f1a",
"version": "1.9.6"
}
},
{
"package": "TunnelKit",
"repositoryURL": "https://github.com/passepartoutvpn/tunnelkit",
"state": {
"branch": null,
"revision": "bda84bf569792fbb702d0173de3c9c58768f9153",
"version": null
}
},
{
"package": "WireGuardKit",
"repositoryURL": "https://github.com/passepartoutvpn/wireguard-apple",
"state": {
"branch": null,
"revision": "73d9152fa0cb661db0348a1ac11dbbf998422a50",
"version": "1.0.17"
}
"pins" : [
{
"identity" : "dtfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cocoanetics/DTFoundation.git",
"state" : {
"revision" : "76062513434421cb6c8a1ae1d4f8368a7ebc2da3",
"version" : "1.7.18"
}
]
},
"version": 1
},
{
"identity" : "generic-json-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zoul/generic-json-swift",
"state" : {
"revision" : "0a06575f4038b504e78ac330913d920f1630f510",
"version" : "2.0.2"
}
},
{
"identity" : "kvitto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cocoanetics/Kvitto",
"state" : {
"revision" : "88888674d772ddcf19671159ed0022cb0bc37be2",
"version" : "1.0.6"
}
},
{
"identity" : "openssl-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/passepartoutvpn/openssl-apple",
"state" : {
"revision" : "026702febcaebcbf9ea68f2fa66b017eba998cdf",
"version" : "3.2.105"
}
},
{
"identity" : "swiftybeaver",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyBeaver/SwiftyBeaver",
"state" : {
"revision" : "12b5acf96d98f91d50de447369bd18df74600f1a",
"version" : "1.9.6"
}
},
{
"identity" : "tunnelkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/passepartoutvpn/tunnelkit",
"state" : {
"revision" : "708c785e615f5715ce08386c772c92fb45730a3a"
}
},
{
"identity" : "wireguard-apple",
"kind" : "remoteSourceControl",
"location" : "https://github.com/passepartoutvpn/wireguard-apple",
"state" : {
"branch" : "develop",
"revision" : "b79f0f150356d8200a64922ecf041dd020140aa0"
}
}
],
"version" : 2
}

View File

@ -127,7 +127,7 @@
<EnvironmentVariable
key = "APP_TYPE"
value = "2"
isEnabled = "YES">
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "LOG_LEVEL"

View File

@ -7,6 +7,7 @@
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.algoritmico.Passepartout</string>
<string>iCloud.com.algoritmico.Passepartout.Shared</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AppIcon.png",
"idiom" : "tv"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"layers" : [
{
"filename" : "Front.imagestacklayer"
},
{
"filename" : "Back.imagestacklayer"
}
]
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AppIcon.png",
"idiom" : "tv"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "AppIcon.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"filename" : "AppIcon@2x.png",
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"layers" : [
{
"filename" : "Front.imagestacklayer"
},
{
"filename" : "Back.imagestacklayer"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "AppIcon.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"filename" : "AppIcon@2x.png",
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,32 @@
{
"assets" : [
{
"filename" : "App Icon - App Store.imagestack",
"idiom" : "tv",
"role" : "primary-app-icon",
"size" : "1280x768"
},
{
"filename" : "App Icon.imagestack",
"idiom" : "tv",
"role" : "primary-app-icon",
"size" : "400x240"
},
{
"filename" : "Top Shelf Image Wide.imageset",
"idiom" : "tv",
"role" : "top-shelf-image-wide",
"size" : "2320x720"
},
{
"filename" : "Top Shelf Image.imageset",
"idiom" : "tv",
"role" : "top-shelf-image",
"size" : "1920x720"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "TopShelf.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"filename" : "TopShelf@2x.png",
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "TopShelf.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"filename" : "TopShelf@2x.png",
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -44,6 +44,8 @@ extension Constants {
enum CloudKit {
static let containerId: String = bundleConfig("cloudkit_id")
static let sharedContainerId: String = bundleConfig("cloudkit_shared_id")
static let coreDataZone = "com.apple.coredata.cloudkit.zone"
}
@ -84,6 +86,8 @@ extension Constants {
return []
}
#endif
static let tvLimitedMinutes = 10
}
}
@ -131,6 +135,8 @@ extension Constants {
enum Persistence {
static let profilesContainerName = "Profiles"
static let sharedProfilesContainerName = "SharedProfiles"
static let providersContainerName = "Providers"
}

View File

@ -23,7 +23,9 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import LocalAuthentication
#endif
import PassepartoutLibrary
import SwiftUI
@ -33,20 +35,28 @@ extension View {
}
var themeIsiPadPortrait: Bool {
#if !os(tvOS)
#if targetEnvironment(macCatalyst)
false
#else
let device: UIDevice = .current
return device.userInterfaceIdiom == .pad && device.orientation.isPortrait
#endif
#else
false
#endif
}
var themeIsiPadMultitasking: Bool {
#if !os(tvOS)
#if targetEnvironment(macCatalyst)
false
#else
UIDevice.current.userInterfaceIdiom == .pad
#endif
#else
false
#endif
}
}
@ -55,6 +65,7 @@ extension View {
extension View {
func themeGlobal() -> some View {
themeNavigationViewStyle()
#if !os(tvOS)
#if !targetEnvironment(macCatalyst)
.themeLockScreen()
#endif
@ -62,22 +73,39 @@ extension View {
.listStyle(themeListStyleValue())
.toggleStyle(themeToggleStyleValue())
.menuStyle(.borderlessButton)
#endif
.withErrorHandler()
}
#if os(tvOS)
func themeTV() -> some View {
GeometryReader { geo in
self
.padding(.horizontal, 0.25 * geo.size.width)
.scrollClipDisabled()
}
}
#endif
func themePrimaryView() -> some View {
#if targetEnvironment(macCatalyst)
navigationBarTitleDisplayMode(.inline)
.themeSidebarListStyle()
#else
#elseif !os(tvOS)
navigationBarTitleDisplayMode(.large)
.navigationTitle(Unlocalized.appName)
.themeSidebarListStyle()
#else
self
#endif
}
func themeSecondaryView() -> some View {
#if !os(tvOS)
navigationBarTitleDisplayMode(.inline)
#else
self
#endif
}
@ViewBuilder
@ -93,6 +121,7 @@ extension View {
@ViewBuilder
private func themeSidebarListStyle() -> some View {
#if !os(tvOS)
switch themeIdiom {
case .phone:
listStyle(.insetGrouped)
@ -100,6 +129,9 @@ extension View {
default:
listStyle(.sidebar)
}
#else
self
#endif
}
@ViewBuilder
@ -108,11 +140,19 @@ extension View {
}
private func themeListStyleValue() -> some ListStyle {
#if !os(tvOS)
.insetGrouped
#else
PlainListStyle()
#endif
}
private func themeToggleStyleValue() -> some ToggleStyle {
#if !os(tvOS)
.switch
#else
DefaultToggleStyle()
#endif
}
}
@ -179,6 +219,10 @@ extension View {
"eye"
}
var themeAppleTVImage: String {
"tv"
}
// MARK: Organizer
func themeAssetsProviderImage(_ providerName: ProviderName) -> String {
@ -387,6 +431,7 @@ extension View {
// MARK: Shortcuts
#if !os(tvOS)
extension ShortcutType {
var themeImageName: String {
switch self {
@ -401,6 +446,7 @@ extension ShortcutType {
}
}
}
#endif
// MARK: Animations
@ -497,6 +543,7 @@ extension View {
// MARK: Lock screen
#if !os(tvOS)
extension View {
func themeLockScreen() -> some View {
@AppStorage(AppPreference.locksInBackground.key) var locksInBackground = false
@ -528,6 +575,7 @@ extension View {
}
}
}
#endif
// MARK: Validation

View File

@ -66,6 +66,7 @@ final class AppContext {
persistenceManager = PersistenceManager(
store: store,
ckContainerId: Constants.CloudKit.containerId,
ckSharedContainerId: Constants.CloudKit.sharedContainerId,
ckCoreDataZone: Constants.CloudKit.coreDataZone
)
@ -94,6 +95,10 @@ final class AppContext {
private extension AppContext {
func configureObjects() {
coreContext.profileManager.willSaveSharedProfile = { [unowned self] in
willSaveSharedProfile(withNewProfile: $0, existingProfile: $1)
}
coreContext.vpnManager.isOnDemandRulesSupported = {
self.isEligibleForOnDemandRules()
}
@ -101,6 +106,13 @@ private extension AppContext {
self.isEligibleForNetworkSettings()
}
coreContext.vpnManager.userData = {
if let expirationDate = $0.connectionExpirationDate {
return [Constants.Tunnel.expirationTimeIntervalKey: expirationDate.timeIntervalSinceReferenceDate]
}
return nil
}
coreContext.vpnManager.currentState.$vpnStatus
.removeDuplicates()
.receive(on: DispatchQueue.main)
@ -138,4 +150,41 @@ private extension AppContext {
}
return true
}
// eligibility: expire restricted TV profiles after N minutes
func willSaveSharedProfile(withNewProfile newProfile: Profile, existingProfile: Profile?) -> Profile {
if let existingProfile {
assert(newProfile.id == existingProfile.id)
}
guard productManager.isEligible(forFeature: .appleTV) else {
var restricted = newProfile
let remainingMinutes: Int
let expirationDate: Date
// retain current expiration period if any
if let currentExpirationDate = existingProfile?.connectionExpirationDate {
remainingMinutes = Int(currentExpirationDate.timeIntervalSinceNow / 60.0)
expirationDate = currentExpirationDate
}
// otherwise, expire in N minutes from now
else {
remainingMinutes = Constants.InApp.tvLimitedMinutes
expirationDate = Date()
.addingTimeInterval(TimeInterval(remainingMinutes) * 60.0)
restricted.connectionExpirationDate = expirationDate
}
if remainingMinutes > 0 {
pp_log.warning("\(newProfile.logDescription): TV connection expires in \(remainingMinutes) minutes (at \(expirationDate))")
} else {
pp_log.warning("\(newProfile.logDescription): TV connection expired at \(expirationDate)")
}
return restricted
}
return newProfile
}
}

View File

@ -44,9 +44,14 @@ final class CoreContext {
init(persistenceManager: PersistenceManager) {
store = persistenceManager.store
#if !os(tvOS)
let vpnPersistence = persistenceManager.loadVPNPersistence(
withName: Constants.Persistence.profilesContainerName
)
#endif
let sharedVPNPersistence = persistenceManager.loadSharedVPNPersistence(
withName: Constants.Persistence.sharedProfilesContainerName
)
let providersPersistence = persistenceManager.loadProvidersPersistence(
withName: Constants.Persistence.providersContainerName
)
@ -68,10 +73,20 @@ final class CoreContext {
remoteProvidersStrategy: remoteProvidersStrategy
)
let tvProfileRepository = sharedVPNPersistence.profileRepository()
#if !os(tvOS)
let profileRepository = vpnPersistence.profileRepository()
let sharedProfileRepository = tvProfileRepository
#else
let profileRepository = tvProfileRepository
let sharedProfileRepository: ProfileRepository? = nil
#endif
profileManager = ProfileManager(
store: store,
providerManager: providerManager,
profileRepository: vpnPersistence.profileRepository(),
profileRepository: profileRepository,
sharedProfileRepository: sharedProfileRepository,
keychain: KeychainSecretRepository(appGroup: Constants.App.appGroupId),
keychainEntry: Unlocalized.Keychain.passwordEntry,
keychainLabel: Unlocalized.Keychain.passwordLabel

View File

@ -0,0 +1,60 @@
//
// MockProfileRepository.swift
// Passepartout
//
// Created by Davide De Rosa on 12/18/23.
// 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 Combine
import Foundation
import PassepartoutLibrary
final class MockProfileRepository: ProfileRepository, ObservableObject {
@Published var profiles: [UUID: Profile]
init() {
profiles = [:]
}
func allProfiles() -> [UUID: Profile] {
profiles
}
func profile(withId id: UUID) -> Profile? {
profiles[id]
}
func saveProfiles(_ profiles: [Profile]) throws {
profiles.forEach {
self.profiles[$0.id] = $0
}
}
func removeProfiles(withIds ids: [UUID]) {
ids.forEach {
profiles.removeValue(forKey: $0)
}
}
func willUpdateProfiles() -> AnyPublisher<[UUID: Profile], Never> {
$profiles.eraseToAnyPublisher()
}
}

View File

@ -33,6 +33,8 @@ enum AppError: Error {
case vpn(Passepartout.VPNError)
case tunnel(TunnelError)
case generic(Error)
init(_ error: Error) {

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Foundation
import Intents
import PassepartoutLibrary
@ -157,3 +158,4 @@ extension IntentDispatcher {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Foundation
import Intents
import PassepartoutLibrary
@ -157,3 +158,4 @@ private extension INInteraction {
}
}
}
#endif

View File

@ -24,7 +24,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(CFG_APP_ID)</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
@ -80,7 +80,7 @@
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
@ -99,10 +99,12 @@
<dict>
<key>appstore_id</key>
<string>$(CFG_APPSTORE_ID)</string>
<key>group_id</key>
<string>group.$(CFG_GROUP_ID)</string>
<key>cloudkit_id</key>
<string>iCloud.$(CFG_GROUP_ID)</string>
<key>cloudkit_shared_id</key>
<string>iCloud.$(CFG_GROUP_ID).Shared</string>
<key>group_id</key>
<string>group.$(CFG_GROUP_ID)</string>
</dict>
</dict>
</plist>

View File

@ -77,6 +77,12 @@ extension AppError: LocalizedError {
return nil
}
case .tunnel(let tunnelError):
switch tunnelError {
case .expired:
return V.tunnelExpired
}
case .generic(let error):
return error.localizedDescription
}

View File

@ -259,6 +259,8 @@ enum Unlocalized {
static let iCloud = "iCloud"
static let appleTV = "Apple TV"
static let totp = "TOTP"
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Combine
import Foundation
@preconcurrency import Intents
@ -106,3 +107,4 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate {
shouldDismissIntentView.send()
}
}
#endif

View File

@ -35,10 +35,14 @@ final class PersistenceManager: ObservableObject {
private let ckContainerId: String
private let ckSharedContainerId: String
private let ckCoreDataZone: String
private var vpnPersistence: VPNPersistence?
private var sharedVPNPersistence: VPNPersistence?
private var providersPersistence: ProvidersPersistence?
private(set) var isCloudSyncingEnabled: Bool {
@ -52,9 +56,13 @@ final class PersistenceManager: ObservableObject {
let didChangePersistence = PassthroughSubject<Void, Never>()
init(store: KeyValueStore, ckContainerId: String, ckCoreDataZone: String) {
init(store: KeyValueStore,
ckContainerId: String,
ckSharedContainerId: String,
ckCoreDataZone: String) {
self.store = store
self.ckContainerId = ckContainerId
self.ckSharedContainerId = ckSharedContainerId
self.ckCoreDataZone = ckCoreDataZone
isCloudSyncingEnabled = store.canEnableCloudSyncing
@ -65,11 +73,23 @@ final class PersistenceManager: ObservableObject {
}
func loadVPNPersistence(withName containerName: String) -> VPNPersistence {
let persistence = VPNPersistence(withName: containerName, cloudKit: isCloudSyncingEnabled, author: persistenceAuthor)
let persistence = VPNPersistence(withName: containerName,
cloudKit: isCloudSyncingEnabled,
cloudKitIdentifier: nil,
author: persistenceAuthor)
vpnPersistence = persistence
return persistence
}
func loadSharedVPNPersistence(withName containerName: String) -> VPNPersistence {
let persistence = VPNPersistence(withName: containerName,
cloudKit: true,
cloudKitIdentifier: ckSharedContainerId,
author: persistenceAuthor)
sharedVPNPersistence = persistence
return persistence
}
func loadProvidersPersistence(withName containerName: String) -> ProvidersPersistence {
let persistence = ProvidersPersistence(withName: containerName, cloudKit: false, author: persistenceAuthor)
providersPersistence = persistence
@ -86,6 +106,10 @@ extension PersistenceManager {
fromContainerWithId: ckContainerId,
zoneId: .init(zoneName: ckCoreDataZone)
)
await Self.eraseCloudKitStore(
fromContainerWithId: ckSharedContainerId,
zoneId: .init(zoneName: ckCoreDataZone)
)
isErasingCloudKitStore = false
}
@ -109,7 +133,11 @@ private extension KeyValueStore {
}
private var isCloudKitSupported: Bool {
#if !os(tvOS)
cloudKitToken != nil
#else
true
#endif
}
var canEnableCloudSyncing: Bool {

View File

@ -33,6 +33,7 @@ struct PassepartoutApp: App {
@SceneBuilder var body: some Scene {
WindowGroup {
MainView()
#if !os(tvOS)
.withoutTitleBar()
.onIntentActivity(IntentDispatcher.connectVPN)
.onIntentActivity(IntentDispatcher.disableVPN)
@ -42,6 +43,7 @@ struct PassepartoutApp: App {
.onIntentActivity(IntentDispatcher.trustCurrentNetwork)
.onIntentActivity(IntentDispatcher.untrustCellularNetwork)
.onIntentActivity(IntentDispatcher.untrustCurrentNetwork)
#endif
}
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import SwiftUI
import UIKit
@ -38,3 +39,4 @@ struct ActivityView: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
}
}
#endif

View File

@ -180,7 +180,9 @@ private extension GenericCreditsView {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}.navigationTitle(content.name)
#if !os(tvOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Intents
import IntentsUI
import SwiftUI
@ -41,3 +42,4 @@ struct IntentAddView: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: INUIAddVoiceShortcutViewController, context: UIViewControllerRepresentableContext<IntentAddView>) {
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Intents
import IntentsUI
import SwiftUI
@ -41,3 +42,4 @@ struct IntentEditView: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: INUIEditVoiceShortcutViewController, context: UIViewControllerRepresentableContext<IntentEditView>) {
}
}
#endif

View File

@ -29,9 +29,15 @@ struct LongContentView: View {
@Binding var content: String
var body: some View {
#if !os(tvOS)
TextEditor(text: $content)
.font(.system(.body, design: .monospaced))
// .padding()
#else
Text(content)
.font(.system(.body, design: .monospaced))
// .padding()
#endif
// TODO: layout, add padding an inset, let content extend beyond safe areas
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import MessageUI
import SwiftUI
@ -82,3 +83,4 @@ extension MailComposerView {
}
}
}
#endif

View File

@ -75,17 +75,21 @@ public final class Reviewer: ObservableObject {
defaults.removeObject(forKey: Keys.eventCount)
defaults.set(currentVersion, forKey: Keys.lastVersion)
#if !os(tvOS)
requestReview()
#endif
return true
}
// may or may not appear
#if !os(tvOS)
private func requestReview() {
guard let scene = UIApplication.shared.connectedScenes.first(where: { $0 is UIWindowScene }) as? UIWindowScene else {
return
}
SKStoreReviewController.requestReview(in: scene)
}
#endif
public static func urlForReview(withAppId appId: String) -> URL {
URL(string: "https://apps.apple.com/app/id\(appId)?action=write-review")!

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Foundation
import Intents
@ -53,3 +54,4 @@ struct Shortcut: Identifiable, Hashable, Comparable {
native.invocationPhrase.lowercased()
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
@ -89,3 +90,4 @@ private extension ShortcutType {
)
}
}
#endif

View File

@ -33,10 +33,14 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
#if targetEnvironment(macCatalyst)
MacBundle.shared.utils.sendAppToBackground()
#endif
#if !os(tvOS)
rebuildShortcutItems()
#endif
}
#if !os(tvOS)
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
handleShortcutItem(shortcutItem)
}
#endif
}

View File

@ -67,6 +67,7 @@ struct DebugLogView: View {
refreshLog(scrollingToLatestWith: scrollProxy)
}
}
#if !os(tvOS)
#if targetEnvironment(macCatalyst)
.toolbar {
Button(action: copyDebugLog) {
@ -82,7 +83,9 @@ struct DebugLogView: View {
} else {
ProgressView()
}
}.sheet(isPresented: $isSharing, content: sharingActivityView)
}
.sheet(isPresented: $isSharing, content: sharingActivityView)
#endif
#endif
.edgesIgnoringSafeArea([.leading, .trailing])
.onReceive(timer, perform: refreshLog)
@ -104,9 +107,11 @@ private extension DebugLogView {
// TODO: layout, a slight padding would be nice, but it glitches on first touch
}
#if !os(tvOS)
func sharingActivityView() -> some View {
ActivityView(activityItems: sharingItems)
}
#endif
var sharingItems: [Any] {
let raw = logLines.joined(separator: "\n")
@ -140,6 +145,7 @@ private extension DebugLogView {
}
}
#if !os(tvOS)
func shareDebugLog() {
guard !logLines.isEmpty else {
assertionFailure("Log is empty, why could it share?")
@ -159,6 +165,7 @@ private extension DebugLogView {
Utils.copyToPasteboard(content)
}
#endif
func scrollToLatestUpdate(_ proxy: ScrollViewProxy) {
proxy.maybeScrollTo(logLines.count - 1, anchor: .bottomLeading)

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
import TunnelKitOpenVPN
@ -200,3 +201,4 @@ private extension DiagnosticsView.OpenVPNView {
}
}
}
#endif

View File

@ -32,7 +32,9 @@ struct DiagnosticsView: View {
var body: some View {
Group {
if !profile.isPlaceholder {
#if !os(tvOS)
vpnView
#endif
} else {
genericView
}
@ -41,6 +43,8 @@ struct DiagnosticsView: View {
}
private extension DiagnosticsView {
#if !os(tvOS)
var vpnView: some View {
Group {
switch profile.currentVPNProtocol {
@ -56,6 +60,7 @@ private extension DiagnosticsView {
}
}
}
#endif
var genericView: some View {
List {

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
import TunnelKitCore
@ -111,3 +112,4 @@ private extension EndpointView.AddView {
presentationMode.wrappedValue.dismiss()
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
import TunnelKitOpenVPN
@ -394,3 +395,4 @@ private extension Profile {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
import TunnelKitWireGuard
@ -155,3 +156,4 @@ private extension ObservableProfile {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
@ -48,3 +49,4 @@ struct EndpointView: View {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import SwiftUI
struct MainView: View {
@ -33,3 +34,4 @@ struct MainView: View {
}.themeGlobal()
}
}
#endif

View File

@ -137,8 +137,10 @@ private extension OnDemandView {
guard isEligibleForSiri else {
return
}
#if !os(tvOS)
IntentDispatcher.donateTrustCellularNetwork()
IntentDispatcher.donateUntrustCellularNetwork()
#endif
}
// eligibility: donate intents if eligible for Siri
@ -146,7 +148,9 @@ private extension OnDemandView {
guard isEligibleForSiri else {
return
}
#if !os(tvOS)
IntentDispatcher.donateTrustCurrentNetwork()
IntentDispatcher.donateUntrustCurrentNetwork()
#endif
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
@ -56,6 +57,7 @@ extension OrganizerView {
VPNToggle(
profile: profile,
interactiveProfile: interactiveProfile,
title: L10n.Global.Strings.enabled,
rateLimit: Constants.RateLimit.vpnToggle
).labelsHidden()
}.padding([.top, .bottom], 10)
@ -81,3 +83,4 @@ private extension OrganizerView.ProfileRow {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
@ -187,3 +188,4 @@ private extension OrganizerView.ProfilesList {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
@ -84,3 +85,4 @@ private extension OrganizerView.SceneView {
}
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import PassepartoutLibrary
import SwiftUI
@ -89,12 +90,14 @@ struct OrganizerView: View {
presenting: alertType,
actions: alertActions,
message: alertMessage
).fileImporter(
)
.fileImporter(
isPresented: $isHostFileImporterPresented,
allowedContentTypes: hostFileTypes,
allowsMultipleSelection: false,
onCompletion: onHostFileImporterResult
).onOpenURL(perform: onOpenURL)
)
.onOpenURL(perform: onOpenURL)
.themePrimaryView()
}
}
@ -171,3 +174,4 @@ private extension OrganizerView {
addProfileModalType = .addHost(url, false)
}
}
#endif

View File

@ -44,6 +44,8 @@ extension PaywallView {
@State private var purchaseState: PurchaseState?
@State private var didPurchaseAppleTV = false
init(isPresented: Binding<Bool>, feature: LocalProduct? = nil) {
productManager = .shared
_isPresented = isPresented
@ -52,12 +54,22 @@ extension PaywallView {
var body: some View {
List {
featuresSection
skFullVersion.map {
fullFeaturesSection(withHeader: $0.localizedTitle)
}
purchaseSection
.disabled(purchaseState != nil)
restoreSection
.disabled(purchaseState != nil)
}.navigationTitle(Unlocalized.appName)
}
.navigationTitle(Unlocalized.appName)
.alert(Unlocalized.Other.appleTV, isPresented: $didPurchaseAppleTV) {
Button(L10n.Global.Strings.ok) {
isPresented = false
}
} message: {
Text(L10n.Paywall.Alerts.Purchase.Appletv.Success.message)
}
// reloading
.task {
@ -121,9 +133,9 @@ private struct PurchaseRow: View {
// MARK: -
private extension PaywallView.PurchaseView {
var featuresSection: some View {
func fullFeaturesSection(withHeader header: String) -> some View {
Section {
ForEach(features) { feature in
ForEach(fullFeatures) { feature in
VStack(alignment: .leading) {
Text(feature.title)
.themeCellTitleStyle()
@ -134,6 +146,8 @@ private extension PaywallView.PurchaseView {
}
}
}
} header: {
Text(header)
}
}
@ -183,6 +197,9 @@ private extension PaywallView.PurchaseView {
// hide full version if already bought the other platform version
var skFullVersion: InAppProduct? {
guard !productManager.isFullVersion() else {
return nil
}
#if targetEnvironment(macCatalyst)
guard !productManager.hasPurchased(.fullVersion_iOS) else {
return nil
@ -195,17 +212,25 @@ private extension PaywallView.PurchaseView {
return productManager.product(withIdentifier: .fullVersion)
}
var features: [FeatureModel] {
var skAppleTV: InAppProduct? {
guard feature == .appleTV else {
return nil
}
return productManager.product(withIdentifier: .appleTV)
}
var fullFeatures: [FeatureModel] {
productManager.featureProducts(excluding: {
$0 == .fullVersion || $0.isPlatformVersion
$0 == .fullVersion || $0 == .appleTV || $0.isLegacyPlatformVersion
})
.map {
FeatureModel(product: $0)
}
.sorted()
}
var productRowModels: [InAppProduct] {
[skFullVersion]
[skFullVersion, skAppleTV]
.compactMap { $0 }
}
}
@ -249,16 +274,25 @@ private extension PurchaseRow {
// MARK: -
// IMPORTANT: resync shared profiles after purchasing Apple TV feature (drop time limit)
private extension PaywallView.PurchaseView {
func purchaseProduct(_ product: InAppProduct) {
purchaseState = .purchasing(product)
Task {
do {
let wasEligibleForAppleTV = productManager.isEligible(forFeature: .appleTV)
let result = try await productManager.purchase(product)
switch result {
case .done:
isPresented = false
if !wasEligibleForAppleTV && productManager.isEligible(forFeature: .appleTV) {
ProfileManager.shared.refreshSharedProfiles()
didPurchaseAppleTV = true
} else {
isPresented = false
}
case .cancelled:
break
@ -281,8 +315,16 @@ private extension PaywallView.PurchaseView {
Task {
do {
let wasEligibleForAppleTV = productManager.isEligible(forFeature: .appleTV)
try await productManager.restorePurchases()
isPresented = false
if !wasEligibleForAppleTV && productManager.isEligible(forFeature: .appleTV) {
ProfileManager.shared.refreshSharedProfiles()
didPurchaseAppleTV = true
} else {
isPresented = false
}
purchaseState = nil
} catch {
pp_log.error("Unable to restore purchases: \(error)")

View File

@ -53,11 +53,13 @@ extension ProfileView {
Label(L10n.Global.Strings.protocol, systemImage: themeVPNProtocolImage)
.withTrailingText(currentProfile.value.currentVPNProtocol.description)
}
#if !os(tvOS)
NavigationLink {
EndpointView(currentProfile: currentProfile)
} label: {
Label(L10n.Global.Strings.endpoint, systemImage: themeEndpointImage)
}
#endif
if currentProfile.value.requiresCredentials {
NavigationLink {
AccountView(

View File

@ -96,6 +96,7 @@ extension ProfileView {
}
}
#if !os(tvOS)
struct ShortcutsButton: View {
@ObservedObject private var productManager: ProductManager
@ -114,6 +115,7 @@ extension ProfileView {
}
}
}
#endif
struct RenameButton: View {
@Binding private var modalType: ModalType?
@ -164,9 +166,11 @@ private extension ProfileView.MainMenu {
var mainView: some View {
Menu {
ProfileView.ReconnectButton()
#if !os(tvOS)
ProfileView.ShortcutsButton(
modalType: $modalType
)
#endif
Divider()
ProfileView.RenameButton(
modalType: $modalType
@ -238,11 +242,13 @@ private extension ProfileView.MainMenu {
}
}
#if !os(tvOS)
private extension ProfileView.ShortcutsButton {
var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
}
#endif
// MARK: -
@ -260,6 +266,7 @@ private extension ProfileView.MainMenu {
}
}
#if !os(tvOS)
private extension ProfileView.ShortcutsButton {
func presentShortcutsOrPaywall() {
@ -271,6 +278,7 @@ private extension ProfileView.ShortcutsButton {
}
}
}
#endif
private extension ProfileView.DuplicateButton {
func duplicateProfile(withId id: UUID) {

View File

@ -0,0 +1,95 @@
//
// ProfileView+TV.swift
// Passepartout
//
// Created by Davide De Rosa on 12/22/23.
// 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 PassepartoutLibrary
import SwiftUI
extension ProfileView {
struct TVSection: View {
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var productManager: ProductManager
@ObservedObject private var currentProfile: ObservableProfile
@Binding private var isProfileShared: Bool
@Binding private var modalType: ModalType?
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
let profileManager: ProfileManager = .shared
self.profileManager = profileManager
productManager = .shared
self.currentProfile = currentProfile
_isProfileShared = Binding {
profileManager.isSharing(profile: currentProfile.value)
} set: {
profileManager.setSharing($0, profile: currentProfile.value)
}
_modalType = modalType
}
var body: some View {
Section {
Toggle(isOn: $isProfileShared) {
Label(shareText, systemImage: themeAppleTVImage)
}
// eligibility: present paywall for full support for Apple TV
if !isEligibleForAppleTV {
Button(L10n.Paywall.title) {
modalType = .paywallAppleTV
}
}
} footer: {
Text(footerText)
}
}
}
}
private extension ProfileView.TVSection {
var isEligibleForAppleTV: Bool {
productManager.isEligible(forFeature: .appleTV)
}
var shareText: String {
var sentences: [String] = [Unlocalized.Other.appleTV]
if !isEligibleForAppleTV {
sentences.append(L10n.Profile.Items.TvSharing.Caption.limited(Constants.InApp.tvLimitedMinutes))
}
return sentences.joined(separator: "")
}
var footerText: String {
var sentences: [String] = [L10n.Profile.Sections.Tv.Footer.encryption]
if !isEligibleForAppleTV {
sentences.append(L10n.Profile.Sections.Tv.Footer.Restricted.p1(Constants.InApp.tvLimitedMinutes))
sentences.append(L10n.Profile.Sections.Tv.Footer.Restricted.p2)
}
return sentences.joined(separator: " ")
}
}

View File

@ -73,6 +73,7 @@ private extension ProfileView.VPNSection {
VPNToggle(
profile: profile,
interactiveProfile: interactiveProfile,
title: L10n.Global.Strings.enabled,
rateLimit: Constants.RateLimit.vpnToggle
)
}

View File

@ -30,7 +30,9 @@ struct ProfileView: View {
enum ModalType: Int, Identifiable {
case interactiveAccount
#if !os(tvOS)
case shortcuts
#endif
case rename
@ -40,6 +42,8 @@ struct ProfileView: View {
case paywallTrustedNetworks
case paywallAppleTV
var id: Int {
rawValue
}
@ -104,6 +108,10 @@ private extension ProfileView {
currentProfile: currentProfile,
modalType: $modalType
)
TVSection(
currentProfile: currentProfile,
modalType: $modalType
)
ExtraSection(currentProfile: currentProfile)
Section {
DiagnosticsRow(currentProfile: currentProfile)
@ -122,10 +130,12 @@ private extension ProfileView {
InteractiveConnectionView(profile: currentProfile.value)
}.themeGlobal()
#if !os(tvOS)
case .shortcuts:
NavigationView {
ShortcutsView(target: currentProfile.value)
}.themeGlobal()
#endif
case .rename:
NavigationView {
@ -155,6 +165,14 @@ private extension ProfileView {
feature: .trustedNetworks
)
}.themeGlobal()
case .paywallAppleTV:
NavigationView {
PaywallView(
modalType: $modalType,
feature: .appleTV
)
}.themeGlobal()
}
}
}

View File

@ -168,9 +168,11 @@ private extension ProviderLocationView {
ForEach(filteredLocations(for: category)) { location in
if isEditable {
locationRow(location)
#if !os(tvOS)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
favoriteActions(location)
}
#endif
} else {
locationRow(location)
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import MessageUI
import PassepartoutLibrary
import SwiftUI
@ -88,3 +89,4 @@ struct ReportIssueView: View {
)
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Intents
import PassepartoutLibrary
import SwiftUI
@ -162,3 +163,4 @@ private extension ShortcutsView.AddView {
pendingShortcut = shortcut
}
}
#endif

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if !os(tvOS)
import Intents
import PassepartoutLibrary
import SwiftUI
@ -176,3 +177,4 @@ private extension ShortcutsView {
modalType = .add(shortcut: shortcut)
}
}
#endif

View File

@ -0,0 +1,118 @@
//
// ActiveProfileView+TV.swift
// Passepartout
//
// Created by Davide De Rosa on 12/18/23.
// 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/>.
//
#if os(tvOS)
import PassepartoutLibrary
import SwiftUI
struct ActiveProfileView: View {
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var vpnState: ObservableVPNState
init(profileManager: ProfileManager) {
self.profileManager = profileManager
vpnState = .shared
}
var body: some View {
Section {
let activeProfile = profileManager.activeProfile
nameView(for: activeProfile)
if let activeProfile {
toggleView(for: activeProfile)
vpnProtocolView(for: activeProfile)
statusView(for: activeProfile)
if let expirationDate = activeProfile.connectionExpirationDate {
expirationView(at: expirationDate)
}
}
}
if let activeProfile = profileManager.activeProfile,
let server = activeProfile.providerServer(.shared) {
providerSection(with: server)
}
}
}
private extension ActiveProfileView {
func nameView(for profile: Profile?) -> some View {
NavigationLink {
ProfilesListView(profileManager: profileManager)
} label: {
Text(L10n.Global.Placeholders.profileName)
.withTrailingText(profile?.header.name)
}
}
func vpnProtocolView(for profile: Profile) -> some View {
Text(L10n.Global.Strings.protocol)
.withTrailingText(profile.currentVPNProtocol.description)
}
func toggleView(for profile: Profile) -> some View {
VPNToggle(
profile: profile,
interactiveProfile: .constant(nil),
title: L10n.Profile.Items.ConnectionStatus.caption,
rateLimit: Constants.RateLimit.vpnToggle
)
}
func statusView(for activeProfile: Profile) -> some View {
HStack {
Text(Unlocalized.VPN.vpn)
Spacer()
if vpnState.isEnabled && activeProfile.isExpired {
Text(L10n.Global.Errors.tunnelExpired)
.themeSecondaryTextStyle()
} else {
VPNStatusText(isActiveProfile: true)
.themeSecondaryTextStyle()
}
}
}
func expirationView(at expirationDate: Date) -> some View {
Text(L10n.Profile.Items.ExpiresAt.caption)
.withTrailingText(expirationDate.timestamp)
}
func providerSection(with server: ProviderServer) -> some View {
Section {
Text(L10n.Global.Strings.name)
.withTrailingText(server.providerMetadata.fullName)
HStack {
Text(L10n.Provider.Location.title)
Spacer()
Label(server.localizedDescription(style: .country), image: themeAssetsCountryImage(server.countryCode))
.themeSecondaryTextStyle()
}
} header: {
Text(L10n.Global.Strings.provider)
}
}
}
#endif

View File

@ -0,0 +1,43 @@
//
// MainView+TV.swift
// Passepartout
//
// Created by Davide De Rosa on 12/17/23.
// 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/>.
//
#if os(tvOS)
import SwiftUI
struct MainView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
OrganizerView()
}
.themeGlobal()
}
}
#Preview {
MainView()
}
#endif

View File

@ -0,0 +1,163 @@
//
// OrganizerView+TV.swift
// Passepartout
//
// Created by Davide De Rosa on 12/17/23.
// 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/>.
//
#if os(tvOS)
import PassepartoutLibrary
import SwiftUI
struct OrganizerView: View {
@ObservedObject private var profileManager: ProfileManager
#if targetEnvironment(simulator)
@State private var didLoadMockProfiles = false
#endif
init(profileManager: ProfileManager = .shared) {
self.profileManager = profileManager
}
var body: some View {
List {
ActiveProfileView(profileManager: profileManager)
aboutSection
}
.navigationTitle(Unlocalized.appName)
.themeTV()
.themeAnimation(on: profileManager.activeProfileId)
#if targetEnvironment(simulator)
.task {
await loadMockProfiles()
}
#endif
}
}
private extension OrganizerView {
var aboutSection: some View {
Section {
Text(L10n.Version.title)
.withTrailingText(Constants.Global.appVersionString)
} header: {
Text(L10n.About.title)
}
}
}
// MARK: Mock
#if targetEnvironment(simulator)
import TunnelKitOpenVPN
import TunnelKitWireGuard
// poor man's preview:
//
// https://developer.apple.com/forums/thread/719078
private let mockHosts: [(String, VPNProtocolType)] = [
("My Profile", .wireGuard),
("Friend's House", .openVPN),
("At School", .wireGuard)
]
private let mockProviders: [ProviderName] = [
.hideme,
.pia,
.protonvpn
]
@MainActor
private let mockRepository: ProfileRepository = {
let hostProfiles = mockHosts.map { name, vpnType in
let header = Profile.Header(name: name)
switch vpnType {
case .openVPN:
let ovpn = OpenVPN.ConfigurationBuilder()
return Profile(header, configuration: ovpn.build())
case .wireGuard:
let wg = WireGuard.ConfigurationBuilder()
return Profile(header, configuration: wg.build())
}
}
let providerProfiles = mockProviders.map { providerName in
let manager = ProviderManager.shared
let metadata = manager.provider(withName: providerName)!
let header = Profile.Header(name: metadata.fullName, providerName: providerName)
var provider = Profile.Provider(providerName)
let vpnType: VPNProtocolType = .openVPN // isOpenVPN ? .openVPN : .wireGuard
var settings = Profile.Provider.Settings()
let anyServer = manager.anyDefaultServer(providerName, vpnProtocol: vpnType)
settings.serverId = anyServer?.id
settings.presetId = anyServer?.presetIds.first
settings.account = .init("hello", "world")
provider.vpnSettings[vpnType] = settings
return Profile(header, provider: provider)
}
var profiles: [Profile] = []
profiles.append(contentsOf: hostProfiles)
profiles.append(contentsOf: providerProfiles)
let repo = MockProfileRepository()
try? repo.saveProfiles(profiles.map {
var copy = $0
copy.connectionExpirationDate = Date().addingTimeInterval(10.0)
return copy
})
return repo
}()
private extension OrganizerView {
func loadMockProfiles() async {
guard !didLoadMockProfiles else {
return
}
do {
let providerManager: ProviderManager = .shared
try await providerManager.fetchProvidersIndexPublisher(priority: .bundle).async()
for name in mockProviders {
try? await providerManager.fetchProviderPublisher(withName: name, vpnProtocol: .openVPN, priority: .bundle).async()
}
profileManager.swapProfileRepository(mockRepository)
profileManager.activateProfile(mockRepository.allProfiles().first!.value)
didLoadMockProfiles = true
} catch {
ErrorHandler.shared.handle(AppError(error))
}
}
}
#Preview {
NavigationStack {
OrganizerView()
}
}
#endif
#endif

View File

@ -0,0 +1,113 @@
//
// ProfilesList+TV.swift
// Passepartout
//
// Created by Davide De Rosa on 12/18/23.
// 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/>.
//
#if os(tvOS)
import PassepartoutLibrary
import SwiftUI
struct ProfilesListView: View {
@ObservedObject private var profileManager: ProfileManager
@Environment(\.presentationMode) private var presentationMode
@State private var profileIdPendingSelection: UUID?
@FocusState private var focusedProfileId: UUID?
init(profileManager: ProfileManager) {
self.profileManager = profileManager
}
var body: some View {
List {
Section {
Text(listHeader)
.font(.footnote)
.themeSecondaryTextStyle()
ForEach(profiles, content: profileRow)
.themeAnimation(on: profiles)
}
}
.onAppear {
focusedProfileId = profileManager.activeProfileId
}
.disabled(profileIdPendingSelection != nil)
.navigationTitle(Text(L10n.Global.Strings.profiles))
.themeTV()
}
}
private extension ProfilesListView {
var profiles: [Profile] {
profileManager.profiles.sorted()
}
var listHeader: String {
[
L10n.Organizer.Sections.Tv.ProfilesList.Header.p1,
L10n.Profile.Sections.Tv.Footer.encryption
]
.joined(separator: " ")
}
func profileRow(for profile: Profile) -> some View {
Button {
activateProfile(profile)
} label: {
HStack {
Text(profile.header.name)
Spacer()
if profile.header.id == profileIdPendingSelection {
ProgressView()
} else if profileManager.isActiveProfile(profile.header.id) {
themeCheckmarkImage.asSystemImage
}
}
}
.focused($focusedProfileId, equals: profile.id)
}
func activateProfile(_ profile: Profile) {
guard profile.id != profileManager.activeProfileId else {
presentationMode.wrappedValue.dismiss()
return
}
Task {
profileIdPendingSelection = profile.id
do {
try await profileManager.makeProfileReady(profile)
await VPNManager.shared.disable()
profileManager.activateProfile(profile)
presentationMode.wrappedValue.dismiss()
} catch {
ErrorHandler.shared.handle(
title: L10n.Global.Strings.profiles,
message: AppError(error).localizedDescription
)
}
}
}
}
#endif

View File

@ -39,22 +39,25 @@ struct VPNToggle: View {
@Binding private var interactiveProfile: Profile?
private let title: String
private let rateLimit: Int
@State private var canToggle = true
init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
init(profile: Profile, interactiveProfile: Binding<Profile?>, title: String, rateLimit: Int) {
profileManager = .shared
vpnManager = .shared
currentVPNState = .shared
productManager = .shared
self.profile = profile
_interactiveProfile = interactiveProfile
self.title = title
self.rateLimit = rateLimit
}
var body: some View {
Toggle(L10n.Global.Strings.enabled, isOn: isEnabled)
Toggle(title, isOn: isEnabled)
.disabled(!canToggle)
.themeAnimation(on: currentVPNState.isEnabled)
}
@ -129,11 +132,13 @@ private extension VPNToggle {
pp_log.debug("Donating connection intents...")
#if !os(tvOS)
IntentDispatcher.donateEnableVPN()
IntentDispatcher.donateDisableVPN()
IntentDispatcher.donateConnection(
with: profile,
providerManager: ProviderManager.shared
)
#endif
}
}

View File

@ -71,9 +71,11 @@ extension View {
.truncationMode(truncationMode)
if copyOnTap {
trailing
#if !os(tvOS)
.onTapGesture {
text.map(Utils.copyToPasteboard)
}
#endif
} else {
trailing
}

View File

@ -72,7 +72,7 @@
"global.strings.authentication" = "Authentifizierung";
"global.messages.unlock_app" = "Passepartout ist gesperrt";
"global.messages.email_not_configured" = "Es wurde kein Email-Account konfiguriert.";
"global.messages.share" = "Passepartout ist ein Benutzerfreundlicher, Open Source OpenVPN / WireGuard client für iOS und macOS";
"global.messages.share" = "Passepartout ist ein Benutzerfreundlicher, Open Source OpenVPN / WireGuard client für iOS und macOS"; // FIXME: l10n, Apple platforms
"global.alerts.buttons.remind" = "Später erinnern";
"global.alerts.buttons.never" = "Nicht erneut fragen";
@ -83,6 +83,7 @@
"global.errors.missing_account" = "Fehlender Account";
"global.errors.missing_provider_server" = "Fehlender Standort";
"global.errors.missing_provider_preset" = "Fehlende Voreinstellung";
"global.errors.tunnel_expired" = "Verbindung abgelaufen";
/* MARK: Menus */
@ -132,6 +133,7 @@
/* MARK: OrganizerView */
"organizer.sections.active" = "In Benutzung";
"organizer.sections.tv.profiles_list.header.p1" = "Öffne Passepartout auf deinem iOS- oder macOS-Gerät und aktiviere den „Apple TV“-Schalter eines Profils, damit es hier angezeigt wird.";
/* MARK: OrganizerView */
"organizer.empty.no_profiles" = "Keine Profile";
@ -163,6 +165,9 @@
"profile.sections.vpn.footer" = "Die Verbindung wird immer aufgebaut wenn notwendig.";
"profile.sections.status.header" = "Verbindung";
"profile.sections.provider_infrastructure.footer" = "Zuletzt aktualisiert am %@.";
"profile.sections.tv.footer.encryption" = "Profile sind verschlüsselt und werden deinem Apple TV über iCloud zur Verfügung gestellt.";
"profile.sections.tv.footer.restricted.p1" = "Die Verbindung läuft jedoch nach %d Minuten ab.";
"profile.sections.tv.footer.restricted.p2" = "Bei einem Kauf entfällt diese Beschränkung.";
"profile.sections.vpn_survives_sleep.footer" = "Deaktivieren um die Batterielaufzeit zu verbessern, allerdings verzögert sich der Verbindungsaufbau beim Aufwachen.";
"profile.sections.vpn_resolves_hostname.footer" = "Bevorzugt in den meisten Netzwerken und benötigt in manchen IPv6 Netzwerken. Deaktivieren wo DNS geblockt ist oder um die Aushandlung zu beschleunigen bei langsam antwortenden DNS.";
"profile.sections.feedback.header" = "Feedback";
@ -178,6 +183,8 @@
"profile.items.only_shows_favorites.caption" = "Nur favorisierte Standorte anzeigen";
"profile.items.vpn_survives_sleep.caption" = "Verbindung aktiv halten trotz Schlafmodus";
"profile.items.vpn_resolves_hostname.caption" = "Server Hostname auflösen";
"profile.items.tv_sharing.caption.limited" = "Begrenzt auf %d Minuten";
"profile.items.expires_at.caption" = "Endet";
"profile.alerts.rename.title" = "Profil umbenennen";
"profile.alerts.reconnect_vpn.message" = "Möchtest du erneut zum VPN verbinden?";
@ -323,6 +330,7 @@
"paywall.items.full_version.extra_description" = "Alle Anbieter (inklusive Zukünftige)\n%@";
"paywall.items.restore.title" = "Einkäufe wiederherstellen";
"paywall.items.restore.description" = "Wenn Sie diese App oder Funktion in der Vergangenheit gekauft haben, können Sie Ihre Einkäufe wiederherstellen und dieser Bildschirm wird nicht mehr angezeigt.";
"paywall.alerts.purchase.appletv.success.message" = "Vielen Dank! Das Zeitlimit entfällt, sobald iCloud auf dem neuesten Stand ist. Warte einen Moment und starte die Verbindung in der TV-App dann neu.";
/* MARK: DonateView */

View File

@ -72,7 +72,7 @@
"global.strings.authentication" = "Αυθεντικοποίηση";
"global.messages.unlock_app" = "Το πασπαρτού είναι κλειδωμένο";
"global.messages.email_not_configured" = "Δεν έχει ρυθμιστεί λογαριασμός ηλεκτρονικού ταχυδρομείου.";
"global.messages.share" = "Το Passepartout είναι φιλικό προς το χρήστη, ανοιχτού κώδικα OpenVPN / WireGuard πρόγραμμα για iOS και macOS";
"global.messages.share" = "Το Passepartout είναι φιλικό προς το χρήστη, ανοιχτού κώδικα OpenVPN / WireGuard πρόγραμμα για iOS και macOS"; // FIXME: l10n, Apple platforms
"global.alerts.buttons.remind" = "Υπενθύμιση Αργότερα";
"global.alerts.buttons.never" = "Μη με ρωτήσεις ξανά";
@ -83,6 +83,7 @@
"global.errors.missing_account" = "Λείπει λογαριασμός";
"global.errors.missing_provider_server" = "Λείπει η τοποθεσία";
"global.errors.missing_provider_preset" = "Λείπει προεπιλογή";
"global.errors.tunnel_expired" = "H σύνδεση έληξε";
/* MARK: Menus */
@ -132,6 +133,7 @@
/* MARK: OrganizerView */
"organizer.sections.active" = "Σε χρήση";
"organizer.sections.tv.profiles_list.header.p1" = "Ανοίξτε το Passepartout στη συσκευή σας iOS ή macOS και ενεργοποιήστε την εναλλαγή \"Apple TV\" ενός προφίλ για να εμφανιστεί εδώ.";
/* MARK: OrganizerView */
"organizer.empty.no_profiles" = "Δεν υπάρχουν προφίλ";
@ -163,6 +165,9 @@
"profile.sections.vpn.footer" = "Η σύνδεση θα πραγματοποιηθεί όποτε είναι απαραίτητο.";
"profile.sections.status.header" = "Σύνδεση";
"profile.sections.provider_infrastructure.footer" = "Τελευταία ενημέρωση στις %@.";
"profile.sections.tv.footer.encryption" = "Τα προφίλ κρυπτογραφούνται και διατίθενται στο Apple TV σας μέσω iCloud.";
"profile.sections.tv.footer.restricted.p1" = "Ωστόσο, η σύνδεση θα λήξει μετά από %d λεπτά.";
"profile.sections.tv.footer.restricted.p2" = "Αγορά για την κατάργηση του περιορισμού.";
"profile.sections.vpn_survives_sleep.footer" = "Απενεργοποιήστε για να βελτιώσετε τη χρήση της μπαταρίας, εις βάρος των περιστασιακών επιβραδύνσεων που οφείλονται σε επανασύνδεση αφύπνισης.";
"profile.sections.vpn_resolves_hostname.footer" = "Προτιμάται στα περισσότερα δίκτυα και απαιτείται σε ορισμένα δίκτυα IPv6. Απενεργοποιήστε το εκεί που μπλοκάρεται το DNS ή για να επιταχύνετε τη επικοινωνία όταν το DNS είναι αργό για να ανταποκριθεί.";
"profile.sections.feedback.header" = "Ανατροφοδότηση";
@ -178,6 +183,8 @@
"profile.items.only_shows_favorites.caption" = "Προβολή αγαπημένων τοποθεσιών μόνο";
"profile.items.vpn_survives_sleep.caption" = "Κρατήστε ζωντανό στον ύπνο";
"profile.items.vpn_resolves_hostname.caption" = "Επίλυση του ονόματος σέρβερ διακομιστή";
"profile.items.tv_sharing.caption.limited" = "Περιορίστηκε σε %d λεπτά";
"profile.items.expires_at.caption" = "Λήξη";
"profile.alerts.rename.title" = "Μετονομασία προφίλ";
"profile.alerts.reconnect_vpn.message" = "Θέλετε να συνδεθείτε ξανά με το VPN;";
@ -323,6 +330,7 @@
"paywall.items.full_version.extra_description" = "Όλοι οι πάροχοι (περιλαμβάνονται και οι μελλοντικοί)\n%@";
"paywall.items.restore.title" = "Επαναφορά Αγορών";
"paywall.items.restore.description" = "Εαν αγοράσατε την εφαρμογή στο παρελθόν, μπορείτε να κάνετε επαναφορά αγορών και αυτή η οθόνη δε θα εμφανιστεί ξανά.";
"paywall.alerts.purchase.appletv.success.message" = "Σας ευχαριστούμε! Το χρονικό όριο θα αφαιρεθεί μόλις ενημερωθεί το iCloud. Περιμένετε μερικά λεπτά και μετά επανεκκινήστε τη σύνδεση στην εφαρμογή TV.";
/* MARK: DonateView */

View File

@ -72,7 +72,7 @@
"global.strings.authentication" = "Authentication";
"global.messages.unlock_app" = "Passepartout is locked";
"global.messages.email_not_configured" = "No e-mail account is configured.";
"global.messages.share" = "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS";
"global.messages.share" = "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS"; // FIXME: l10n, Apple platforms
"global.alerts.buttons.remind" = "Remind me later";
"global.alerts.buttons.never" = "Don't ask again";
@ -83,6 +83,7 @@
"global.errors.missing_account" = "Missing account";
"global.errors.missing_provider_server" = "Missing location";
"global.errors.missing_provider_preset" = "Missing preset";
"global.errors.tunnel_expired" = "Connection expired";
/* MARK: Menus */
@ -132,6 +133,7 @@
/* MARK: OrganizerView */
"organizer.sections.active" = "In use";
"organizer.sections.tv.profiles_list.header.p1" = "Open Passepartout on your iOS or macOS device and enable the \"Apple TV\" toggle of a profile to make it appear here.";
/* MARK: OrganizerView */
"organizer.empty.no_profiles" = "No profiles";
@ -163,6 +165,9 @@
"profile.sections.vpn.footer" = "The connection will be established whenever necessary.";
"profile.sections.status.header" = "Connection";
"profile.sections.provider_infrastructure.footer" = "Last updated on %@.";
"profile.sections.tv.footer.encryption" = "Profiles are encrypted and made available to your Apple TV via iCloud.";
"profile.sections.tv.footer.restricted.p1" = "However, the connection will expire after %d minutes.";
"profile.sections.tv.footer.restricted.p2" = "Purchase to drop the restriction.";
"profile.sections.vpn_survives_sleep.footer" = "Disable to improve battery usage, at the expense of occasional slowdowns due to wake-up reconnections.";
"profile.sections.vpn_resolves_hostname.footer" = "Preferred in most networks and required in some IPv6 networks. Disable where DNS is blocked, or to speed up negotiation when DNS is slow to respond.";
"profile.sections.feedback.header" = "Feedback";
@ -178,6 +183,8 @@
"profile.items.only_shows_favorites.caption" = "Only show favorite locations";
"profile.items.vpn_survives_sleep.caption" = "Keep alive on sleep";
"profile.items.vpn_resolves_hostname.caption" = "Resolve provider hostname";
"profile.items.tv_sharing.caption.limited" = "Limited to %d minutes";
"profile.items.expires_at.caption" = "Expiration";
"profile.alerts.rename.title" = "Rename profile";
"profile.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?";
@ -323,6 +330,7 @@
"paywall.items.full_version.extra_description" = "All providers (including future ones)\n%@";
"paywall.items.restore.title" = "Restore purchases";
"paywall.items.restore.description" = "If you bought this app or feature in the past, you can restore your purchases and this screen won't show again.";
"paywall.alerts.purchase.appletv.success.message" = "Thank you! The time limit will be dropped as soon as iCloud catches up. Wait a few moments, then restart the connection on the TV app.";
/* MARK: DonateView */

View File

@ -72,7 +72,7 @@
"global.strings.authentication" = "Autenticación";
"global.messages.unlock_app" = "Passepartout está bloqueada";
"global.messages.email_not_configured" = "Ningún e-mail configurado.";
"global.messages.share" = "Passepartout es un cliente OpenVPN / WireGuard intuitivo, de código abierto para iOS y macOS";
"global.messages.share" = "Passepartout es un cliente OpenVPN / WireGuard intuitivo, de código abierto para iOS y macOS"; // FIXME: l10n, Apple platforms
"global.alerts.buttons.remind" = "Recordar más tarde";
"global.alerts.buttons.never" = "No preguntar más";
@ -83,6 +83,7 @@
"global.errors.missing_account" = "Sin cuenta";
"global.errors.missing_provider_server" = "Sin ubicación";
"global.errors.missing_provider_preset" = "Sin ajuste";
"global.errors.tunnel_expired" = "Conexión caducada";
/* MARK: Menus */
@ -132,6 +133,7 @@
/* MARK: OrganizerView */
"organizer.sections.active" = "En uso";
"organizer.sections.tv.profiles_list.header.p1" = "Abre Passepartout desde tu dispositivo iOS o macOS y habilita el switch \"Apple TV\" de un perfil para que aparezca aquí abajo.";
/* MARK: OrganizerView */
"organizer.empty.no_profiles" = "Ningún perfil";
@ -163,6 +165,9 @@
"profile.sections.vpn.footer" = "La conexión se establecerá siempre y cuando sea necesario.";
"profile.sections.status.header" = "Conexión";
"profile.sections.provider_infrastructure.footer" = "Última actualización: %@.";
"profile.sections.tv.footer.encryption" = "Los perfiles están encriptados y puestos a disposición para tu Apple TV a través de iCloud.";
"profile.sections.tv.footer.restricted.p1" = "Sin embargo, la conexión caducará en %d minutos.";
"profile.sections.tv.footer.restricted.p2" = "Comprar para eliminar la restricción.";
"profile.sections.vpn_survives_sleep.footer" = "Deshabilitar para mejorar el uso de la batería, a costa de ralentizaciones ocasionales por las reconexiones al despertar el dispositivo.";
"profile.sections.vpn_resolves_hostname.footer" = "Preferido en la mayoría de las redes y necesario en algunas redes IPv6. Deshabilitar donde el DNS esté bloqueado, o para acelerar la negociación cuando el DNS sea lento en responder.";
"profile.sections.feedback.header" = "Feedback";
@ -178,6 +183,8 @@
"profile.items.only_shows_favorites.caption" = "Mostrar solo ubicaciones favoritas";
"profile.items.vpn_survives_sleep.caption" = "Mantener en modo inactivo";
"profile.items.vpn_resolves_hostname.caption" = "Resolver hostname del servidor";
"profile.items.tv_sharing.caption.limited" = "Limitado a %d minutos";
"profile.items.expires_at.caption" = "Caducidad";
"profile.alerts.rename.title" = "Renombrar perfil";
"profile.alerts.reconnect_vpn.message" = "Quieres reconectarte al VPN?";
@ -323,6 +330,7 @@
"paywall.items.full_version.extra_description" = "Todos los proveedores (incluye los futuros)\n%@";
"paywall.items.restore.title" = "Restaurar compras";
"paywall.items.restore.description" = "Si compraste esta aplicación o funcionalidad anteriormente, puedes restaurar tus compras y esta pantalla no volverá a aparecer.";
"paywall.alerts.purchase.appletv.success.message" = "Gracias! El limite de tiempo será eliminado en cuanto iCloud se sincronice. Espera unos momentos, y reinicia la conexión en la app de la TV.";
/* MARK: DonateView */

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1 +1,3 @@
Passepartout is a bare VPN client and, as such, does not store nor forward any personal data. Therefore, you should get in touch with your VPN service provider to learn about its data retention policy.
Passepartout may access GitHub repositories on request. Please refer to GitHub privacy statement for any information about data retention when accessing their website.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

View File

@ -1,4 +1,9 @@
### Fixed
### Added
- Persisted profile is overwritten with its former value.
- WireGuard: Show data count.
### Changed
- Upgrade OpenSSL to 3.2.0.
- Encrypt profiles stored to iCloud.

Some files were not shown because too many files have changed in this diff Show More