14 Commits

Author SHA1 Message Date
relikd
412d533275 Version 1.0.0 (15) 2020-04-18 18:56:37 +02:00
relikd
245bb46e4f DNS filters: proper sort + no cell selection + copy cell value 2020-04-18 00:39:59 +02:00
relikd
70508c1325 Tutorial Sheet (incl. Welcome message + Recordings introduction) 2020-04-17 23:37:03 +02:00
relikd
b44fd788b5 Fix iOS9 row edit issue 2020-04-08 22:34:44 +02:00
relikd
80f3503e16 Edit delete recordings 2020-04-08 21:34:45 +02:00
relikd
d0056c0275 Recording details duplicate and display 2020-04-08 18:53:00 +02:00
relikd
e7560479ee remove unused 2020-04-08 16:43:35 +02:00
relikd
ed5298f7a2 Storyboard logical sort 2020-04-06 23:46:38 +02:00
relikd
647eca310f Previous recordings detail view template 2020-04-06 23:37:46 +02:00
relikd
515c296b26 Keep title for expanded notes 2020-04-04 01:52:07 +02:00
relikd
61ae50cdfa Enlarge notes above keyboard 2020-04-04 00:06:16 +02:00
relikd
fcb6e9c5dd Stack view for recordings tab 2020-04-02 20:14:57 +02:00
relikd
79f836016a Recordings interface 2020-04-02 18:28:20 +02:00
relikd
144773ddaa Add (+) button to domain filter view 2020-03-26 19:28:03 +01:00
32 changed files with 1716 additions and 1248 deletions

View File

@@ -8,6 +8,9 @@
/* Begin PBXBuildFile section */
540C6457240D929300E948F9 /* EditableRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540C6456240D929300E948F9 /* EditableRows.swift */; };
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E677F242D2CF100871BBE /* VCRecordings.swift */; };
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67812433483D00871BBE /* VCEditRecording.swift */; };
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */; };
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 541A957523E602DF00C09C19 /* LaunchIcon.png */; };
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541AC5D72399498A00A769D7 /* AppDelegate.swift */; };
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DB2399498A00A769D7 /* Main.storyboard */; };
@@ -18,9 +21,13 @@
543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */; };
543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; };
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; };
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; };
546063E523FEFAFE008F505A /* SQDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* SQDB.swift */; };
54751E512423955100168273 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* FileManager.swift */; };
54751E522423955100168273 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* FileManager.swift */; };
54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54953E3323DC752E0054345C /* SQDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* SQDB.swift */; };
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E5E23DEBE840054345C /* TVCDomains.swift */; };
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; };
@@ -32,7 +39,7 @@
54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; };
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A8241BBA0B004C53CC /* Generic.swift */; };
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; };
54B345AD241BBB00004C53CC /* GroupedDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* GroupedDomain.swift */; };
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.swift */; };
54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; };
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; };
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; };
@@ -86,8 +93,6 @@
54CA02952426B2FD003A5E04 /* DNSEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02242426B2FC003A5E04 /* DNSEnums.swift */; };
54CA02962426B2FD003A5E04 /* PacketProtocolParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02262426B2FC003A5E04 /* PacketProtocolParser.swift */; };
54CA02972426B2FD003A5E04 /* IPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02272426B2FC003A5E04 /* IPPacket.swift */; };
54CA02982426B2FD003A5E04 /* IPMutablePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02282426B2FC003A5E04 /* IPMutablePacket.swift */; };
54CA02992426B2FD003A5E04 /* TCPMutablePacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02292426B2FC003A5E04 /* TCPMutablePacket.swift */; };
54CA029A2426B2FD003A5E04 /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA022B2426B2FC003A5E04 /* Observer.swift */; };
54CA029C2426B2FD003A5E04 /* AdapterSocketEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA022E2426B2FC003A5E04 /* AdapterSocketEvent.swift */; };
54CA029D2426B2FD003A5E04 /* ProxyServerEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA022F2426B2FC003A5E04 /* ProxyServerEvent.swift */; };
@@ -102,8 +107,6 @@
54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */; };
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */; };
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */; };
54CA02AA2426B2FD003A5E04 /* SpeedAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023F2426B2FC003A5E04 /* SpeedAdapterFactory.swift */; };
54CA02AB2426B2FD003A5E04 /* ShadowsocksAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02402426B2FC003A5E04 /* ShadowsocksAdapterFactory.swift */; };
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */; };
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */; };
54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02432426B2FD003A5E04 /* AdapterFactory.swift */; };
@@ -112,10 +115,6 @@
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */; };
54CA02B22426B2FD003A5E04 /* AdapterFactoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */; };
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */; };
54CA02B42426B2FD003A5E04 /* StreamObfuscater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024A2426B2FD003A5E04 /* StreamObfuscater.swift */; };
54CA02B52426B2FD003A5E04 /* CryptoStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024B2426B2FD003A5E04 /* CryptoStreamProcessor.swift */; };
54CA02B62426B2FD003A5E04 /* ProtocolObfuscater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024C2426B2FD003A5E04 /* ProtocolObfuscater.swift */; };
54CA02B72426B2FD003A5E04 /* ShadowsocksAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024D2426B2FD003A5E04 /* ShadowsocksAdapter.swift */; };
54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */; };
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */; };
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02512426B2FD003A5E04 /* ProxySocket.swift */; };
@@ -152,6 +151,9 @@
/* Begin PBXFileReference section */
540C6456240D929300E948F9 /* EditableRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableRows.swift; sourceTree = "<group>"; };
540E677F242D2CF100871BBE /* VCRecordings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCRecordings.swift; sourceTree = "<group>"; };
540E67812433483D00871BBE /* VCEditRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditRecording.swift; sourceTree = "<group>"; };
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCPreviousRecords.swift; sourceTree = "<group>"; };
541A957523E602DF00C09C19 /* LaunchIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = LaunchIcon.png; sourceTree = "<group>"; };
541AC5D42399498A00A769D7 /* AppCheck.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppCheck.app; sourceTree = BUILT_PRODUCTS_DIR; };
541AC5D72399498A00A769D7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -166,7 +168,11 @@
543CDB2123EEE61900B7F323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = "<group>"; };
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
54751E502423955000168273 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.swift; sourceTree = "<group>"; };
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = "<group>"; };
545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; };
54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = "<group>"; };
54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = "<group>"; };
54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
@@ -178,7 +184,7 @@
54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
54B345A8241BBA0B004C53CC /* Generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generic.swift; sourceTree = "<group>"; };
54B345AA241BBA5B004C53CC /* AlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSheet.swift; sourceTree = "<group>"; };
54B345AC241BBB00004C53CC /* GroupedDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomain.swift; sourceTree = "<group>"; };
54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.swift; sourceTree = "<group>"; };
54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
54B7562223D7B2DC008F0C41 /* SQDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQDB.swift; sourceTree = "<group>"; };
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; };
@@ -234,8 +240,6 @@
54CA02242426B2FC003A5E04 /* DNSEnums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSEnums.swift; sourceTree = "<group>"; };
54CA02262426B2FC003A5E04 /* PacketProtocolParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketProtocolParser.swift; sourceTree = "<group>"; };
54CA02272426B2FC003A5E04 /* IPPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPPacket.swift; sourceTree = "<group>"; };
54CA02282426B2FC003A5E04 /* IPMutablePacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPMutablePacket.swift; sourceTree = "<group>"; };
54CA02292426B2FC003A5E04 /* TCPMutablePacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TCPMutablePacket.swift; sourceTree = "<group>"; };
54CA022B2426B2FC003A5E04 /* Observer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = "<group>"; };
54CA022E2426B2FC003A5E04 /* AdapterSocketEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterSocketEvent.swift; sourceTree = "<group>"; };
54CA022F2426B2FC003A5E04 /* ProxyServerEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyServerEvent.swift; sourceTree = "<group>"; };
@@ -250,8 +254,6 @@
54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectAdapter.swift; sourceTree = "<group>"; };
54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5Adapter.swift; sourceTree = "<group>"; };
54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RejectAdapter.swift; sourceTree = "<group>"; };
54CA023F2426B2FC003A5E04 /* SpeedAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeedAdapterFactory.swift; sourceTree = "<group>"; };
54CA02402426B2FC003A5E04 /* ShadowsocksAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksAdapterFactory.swift; sourceTree = "<group>"; };
54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationServerAdapterFactory.swift; sourceTree = "<group>"; };
54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RejectAdapterFactory.swift; sourceTree = "<group>"; };
54CA02432426B2FD003A5E04 /* AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactory.swift; sourceTree = "<group>"; };
@@ -260,10 +262,6 @@
54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerAdapterFactory.swift; sourceTree = "<group>"; };
54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactoryManager.swift; sourceTree = "<group>"; };
54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAdapterFactory.swift; sourceTree = "<group>"; };
54CA024A2426B2FD003A5E04 /* StreamObfuscater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamObfuscater.swift; sourceTree = "<group>"; };
54CA024B2426B2FD003A5E04 /* CryptoStreamProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoStreamProcessor.swift; sourceTree = "<group>"; };
54CA024C2426B2FD003A5E04 /* ProtocolObfuscater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscater.swift; sourceTree = "<group>"; };
54CA024D2426B2FD003A5E04 /* ShadowsocksAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksAdapter.swift; sourceTree = "<group>"; };
54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPProxySocket.swift; sourceTree = "<group>"; };
54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectProxySocket.swift; sourceTree = "<group>"; };
54CA02512426B2FD003A5E04 /* ProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocket.swift; sourceTree = "<group>"; };
@@ -313,6 +311,17 @@
path = Settings;
sourceTree = "<group>";
};
540E677E242D2CD200871BBE /* Recordings */ = {
isa = PBXGroup;
children = (
540E677F242D2CF100871BBE /* VCRecordings.swift */,
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
);
path = Recordings;
sourceTree = "<group>";
};
541AC5CB2399498A00A769D7 = {
isa = PBXGroup;
children = (
@@ -337,11 +346,12 @@
children = (
54B3459A2415651C004C53CC /* DB */,
54B345A4241BB975004C53CC /* Extensions */,
545DDDD224436A03003B6544 /* Common Classes */,
548B1F9423D338EC005B047C /* main.entitlements */,
541AC5D72399498A00A769D7 /* AppDelegate.swift */,
542E2A972404973F001462DC /* TBCMain.swift */,
54B34597240F18DD004C53CC /* TVC Extensions */,
540C6454240D5BAE00E948F9 /* Requests */,
540E677E242D2CD200871BBE /* Recordings */,
540C6455240D5BD200E948F9 /* Settings */,
54B345B12422E029004C53CC /* unused */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
@@ -376,12 +386,14 @@
path = GlassVPN;
sourceTree = "<group>";
};
54B34597240F18DD004C53CC /* TVC Extensions */ = {
545DDDD224436A03003B6544 /* Common Classes */ = {
isa = PBXGroup;
children = (
545DDDD024436983003B6544 /* QuickUI.swift */,
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
540C6456240D929300E948F9 /* EditableRows.swift */,
);
path = "TVC Extensions";
path = "Common Classes";
sourceTree = "<group>";
};
54B3459A2415651C004C53CC /* DB */ = {
@@ -399,10 +411,11 @@
544C95252407B1C700AB89D0 /* SharedState.swift */,
54B345A8241BBA0B004C53CC /* Generic.swift */,
54B345A5241BB982004C53CC /* Notifications.swift */,
54B345AC241BBB00004C53CC /* DBExtensions.swift */,
54B345AA241BBA5B004C53CC /* AlertSheet.swift */,
54B345AC241BBB00004C53CC /* GroupedDomain.swift */,
54B34595240F0513004C53CC /* TableView.swift */,
54751E502423955000168273 /* FileManager.swift */,
54751E502423955000168273 /* URL.swift */,
545DDDD324466D37003B6544 /* AutoLayout.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -562,8 +575,6 @@
children = (
54CA02262426B2FC003A5E04 /* PacketProtocolParser.swift */,
54CA02272426B2FC003A5E04 /* IPPacket.swift */,
54CA02282426B2FC003A5E04 /* IPMutablePacket.swift */,
54CA02292426B2FC003A5E04 /* TCPMutablePacket.swift */,
);
path = Packet;
sourceTree = "<group>";
@@ -611,7 +622,6 @@
54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */,
54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */,
54CA023E2426B2FC003A5E04 /* Factory */,
54CA02492426B2FD003A5E04 /* Shadowsocks */,
);
path = AdapterSocket;
sourceTree = "<group>";
@@ -619,8 +629,6 @@
54CA023E2426B2FC003A5E04 /* Factory */ = {
isa = PBXGroup;
children = (
54CA023F2426B2FC003A5E04 /* SpeedAdapterFactory.swift */,
54CA02402426B2FC003A5E04 /* ShadowsocksAdapterFactory.swift */,
54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */,
54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */,
54CA02432426B2FD003A5E04 /* AdapterFactory.swift */,
@@ -633,17 +641,6 @@
path = Factory;
sourceTree = "<group>";
};
54CA02492426B2FD003A5E04 /* Shadowsocks */ = {
isa = PBXGroup;
children = (
54CA024A2426B2FD003A5E04 /* StreamObfuscater.swift */,
54CA024B2426B2FD003A5E04 /* CryptoStreamProcessor.swift */,
54CA024C2426B2FD003A5E04 /* ProtocolObfuscater.swift */,
54CA024D2426B2FD003A5E04 /* ShadowsocksAdapter.swift */,
);
path = Shadowsocks;
sourceTree = "<group>";
};
54CA024E2426B2FD003A5E04 /* ProxySocket */ = {
isa = PBXGroup;
children = (
@@ -772,24 +769,31 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54B345AD241BBB00004C53CC /* GroupedDomain.swift in Sources */,
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */,
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */,
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */,
54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */,
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */,
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */,
54B34596240F0513004C53CC /* TableView.swift in Sources */,
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */,
54953E3323DC752E0054345C /* SQDB.swift in Sources */,
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
540C6457240D929300E948F9 /* EditableRows.swift in Sources */,
54751E512423955100168273 /* FileManager.swift in Sources */,
54751E512423955100168273 /* URL.swift in Sources */,
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */,
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */,
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
54B345992414F491004C53CC /* DBWrapper.swift in Sources */,
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */,
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -802,7 +806,6 @@
54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */,
54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */,
54CA02862426B2FD003A5E04 /* RuleManager.swift in Sources */,
54CA02B52426B2FD003A5E04 /* CryptoStreamProcessor.swift in Sources */,
54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */,
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */,
54CA02752426B2FD003A5E04 /* IPRange.swift in Sources */,
@@ -812,7 +815,6 @@
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */,
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */,
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */,
54CA02982426B2FD003A5E04 /* IPMutablePacket.swift in Sources */,
54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */,
54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */,
54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */,
@@ -823,10 +825,8 @@
54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */,
54CA02912426B2FD003A5E04 /* DNSMessage.swift in Sources */,
54CA02712426B2FD003A5E04 /* UInt128.swift in Sources */,
54CA02B62426B2FD003A5E04 /* ProtocolObfuscater.swift in Sources */,
54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */,
54CA02A12426B2FD003A5E04 /* RuleMatchEvent.swift in Sources */,
54CA02AB2426B2FD003A5E04 /* ShadowsocksAdapterFactory.swift in Sources */,
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */,
54CA02962426B2FD003A5E04 /* PacketProtocolParser.swift in Sources */,
54CA02932426B2FD003A5E04 /* DNSServer.swift in Sources */,
@@ -840,7 +840,6 @@
54CA026B2426B2FD003A5E04 /* GCDTCPSocket.swift in Sources */,
54CA028E2426B2FD003A5E04 /* IPStackProtocol.swift in Sources */,
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */,
54CA02992426B2FD003A5E04 /* TCPMutablePacket.swift in Sources */,
54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */,
54CA02762426B2FD003A5E04 /* IPAddress.swift in Sources */,
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */,
@@ -852,14 +851,12 @@
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */,
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */,
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */,
54751E522423955100168273 /* FileManager.swift in Sources */,
54751E522423955100168273 /* URL.swift in Sources */,
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */,
54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */,
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */,
54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */,
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */,
54CA02B42426B2FD003A5E04 /* StreamObfuscater.swift in Sources */,
54CA02AA2426B2FD003A5E04 /* SpeedAdapterFactory.swift in Sources */,
54CA02952426B2FD003A5E04 /* DNSEnums.swift in Sources */,
54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */,
54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */,
@@ -883,7 +880,6 @@
546063E523FEFAFE008F505A /* SQDB.swift in Sources */,
54CA02872426B2FD003A5E04 /* IPRangeListRule.swift in Sources */,
54CA02922426B2FD003A5E04 /* DNSSession.swift in Sources */,
54CA02B72426B2FD003A5E04 /* ShadowsocksAdapter.swift in Sources */,
54CA026D2426B2FD003A5E04 /* Opt.swift in Sources */,
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */,
54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */,
@@ -1049,7 +1045,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 15;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -1068,7 +1064,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 15;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -1087,7 +1083,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 15;
INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
@@ -1105,7 +1101,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 15;
INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";

View File

@@ -1,76 +0,0 @@
//import Foundation
//
//enum ChangeType {
// case Address, Port
//}
//
//public class IPMutablePacket {
// // Support only IPv4 for now
//
// let version: IPVersion
// let proto: TransportType
// let IPHeaderLength: Int
// var sourceAddress: IPv4Address {
// get {
// return IPv4Address(fromBytesInNetworkOrder: payload.bytes.advancedBy(12))
// }
// set {
// setIPv4Address(sourceAddress, newAddress: newValue, at: 12)
// }
// }
// var destinationAddress: IPv4Address {
// get {
// return IPv4Address(fromBytesInNetworkOrder: payload.bytes.advancedBy(16))
// }
// set {
// setIPv4Address(destinationAddress, newAddress: newValue, at: 16)
// }
// }
//
// let payload: NSMutableData
//
// public init(payload: NSData) {
// let vl = UnsafePointer<UInt8>(payload.bytes).memory
// version = IPVersion(rawValue: vl >> 4)!
// IPHeaderLength = Int(vl & 0x0F) * 4
// let p = UnsafePointer<UInt8>(payload.bytes.advancedBy(9)).memory
// proto = TransportType(rawValue: p)!
// self.payload = NSMutableData(data: payload)
// }
//
// func updateChecksum(oldValue: UInt16, newValue: UInt16, type: ChangeType) {
// if type == .Address {
// updateChecksum(oldValue, newValue: newValue, at: 10)
// }
// }
//
// // swiftlint:disable:next variable_name
// internal func updateChecksum(oldValue: UInt16, newValue: UInt16, at: Int) {
// let oldChecksum = UnsafePointer<UInt16>(payload.bytes.advancedBy(at)).memory
// let oc32 = UInt32(~oldChecksum)
// let ov32 = UInt32(~oldValue)
// let nv32 = UInt32(newValue)
// var newChecksum32 = oc32 &+ ov32 &+ nv32
// newChecksum32 = (newChecksum32 & 0xFFFF) + (newChecksum32 >> 16)
// newChecksum32 = (newChecksum32 & 0xFFFF) &+ (newChecksum32 >> 16)
// var newChecksum = ~UInt16(newChecksum32)
// payload.replaceBytesInRange(NSRange(location: at, length: 2), withBytes: &newChecksum, length: 2)
// }
//
// // swiftlint:disable:next variable_name
// private func foldChecksum(checksum: UInt32) -> UInt32 {
// var checksum = checksum
// while checksum > 0xFFFF {
// checksum = (checksum & 0xFFFF) + (checksum >> 16)
// }
// return checksum
// }
//
// // swiftlint:disable:next variable_name
// private func setIPv4Address(oldAddress: IPv4Address, newAddress: IPv4Address, at: Int) {
// payload.replaceBytesInRange(NSRange(location: at, length: 4), withBytes: newAddress.bytesInNetworkOrder, length: 4)
// updateChecksum(UnsafePointer<UInt16>(oldAddress.bytesInNetworkOrder).memory, newValue: UnsafePointer<UInt16>(newAddress.bytesInNetworkOrder).memory, type: .Address)
// updateChecksum(UnsafePointer<UInt16>(oldAddress.bytesInNetworkOrder).advancedBy(1).memory, newValue: UnsafePointer<UInt16>(newAddress.bytesInNetworkOrder).advancedBy(1).memory, type: .Address)
// }
//
//}

View File

@@ -1,32 +0,0 @@
//import Foundation
//
//class TCPMutablePacket: IPMutablePacket {
// var sourcePort: Port {
// get {
// return Port(bytesInNetworkOrder: payload.bytes.advancedBy(IPHeaderLength))
// }
// set {
// setPort(sourcePort, newPort: newValue, at: 0)
// }
// }
//
// var destinationPort: Port {
// get {
// return Port(bytesInNetworkOrder: payload.bytes.advancedBy(IPHeaderLength + 2))
// }
// set {
// setPort(destinationPort, newPort: newValue, at: 2)
// }
// }
//
// override func updateChecksum(oldValue: UInt16, newValue: UInt16, type: ChangeType) {
// super.updateChecksum(oldValue, newValue: newValue, type: type)
// updateChecksum(oldValue, newValue: newValue, at: IPHeaderLength + 16)
// }
//
// // swiftlint:disable:next variable_name
// private func setPort(oldPort: Port, newPort: Port, at: Int) {
// payload.replaceBytesInRange(NSRange(location: at + IPHeaderLength, length: 2), withBytes: newPort.bytesInNetworkOrder, length: 2)
// updateChecksum(oldPort.valueInNetworkOrder, newValue: newPort.valueInNetworkOrder, type: .Port)
// }
//}

View File

@@ -1,28 +0,0 @@
//import Foundation
//
///// Factory building Shadowsocks adapter.
//open class ShadowsocksAdapterFactory: ServerAdapterFactory {
// let protocolObfuscaterFactory: ShadowsocksAdapter.ProtocolObfuscater.Factory
// let cryptorFactory: ShadowsocksAdapter.CryptoStreamProcessor.Factory
// let streamObfuscaterFactory: ShadowsocksAdapter.StreamObfuscater.Factory
//
// public init(serverHost: String, serverPort: Int, protocolObfuscaterFactory: ShadowsocksAdapter.ProtocolObfuscater.Factory, cryptorFactory: ShadowsocksAdapter.CryptoStreamProcessor.Factory, streamObfuscaterFactory: ShadowsocksAdapter.StreamObfuscater.Factory) {
// self.protocolObfuscaterFactory = protocolObfuscaterFactory
// self.cryptorFactory = cryptorFactory
// self.streamObfuscaterFactory = streamObfuscaterFactory
// super.init(serverHost: serverHost, serverPort: serverPort)
// }
//
// /**
// Get a Shadowsocks adapter.
//
// - parameter session: The connect session.
//
// - returns: The built adapter.
// */
// override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
// let adapter = ShadowsocksAdapter(host: serverHost, port: serverPort, protocolObfuscater: protocolObfuscaterFactory.build(), cryptor: cryptorFactory.build(), streamObfuscator: streamObfuscaterFactory.build(for: session))
// adapter.socket = RawSocketFactory.getRawSocket()
// return adapter
// }
//}

View File

@@ -1,26 +0,0 @@
//import Foundation
//
///// Factory building speed adapter.
//open class SpeedAdapterFactory: AdapterFactory {
// open var adapterFactories: [(AdapterFactory, Int)]!
//
// public override init() {}
//
// /**
// Get a speed adapter.
//
// - parameter session: The connect session.
//
// - returns: The built adapter.
// */
// override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
// let adapters = adapterFactories.map { adapterFactory, delay -> (AdapterSocket, Int) in
// let adapter = adapterFactory.getAdapterFor(session: session)
// adapter.socket = RawSocketFactory.getRawSocket()
// return (adapter, delay)
// }
// let speedAdapter = SpeedAdapter()
// speedAdapter.adapters = adapters
// return speedAdapter
// }
//}

View File

@@ -14,7 +14,7 @@ public class SOCKS5Adapter: AdapterSocket {
var internalStatus: SOCKS5AdapterStatus = .invalid
let helloData = Data(bytes: UnsafePointer<UInt8>(([0x05, 0x01, 0x00] as [UInt8])), count: 3)
let helloData = Data([0x05, 0x01, 0x00])
public enum ReadTag: Int {
case methodResponse = -20000, connectResponseFirstPart, connectResponseSecondPart

View File

@@ -1,133 +0,0 @@
//import Foundation
//
//extension ShadowsocksAdapter {
// public class CryptoStreamProcessor {
// public class Factory {
// let password: String
// let algorithm: CryptoAlgorithm
// let key: Data
//
// public init(password: String, algorithm: CryptoAlgorithm) {
// self.password = password
// self.algorithm = algorithm
// key = CryptoHelper.getKey(password, methodType: algorithm)
// }
//
// public func build() -> CryptoStreamProcessor {
// return CryptoStreamProcessor(key: key, algorithm: algorithm)
// }
// }
//
// public weak var inputStreamProcessor: StreamObfuscater.StreamObfuscaterBase!
// public weak var outputStreamProcessor: ProtocolObfuscater.ProtocolObfuscaterBase!
//
// var readIV: Data!
// let key: Data
// let algorithm: CryptoAlgorithm
//
// var sendKey = false
//
// var buffer = Buffer(capacity: 0)
//
// lazy var writeIV: Data = {
// [unowned self] in
// CryptoHelper.getIV(self.algorithm)
// }()
// lazy var ivLength: Int = {
// [unowned self] in
// CryptoHelper.getIVLength(self.algorithm)
// }()
// lazy var encryptor: StreamCryptoProtocol = {
// [unowned self] in
// self.getCrypto(.encrypt)
// }()
// lazy var decryptor: StreamCryptoProtocol = {
// [unowned self] in
// self.getCrypto(.decrypt)
// }()
//
// init(key: Data, algorithm: CryptoAlgorithm) {
// self.key = key
// self.algorithm = algorithm
// }
//
// func encrypt(data: inout Data) {
// return encryptor.update(&data)
// }
//
// func decrypt(data: inout Data) {
// return decryptor.update(&data)
// }
//
// public func input(data: Data) throws {
// var data = data
//
// if readIV == nil {
// buffer.append(data: data)
// readIV = buffer.get(length: ivLength)
// guard readIV != nil else {
// try inputStreamProcessor!.input(data: Data())
// return
// }
//
// data = buffer.get() ?? Data()
// buffer.release()
// }
//
// decrypt(data: &data)
// try inputStreamProcessor!.input(data: data)
// }
//
// public func output(data: Data) {
// var data = data
// encrypt(data: &data)
// if sendKey {
// return outputStreamProcessor!.output(data: data)
// } else {
// sendKey = true
// var out = Data(capacity: data.count + writeIV.count)
// out.append(writeIV)
// out.append(data)
//
// return outputStreamProcessor!.output(data: out)
// }
// }
//
// private func getCrypto(_ operation: CryptoOperation) -> StreamCryptoProtocol {
// switch algorithm {
// case .AES128CFB, .AES192CFB, .AES256CFB:
// switch operation {
// case .decrypt:
// return CCCrypto(operation: .decrypt, mode: .cfb, algorithm: .aes, initialVector: readIV, key: key)
// case .encrypt:
// return CCCrypto(operation: .encrypt, mode: .cfb, algorithm: .aes, initialVector: writeIV, key: key)
// }
// case .CHACHA20:
// switch operation {
// case .decrypt:
// return SodiumStreamCrypto(key: key, iv: readIV, algorithm: .chacha20)
// case .encrypt:
// return SodiumStreamCrypto(key: key, iv: writeIV, algorithm: .chacha20)
// }
// case .SALSA20:
// switch operation {
// case .decrypt:
// return SodiumStreamCrypto(key: key, iv: readIV, algorithm: .salsa20)
// case .encrypt:
// return SodiumStreamCrypto(key: key, iv: writeIV, algorithm: .salsa20)
// }
// case .RC4MD5:
// var combinedKey = Data(capacity: key.count + ivLength)
// combinedKey.append(key)
// switch operation {
// case .decrypt:
// combinedKey.append(readIV)
// return CCCrypto(operation: .decrypt, mode: .rc4, algorithm: .rc4, initialVector: nil, key: MD5Hash.final(combinedKey))
// case .encrypt:
// combinedKey.append(writeIV)
// return CCCrypto(operation: .encrypt, mode: .rc4, algorithm: .rc4, initialVector: nil, key: MD5Hash.final(combinedKey))
// }
// }
// }
// }
//}

View File

@@ -1,371 +0,0 @@
//import Foundation
//
//extension ShadowsocksAdapter {
// public struct ProtocolObfuscater {
// public class Factory {
// public init() {}
//
// public func build() -> ProtocolObfuscaterBase {
// return ProtocolObfuscaterBase()
// }
// }
//
// public class ProtocolObfuscaterBase {
// public weak var inputStreamProcessor: CryptoStreamProcessor!
// public weak var outputStreamProcessor: ShadowsocksAdapter!
//
// public func start() {}
// public func input(data: Data) throws {}
// public func output(data: Data) {}
//
// public func didWrite() {}
// }
//
// public class OriginProtocolObfuscater: ProtocolObfuscaterBase {
//
// public class Factory: ProtocolObfuscater.Factory {
// public override init() {}
//
// public override func build() -> ShadowsocksAdapter.ProtocolObfuscater.ProtocolObfuscaterBase {
// return OriginProtocolObfuscater()
// }
// }
//
// public override func start() {
// outputStreamProcessor.becomeReadyToForward()
// }
//
// public override func input(data: Data) throws {
// try inputStreamProcessor.input(data: data)
// }
//
// public override func output(data: Data) {
// outputStreamProcessor.output(data: data)
// }
// }
//
// public class HTTPProtocolObfuscater: ProtocolObfuscaterBase {
//
// public class Factory: ProtocolObfuscater.Factory {
// let method: String
// let hosts: [String]
// let customHeader: String?
//
// public init(method: String = "GET", hosts: [String], customHeader: String?) {
// self.method = method
// self.hosts = hosts
// self.customHeader = customHeader
// }
//
// public override func build() -> ShadowsocksAdapter.ProtocolObfuscater.ProtocolObfuscaterBase {
// return HTTPProtocolObfuscater(method: method, hosts: hosts, customHeader: customHeader)
// }
// }
//
// static let headerLength = 30
// static let userAgent = ["Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0",
// "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/44.0",
// "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36",
// "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0",
// "Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)",
// "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27",
// "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)",
// "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
// "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36",
// "Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
// "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"]
//
// let method: String
// let hosts: [String]
// let customHeader: String?
//
// var readingFakeHeader = false
// var sendHeader = false
// var remaining = false
//
// var buffer = Buffer(capacity: 8192)
//
// public init(method: String = "GET", hosts: [String], customHeader: String?) {
// self.method = method
// self.hosts = hosts
// self.customHeader = customHeader
// }
//
// private func generateHeader(encapsulating data: Data) -> String {
// let ind = Int(arc4random_uniform(UInt32(hosts.count)))
// let host = outputStreamProcessor.port == 80 ? hosts[ind] : "\(hosts[ind]):\(outputStreamProcessor.port)"
// var header = "\(method) /\(hexlify(data: data)) HTTP/1.1\r\nHost: \(host)\r\n"
// if let customHeader = customHeader {
// header += customHeader
// } else {
// let ind = Int(arc4random_uniform(UInt32(HTTPProtocolObfuscater.userAgent.count)))
// header += "User-Agent: \(HTTPProtocolObfuscater.userAgent[ind])\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nDNT: 1\r\nConnection: keep-alive"
// }
// header += "\r\n\r\n"
// return header
// }
//
// private func hexlify(data: Data) -> String {
// var result = ""
// for i in data {
// result = result.appendingFormat("%%%02x", i)
// }
// return result
// }
//
// public override func start() {
// readingFakeHeader = true
// outputStreamProcessor.becomeReadyToForward()
// }
//
// public override func input(data: Data) throws {
// if readingFakeHeader {
// buffer.append(data: data)
// if buffer.get(to: Utils.HTTPData.DoubleCRLF) != nil {
// readingFakeHeader = false
// if let remainData = buffer.get() {
// try inputStreamProcessor.input(data: remainData)
// return
// }
// }
// try inputStreamProcessor.input(data: Data())
// return
// }
//
// try inputStreamProcessor.input(data: data)
// }
//
// public override func output(data: Data) {
// if sendHeader {
// outputStreamProcessor.output(data: data)
// } else {
// var fakeRequestDataLength = inputStreamProcessor.key.count + HTTPProtocolObfuscater.headerLength
// if data.count - fakeRequestDataLength > 64 {
// fakeRequestDataLength += Int(arc4random_uniform(64))
// } else {
// fakeRequestDataLength = data.count
// }
//
// var outputData = generateHeader(encapsulating: data.subdata(in: 0 ..< fakeRequestDataLength)).data(using: .utf8)!
// outputData.append(data.subdata(in: fakeRequestDataLength ..< data.count))
// sendHeader = true
// outputStreamProcessor.output(data: outputData)
// }
// }
// }
//
// public class TLSProtocolObfuscater: ProtocolObfuscaterBase {
//
// public class Factory: ProtocolObfuscater.Factory {
// let hosts: [String]
//
// public init(hosts: [String]) {
// self.hosts = hosts
// }
//
// public override func build() -> ShadowsocksAdapter.ProtocolObfuscater.ProtocolObfuscaterBase {
// return TLSProtocolObfuscater(hosts: hosts)
// }
// }
//
// let hosts: [String]
// let clientID: Data = {
// var id = Data(count: 32)
// Utils.Random.fill(data: &id)
// return id
// }()
//
// private var status = 0
//
// private var buffer = Buffer(capacity: 1024)
//
// init(hosts: [String]) {
// self.hosts = hosts
// }
//
// public override func start() {
// handleStatus0()
// outputStreamProcessor.socket.readDataTo(length: 129)
// }
//
// public override func input(data: Data) throws {
// switch status {
// case 8:
// try handleInput(data: data)
// case 1:
// outputStreamProcessor.becomeReadyToForward()
// default:
// break
// }
// }
//
// public override func output(data: Data) {
// switch status {
// case 8:
// handleStatus8(data: data)
// return
// case 1:
// handleStatus1(data: data)
// return
// default:
// break
// }
// }
//
// private func authData() -> Data {
// var time = UInt32(Date.init().timeIntervalSince1970).bigEndian
// var output = Data(count: 32)
// var key = inputStreamProcessor.key
// key.append(clientID)
//
// withUnsafeBytes(of: &time) {
// output.replaceSubrange(0 ..< 4, with: $0)
// }
//
// Utils.Random.fill(data: &output, from: 4, length: 18)
// output.withUnsafeBytes {
// output.replaceSubrange(22 ..< 32, with: HMAC.final(value: $0.baseAddress!, length: 22, algorithm: .SHA1, key: key).subdata(in: 0..<10))
// }
// return output
// }
//
// private func pack(data: Data) -> Data {
// var output = Data()
// var left = data.count
// while left > 0 {
// let blockSize = UInt16(min(Int(arc4random_uniform(UInt32(UInt16.max))) % 4096 + 100, left))
// var blockSizeBE = blockSize.bigEndian
// output.append(contentsOf: [0x17, 0x03, 0x03])
// withUnsafeBytes(of: &blockSizeBE) {
// output.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
// output.append(data.subdata(in: data.count - left ..< data.count - left + Int(blockSize)))
// left -= Int(blockSize)
// }
// return output
// }
//
// private func handleStatus8(data: Data) {
// outputStreamProcessor.output(data: pack(data: data))
// }
//
// private func handleStatus0() {
// status = 1
//
// var outData = Data()
// outData.append(contentsOf: [0x03, 0x03])
// outData.append(authData())
// outData.append(0x20)
// outData.append(clientID)
// outData.append(contentsOf: [0x00, 0x1c, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x0a, 0xc0, 0x14, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x0a])
// outData.append("0100".data(using: .utf8)!)
//
// var extData = Data()
// extData.append(contentsOf: [0xff, 0x01, 0x00, 0x01, 0x00])
// let hostData = hosts[Int(arc4random_uniform(UInt32(hosts.count)))].data(using: .utf8)!
//
// var sniData = Data(capacity: hosts.count + 2 + 1 + 2 + 2 + 2)
//
// sniData.append(contentsOf: [0x00, 0x00])
//
// var _lenBE = UInt16(hostData.count + 5).bigEndian
// withUnsafeBytes(of: &_lenBE) {
// sniData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
//
// _lenBE = UInt16(hostData.count + 3).bigEndian
// withUnsafeBytes(of: &_lenBE) {
// sniData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
//
// sniData.append(0x00)
//
// _lenBE = UInt16(hostData.count).bigEndian
// withUnsafeBytes(of: &_lenBE) {
// sniData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
//
// sniData.append(hostData)
//
// extData.append(sniData)
//
// extData.append(contentsOf: [0x00, 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0xd0])
//
// var randomData = Data(count: 208)
// Utils.Random.fill(data: &randomData)
// extData.append(randomData)
//
// extData.append(contentsOf: [0x00, 0x0d, 0x00, 0x16, 0x00, 0x14, 0x06, 0x01, 0x06, 0x03, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03])
// extData.append(contentsOf: [0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00])
// extData.append(contentsOf: [0x00, 0x12, 0x00, 0x00])
// extData.append(contentsOf: [0x75, 0x50, 0x00, 0x00])
// extData.append(contentsOf: [0x00, 0x0b, 0x00, 0x02, 0x01, 0x00])
// extData.append(contentsOf: [0x00, 0x0a, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x18])
//
// _lenBE = UInt16(extData.count).bigEndian
// withUnsafeBytes(of: &_lenBE) {
// outData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
// outData.append(extData)
//
// var outputData = Data(capacity: outData.count + 9)
// outputData.append(contentsOf: [0x16, 0x03, 0x01])
// _lenBE = UInt16(outData.count + 4).bigEndian
// withUnsafeBytes(of: &_lenBE) {
// outputData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
// outputData.append(contentsOf: [0x01, 0x00])
// _lenBE = UInt16(outData.count).bigEndian
// withUnsafeBytes(of: &_lenBE) {
// outputData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
// }
// outputData.append(outData)
// outputStreamProcessor.output(data: outputData)
// }
//
// private func handleStatus1(data: Data) {
// status = 8
//
// var outputData = Data()
// outputData.append(contentsOf: [0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x16, 0x03, 0x03, 0x00, 0x20])
// var random = Data(count: 22)
// Utils.Random.fill(data: &random)
// outputData.append(random)
//
// var key = inputStreamProcessor.key
// key.append(clientID)
// outputData.withUnsafeBytes {
// outputData.append(HMAC.final(value: $0.baseAddress!, length: outputData.count, algorithm: .SHA1, key: key).subdata(in: 0..<10))
// }
//
// outputData.append(pack(data: data))
//
// outputStreamProcessor.output(data: outputData)
// }
//
// private func handleInput(data: Data) throws {
// buffer.append(data: data)
// var unpackedData = Data()
// while buffer.left > 5 {
// buffer.skip(3)
// var length: Int = 0
// buffer.withUnsafeBytes { (ptr: UnsafePointer<UInt16>) in
// length = Int(ptr.pointee.byteSwapped)
// }
// buffer.skip(2)
// if buffer.left >= length {
// unpackedData.append(buffer.get(length: length)!)
// continue
// } else {
// buffer.setBack(length: 5)
// break
// }
// }
// buffer.squeeze()
// try inputStreamProcessor.input(data: unpackedData)
// }
// }
//
// }
//}

View File

@@ -1,112 +0,0 @@
//import Foundation
//import CommonCrypto
//
///// This adapter connects to remote through Shadowsocks proxy.
//public class ShadowsocksAdapter: AdapterSocket {
// enum ShadowsocksAdapterStatus {
// case invalid,
// connecting,
// connected,
// forwarding,
// stopped
// }
//
// enum EncryptMethod: String {
// case AES128CFB = "AES-128-CFB", AES192CFB = "AES-192-CFB", AES256CFB = "AES-256-CFB"
//
// static let allValues: [EncryptMethod] = [.AES128CFB, .AES192CFB, .AES256CFB]
// }
//
// public let host: String
// public let port: Int
//
// var internalStatus: ShadowsocksAdapterStatus = .invalid
//
// private let protocolObfuscater: ProtocolObfuscater.ProtocolObfuscaterBase
// private let cryptor: CryptoStreamProcessor
// private let streamObfuscator: StreamObfuscater.StreamObfuscaterBase
//
// public init(host: String, port: Int, protocolObfuscater: ProtocolObfuscater.ProtocolObfuscaterBase, cryptor: CryptoStreamProcessor, streamObfuscator: StreamObfuscater.StreamObfuscaterBase) {
// self.host = host
// self.port = port
// self.protocolObfuscater = protocolObfuscater
// self.cryptor = cryptor
// self.streamObfuscator = streamObfuscator
//
// super.init()
//
// protocolObfuscater.inputStreamProcessor = cryptor
// protocolObfuscater.outputStreamProcessor = self
//
// cryptor.inputStreamProcessor = streamObfuscator
// cryptor.outputStreamProcessor = protocolObfuscater
//
// streamObfuscator.inputStreamProcessor = self
// streamObfuscator.outputStreamProcessor = cryptor
// }
//
// override public func openSocketWith(session: ConnectSession) {
// super.openSocketWith(session: session)
//
// do {
// internalStatus = .connecting
// try socket.connectTo(host: host, port: port, enableTLS: false, tlsSettings: nil)
// } catch let error {
// observer?.signal(.errorOccured(error, on: self))
// disconnect()
// }
// }
//
// override public func didConnectWith(socket: RawTCPSocketProtocol) {
// super.didConnectWith(socket: socket)
//
// internalStatus = .connected
//
// protocolObfuscater.start()
// }
//
// override public func didRead(data: Data, from socket: RawTCPSocketProtocol) {
// super.didRead(data: data, from: socket)
//
// do {
// try protocolObfuscater.input(data: data)
// } catch {
// disconnect()
// }
// }
//
// public override func write(data: Data) {
// streamObfuscator.output(data: data)
// }
//
// public func write(rawData: Data) {
// super.write(data: rawData)
// }
//
// public func input(data: Data) {
// delegate?.didRead(data: data, from: self)
// }
//
// public func output(data: Data) {
// write(rawData: data)
// }
//
// override public func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
// super.didWrite(data: data, by: socket)
//
// protocolObfuscater.didWrite()
//
// switch internalStatus {
// case .forwarding:
// delegate?.didWrite(data: data, by: self)
// default:
// return
// }
// }
//
// func becomeReadyToForward() {
// internalStatus = .forwarding
// observer?.signal(.readyForForward(self))
// delegate?.didBecomeReadyToForwardWith(socket: self)
// }
//}

View File

@@ -1,167 +0,0 @@
//import Foundation
//
//extension ShadowsocksAdapter {
// public struct StreamObfuscater {
// public class Factory {
// public init() {}
//
// public func build(for session: ConnectSession) -> StreamObfuscaterBase {
// return StreamObfuscaterBase(for: session)
// }
// }
//
// public class StreamObfuscaterBase {
// public weak var inputStreamProcessor: ShadowsocksAdapter!
// private weak var _outputStreamProcessor: CryptoStreamProcessor!
// public var outputStreamProcessor: CryptoStreamProcessor! {
// get {
// return _outputStreamProcessor
// }
// set {
// _outputStreamProcessor = newValue
// key = _outputStreamProcessor?.key
// writeIV = _outputStreamProcessor?.writeIV
// }
// }
//
// public var key: Data?
// public var writeIV: Data?
//
// let session: ConnectSession
//
// init(for session: ConnectSession) {
// self.session = session
// }
//
// func output(data: Data) {}
// func input(data: Data) throws {}
// }
//
// public class OriginStreamObfuscater: StreamObfuscaterBase {
// public class Factory: StreamObfuscater.Factory {
// public override init() {}
//
// public override func build(for session: ConnectSession) -> ShadowsocksAdapter.StreamObfuscater.StreamObfuscaterBase {
// return OriginStreamObfuscater(for: session)
// }
// }
//
// private var requestSend = false
//
// private func requestData(withData data: Data) -> Data {
// let hostLength = session.host.utf8.count
// let length = 1 + 1 + hostLength + 2 + data.count
// var response = Data(count: length)
// response[0] = 3
// response[1] = UInt8(hostLength)
// response.replaceSubrange(2..<2+hostLength, with: session.host.utf8)
// var beport = UInt16(session.port).bigEndian
// withUnsafeBytes(of: &beport) {
// response.replaceSubrange(2+hostLength..<4+hostLength, with: $0)
// }
// response.replaceSubrange(4+hostLength..<length, with: data)
// return response
// }
//
// public override func input(data: Data) throws {
// inputStreamProcessor!.input(data: data)
// }
//
// public override func output(data: Data) {
// if requestSend {
// return outputStreamProcessor!.output(data: data)
// } else {
// requestSend = true
// return outputStreamProcessor!.output(data: requestData(withData: data))
// }
// }
// }
//
// public class OTAStreamObfuscater: StreamObfuscaterBase {
// public class Factory: StreamObfuscater.Factory {
// public override init() {}
//
// public override func build(for session: ConnectSession) -> ShadowsocksAdapter.StreamObfuscater.StreamObfuscaterBase {
// return OTAStreamObfuscater(for: session)
// }
// }
//
// private var count: UInt32 = 0
//
// private let DATA_BLOCK_SIZE = 0xFFFF - 12
//
// private var requestSend = false
//
// private func requestData() -> Data {
// var response: [UInt8] = [0x13]
// response.append(UInt8(session.host.utf8.count))
// response += [UInt8](session.host.utf8)
// response += [UInt8](Utils.toByteArray(UInt16(session.port)).reversed())
// var responseData = Data(bytes: UnsafePointer<UInt8>(response), count: response.count)
// var keyiv = Data(count: key!.count + writeIV!.count)
//
// keyiv.replaceSubrange(0..<writeIV!.count, with: writeIV!)
// keyiv.replaceSubrange(writeIV!.count..<writeIV!.count + key!.count, with: key!)
// responseData.append(HMAC.final(value: responseData, algorithm: .SHA1, key: keyiv).subdata(in: 0..<10))
// return responseData
// }
//
// public override func input(data: Data) throws {
// inputStreamProcessor!.input(data: data)
// }
//
// public override func output(data: Data) {
// let fullBlockCount = data.count / DATA_BLOCK_SIZE
// var outputSize = fullBlockCount * (DATA_BLOCK_SIZE + 10 + 2)
// if data.count > fullBlockCount * DATA_BLOCK_SIZE {
// outputSize += data.count - fullBlockCount * DATA_BLOCK_SIZE + 10 + 2
// }
//
// let _requestData: Data = requestData()
// if !requestSend {
// outputSize += _requestData.count
// }
//
// var outputData = Data(count: outputSize)
// var outputOffset = 0
// var dataOffset = 0
//
// if !requestSend {
// requestSend = true
// outputData.replaceSubrange(0..<_requestData.count, with: _requestData)
// outputOffset += _requestData.count
// }
//
// while outputOffset != outputSize {
// let blockLength = min(data.count - dataOffset, DATA_BLOCK_SIZE)
// var len = UInt16(blockLength).bigEndian
// withUnsafeBytes(of: &len) {
// outputData.replaceSubrange(outputOffset..<outputOffset+2, with: $0)
// }
//
// var kc = Data(count: writeIV!.count + MemoryLayout.size(ofValue: count))
// kc.replaceSubrange(0..<writeIV!.count, with: writeIV!)
// var c = count.bigEndian
// let ms = MemoryLayout.size(ofValue: c)
// withUnsafeBytes(of: &c) {
// kc.replaceSubrange(writeIV!.count..<writeIV!.count+ms, with: $0)
// }
//
// data.withUnsafeBytes {
// outputData.replaceSubrange(outputOffset+2..<outputOffset+12, with: HMAC.final(value: $0.baseAddress!.advanced(by: dataOffset), length: blockLength, algorithm: .SHA1, key: kc).subdata(in: 0..<10))
// }
//
// data.withUnsafeBytes {
// outputData.replaceSubrange(outputOffset+12..<outputOffset+12+blockLength, with: $0.baseAddress!.advanced(by: dataOffset), count: blockLength)
// }
//
// count += 1
// outputOffset += 12 + blockLength
// dataOffset += blockLength
// }
//
// return outputStreamProcessor!.output(data: outputData)
// }
// }
// }
//}

View File

@@ -27,7 +27,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.173913043478265" y="375"/>
<point key="canvasLocation" x="120" y="100"/>
</scene>
</scenes>
<resources>

View File

@@ -1,13 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Main-->
<scene sceneID="7Rl-BK-ry5">
<objects>
<tabBarController id="sfA-EG-18J" customClass="TBCMain" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="qza-ey-Iaz">
<rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
<segue destination="hm5-7q-Zfi" kind="relationship" relationship="viewControllers" id="pfK-BR-9lf"/>
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="AwW-3j-iAg"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-819" y="150"/>
</scene>
<!--Requests-->
<scene sceneID="bDO-X1-bCe">
<objects>
<navigationController id="RcB-4v-fd4" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Requests" image="journal" id="Sj5-Kb-Li8"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HWd-73-m8j">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="pdd-aM-sKl" kind="relationship" relationship="rootViewController" id="oMe-a0-xN7"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="-1250"/>
</scene>
<!--Domains-->
<scene sceneID="MN1-aZ-cZt">
<objects>
@@ -17,11 +53,11 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" hidesAccessoryWhenEditing="NO" indentationWidth="10" reuseIdentifier="DomainCell" textLabel="0HB-5f-eB1" detailTextLabel="MRe-Eq-gvc" style="IBUITableViewCellStyleSubtitle" id="F8D-aK-j1W">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationWidth="10" reuseIdentifier="DomainCell" textLabel="0HB-5f-eB1" detailTextLabel="MRe-Eq-gvc" style="IBUITableViewCellStyleSubtitle" id="F8D-aK-j1W">
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F8D-aK-j1W" id="FY2-xr-hqh">
<rect key="frame" x="0.0" y="0.0" width="320" height="55.5"/>
<rect key="frame" x="0.0" y="0.0" width="293" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="0HB-5f-eB1">
@@ -41,7 +77,7 @@
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="WcC-nb-Vf5" kind="show" id="EVQ-hO-JE9"/>
<segue destination="WcC-nb-Vf5" kind="push" id="EVQ-hO-JE9"/>
</connections>
</tableViewCell>
</prototypes>
@@ -54,7 +90,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="jfx-iA-E0v" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="686" y="-1245"/>
<point key="canvasLocation" x="700" y="-1250"/>
</scene>
<!--Hosts-->
<scene sceneID="ZCV-Yx-jjW">
@@ -65,11 +101,11 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uv0-9B-Zbb" id="6vH-Du-gCg">
<rect key="frame" x="0.0" y="0.0" width="320" height="55.5"/>
<rect key="frame" x="0.0" y="0.0" width="293" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Rnk-SP-UHm">
@@ -89,7 +125,7 @@
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="h7Z-Qr-pJ5" kind="show" id="TPa-Zn-eOs"/>
<segue destination="h7Z-Qr-pJ5" kind="push" id="TPa-Zn-eOs"/>
</connections>
</tableViewCell>
</prototypes>
@@ -102,7 +138,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1391" y="-1245"/>
<point key="canvasLocation" x="1400" y="-1250"/>
</scene>
<!--Occurrences-->
<scene sceneID="ws3-sK-l8m">
@@ -140,94 +176,405 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2096" y="-1245"/>
<point key="canvasLocation" x="2100" y="-1250"/>
</scene>
<!--Requests-->
<scene sceneID="bDO-X1-bCe">
<!--Recordings-->
<scene sceneID="ODR-PD-nTU">
<objects>
<navigationController id="RcB-4v-fd4" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Requests" image="journal" id="Sj5-Kb-Li8"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HWd-73-m8j">
<viewController id="hm5-7q-Zfi" customClass="VCRecordings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JYr-yE-eGS">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Wz5-zb-gwz">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="La3-9e-6TK" userLabel="NewRec">
<rect key="frame" x="0.0" y="0.0" width="320" height="90"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00.000" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rbR-np-cXD">
<rect key="frame" x="0.0" y="0.0" width="320" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" updatesFrequently="YES"/>
</accessibility>
<fontDescription key="fontDescription" type="system" weight="ultraLight" pointSize="32"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="vAq-EZ-Gmx">
<rect key="frame" x="0.0" y="54" width="320" height="36"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<state key="normal" title="Stop Recording">
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="startRecordingButtonTapped:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="hEp-UI-i6R"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="height" constant="90" id="bqy-bR-yVI"/>
</constraints>
</view>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v3Z-HR-abM">
<rect key="frame" x="0.0" y="98" width="320" height="421"/>
<connections>
<segue destination="C7Q-Vu-xAC" kind="embed" id="ZTW-t1-5G1"/>
</connections>
</containerView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="leading" secondItem="lFq-fl-zah" secondAttribute="leading" id="Sjv-qq-h50"/>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="trailing" secondItem="lFq-fl-zah" secondAttribute="trailing" id="hhA-jU-DJS"/>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="bottom" secondItem="lFq-fl-zah" secondAttribute="bottom" id="m6I-NP-LhY"/>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="top" secondItem="lFq-fl-zah" secondAttribute="top" id="pEc-Cz-v9f"/>
</constraints>
<viewLayoutGuide key="safeArea" id="lFq-fl-zah"/>
</view>
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="mGk-aq-MRP"/>
<connections>
<outlet property="startButton" destination="vAq-EZ-Gmx" id="FSo-GH-jtd"/>
<outlet property="startNewRecView" destination="La3-9e-6TK" id="I5w-aK-DlY"/>
<outlet property="timeLabel" destination="rbR-np-cXD" id="EEe-8F-HT6"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wfy-Tp-A9o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="-550"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="GQx-dK-qb5">
<objects>
<navigationController id="C7Q-Vu-xAC" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="mCN-Hk-Z5i"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translucent="NO" id="ByI-P4-oVv">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="NtQ-rp-G6c">
<autoresizingMask key="autoresizingMask"/>
</toolbar>
<connections>
<segue destination="Fln-DD-aId" kind="relationship" relationship="rootViewController" id="smF-1g-aDM"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="GEP-3e-6Ko" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="-550"/>
</scene>
<!--Previous Recordings-->
<scene sceneID="RqA-Jc-FDE">
<objects>
<tableViewController id="Fln-DD-aId" customClass="TVCPreviousRecords" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="7cH-g6-H5z">
<rect key="frame" x="0.0" y="0.0" width="320" height="377"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailButton" indentationWidth="10" reuseIdentifier="PreviousRecordCell" textLabel="hr0-Xt-5gV" detailTextLabel="Xav-Ub-clj" style="IBUITableViewCellStyleSubtitle" id="3kW-3B-1bx">
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3kW-3B-1bx" id="OKV-a6-jjd">
<rect key="frame" x="0.0" y="0.0" width="280" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hr0-Xt-5gV">
<rect key="frame" x="16" y="10" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Xav-Ub-clj">
<rect key="frame" x="16" y="31.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="50g-BI-Q6S" kind="push" identifier="openRecordDetailsSegue" id="arP-jR-O9d"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="Fln-DD-aId" id="oHb-mU-M1Z"/>
<outlet property="delegate" destination="Fln-DD-aId" id="6PY-c0-Nfp"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Previous Recordings" id="ow1-cy-qXt"/>
<connections>
<segue destination="VRk-wv-rhk" kind="modal" identifier="editRecordSegue" id="8rY-sA-Iig"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Lta-uo-x4m" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="-550"/>
</scene>
<!--Edit Recording-->
<scene sceneID="pqx-CU-4AP">
<objects>
<viewController id="VRk-wv-rhk" customClass="VCEditRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="rXz-Mk-wrK">
<rect key="frame" x="0.0" y="0.0" width="320" height="421"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2yS-xK-Wac">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<gestureRecognizers/>
<items>
<navigationItem title="Edit" id="JSi-oz-VRx">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="TGg-60-wZW">
<connections>
<action selector="didTapCancel:" destination="VRk-wv-rhk" id="Kff-ed-gdd"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" enabled="NO" style="done" systemItem="save" id="rWg-hE-Ydl">
<connections>
<action selector="didTapSave:" destination="VRk-wv-rhk" id="Xee-qo-bQx"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
<connections>
<outletCollection property="gestureRecognizers" destination="klV-Ed-xzV" appends="YES" id="Huf-jb-4Ef"/>
</connections>
</navigationBar>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="xdn-EU-IMx">
<rect key="frame" x="16" y="56" width="288" height="355"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Guy-Ra-fpS" userLabel="Title">
<rect key="frame" x="0.0" y="0.0" width="288" height="58"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Et0-8d-CId">
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Unnamed Recording #12345678" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" id="OCX-wu-l5d">
<rect key="frame" x="4" y="24" width="280" height="34"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" returnKeyType="next"/>
<connections>
<outlet property="delegate" destination="VRk-wv-rhk" id="uJL-hB-9w7"/>
</connections>
</textField>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="height" constant="58" id="5ew-Cq-VKh"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-UG-dwT" userLabel="Notes">
<rect key="frame" x="0.0" y="66" width="288" height="190"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QJp-6C-yoZ">
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NXU-yU-eST">
<rect key="frame" x="0.0" y="24" width="288" height="166"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<string key="text">1. Line
2. Line
3. Line
4. Line</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<connections>
<outlet property="delegate" destination="VRk-wv-rhk" id="vej-jI-13V"/>
</connections>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="NXU-yU-eST" firstAttribute="leading" secondItem="ybL-UG-dwT" secondAttribute="leading" id="D6U-8L-f9m"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="750" constant="107" id="Pfy-uW-kRl"/>
<constraint firstItem="NXU-yU-eST" firstAttribute="trailing" secondItem="ybL-UG-dwT" secondAttribute="trailing" id="eBc-6g-nWr"/>
<constraint firstItem="NXU-yU-eST" firstAttribute="top" secondItem="QJp-6C-yoZ" secondAttribute="bottom" id="mnZ-WQ-LX8"/>
<constraint firstItem="NXU-yU-eST" firstAttribute="bottom" secondItem="ybL-UG-dwT" secondAttribute="bottom" id="vFS-tG-E43"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QiY-Mm-Dej" userLabel="Details">
<rect key="frame" x="0.0" y="264" width="288" height="91"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Details" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FR1-Nt-XuB">
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" fixedFrame="YES" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" bouncesZoom="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pql-H5-k6U">
<rect key="frame" x="0.0" y="24" width="288" height="67"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<string key="text">Start: 1970-01-01 01:00
End: 1970-01-01 02:00
Duration: 60:00</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="height" priority="250" constant="91" id="or7-9o-FZb"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="Guy-Ra-fpS" secondAttribute="width" id="PUH-xO-ZbD"/>
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="Guy-Ra-fpS" secondAttribute="width" id="U6e-10-j55"/>
<constraint firstItem="Guy-Ra-fpS" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="ZCJ-ol-1Jv"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="2yS-xK-Wac" firstAttribute="trailing" secondItem="fMa-Lq-tGz" secondAttribute="trailing" id="1io-bA-4p9"/>
<constraint firstItem="2yS-xK-Wac" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" id="Fv1-fO-22V"/>
<constraint firstItem="xdn-EU-IMx" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" constant="16" id="JuR-Ro-IPi"/>
<constraint firstItem="xdn-EU-IMx" firstAttribute="top" secondItem="2yS-xK-Wac" secondAttribute="bottom" constant="12" id="Lec-83-aaD"/>
<constraint firstItem="xdn-EU-IMx" firstAttribute="trailing" secondItem="fMa-Lq-tGz" secondAttribute="trailing" constant="-16" id="hhC-bL-G3S"/>
<constraint firstItem="xdn-EU-IMx" firstAttribute="bottom" secondItem="fMa-Lq-tGz" secondAttribute="bottom" constant="-10" id="p7W-sr-Wch"/>
<constraint firstItem="2yS-xK-Wac" firstAttribute="top" secondItem="fMa-Lq-tGz" secondAttribute="top" id="yKh-gv-mgg"/>
</constraints>
<viewLayoutGuide key="safeArea" id="fMa-Lq-tGz"/>
</view>
<connections>
<outlet property="buttonCancel" destination="TGg-60-wZW" id="5Ej-7t-jaD"/>
<outlet property="buttonSave" destination="rWg-hE-Ydl" id="zfM-kx-erX"/>
<outlet property="inputDetails" destination="pql-H5-k6U" id="NXm-8f-5E6"/>
<outlet property="inputNotes" destination="NXU-yU-eST" id="c2n-cG-aLq"/>
<outlet property="inputTitle" destination="OCX-wu-l5d" id="PeC-F5-4mx"/>
<outlet property="noteBottom" destination="vFS-tG-E43" id="Bxh-Tl-E2U"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="KN7-F1-BOL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="klV-Ed-xzV">
<connections>
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="iDb-kK-nli"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="2100" y="-550"/>
</scene>
<!--Logs-->
<scene sceneID="DxJ-8o-gTM">
<objects>
<tableViewController id="50g-BI-Q6S" customClass="TVCRecordingDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="cLV-Db-JxM">
<rect key="frame" x="0.0" y="0.0" width="320" height="377"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="PreviousRecordDetailCell" textLabel="rN0-kA-Eln" detailTextLabel="xRp-XG-oKf" style="IBUITableViewCellStyleValue1" id="ceT-cF-lLF">
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ceT-cF-lLF" id="c5Y-xg-hSL">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rN0-kA-Eln">
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xRp-XG-oKf">
<rect key="frame" x="260" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
<outlet property="delegate" destination="50g-BI-Q6S" id="LBY-sp-dg0"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Logs" id="AXT-fV-keV"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2800" y="-550"/>
</scene>
<!--Settings-->
<scene sceneID="OEQ-fb-haL">
<objects>
<navigationController id="dIk-JY-9vE" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="settings" id="dQu-wE-a8u"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yYW-rX-VnB">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="pdd-aM-sKl" kind="relationship" relationship="rootViewController" id="oMe-a0-xN7"/>
<segue destination="qdB-ZO-LHY" kind="relationship" relationship="rootViewController" id="qJW-Jc-O4D"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-21" y="-1245"/>
<point key="canvasLocation" x="0.0" y="150"/>
</scene>
<!--Settings-->
<scene sceneID="gEe-ny-NaU">
<objects>
<tableViewController id="qdB-ZO-LHY" customClass="TVCSettings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" bounces="NO" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="8kq-PY-wp7">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" bounces="NO" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="8kq-PY-wp7">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<sections>
<tableViewSection headerTitle="General Settings" id="w58-6X-Jea">
<tableViewSection headerTitle="VPN Proxy Settings" id="w58-6X-Jea">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ghM-ze-fvp">
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
<rect key="frame" x="0.0" y="55.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ghM-ze-fvp" id="d2v-vz-QIB">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmY-ot-lJW">
<rect key="frame" x="256" y="6" width="45" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmY-ot-lJW">
<rect key="frame" x="255" y="6.5" width="51" height="31"/>
<connections>
<action selector="toggleVPNProxy:" destination="qdB-ZO-LHY" eventType="valueChanged" id="y95-2Z-Uep"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="VPN Proxy enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Qha-4I-go0">
<rect key="frame" x="16" y="5" width="230" height="27"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="VPN Proxy enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qha-4I-go0">
<rect key="frame" x="16" y="12" width="147" height="20"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
<rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qyy-0U-yhd" id="Mfs-fu-W5k">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="9Ko-sD-7x0">
<rect key="frame" x="95" y="7" width="124" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Export DB"/>
<connections>
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
</connections>
</button>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="wzU-8s-HGb">
<rect key="frame" x="0.0" y="142.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="wzU-8s-HGb" id="aNM-6U-bho">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="S6B-i8-CoC">
<rect key="frame" x="94" y="7" width="125" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Delete all logs"/>
<connections>
<action selector="clearDatabaseResults:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="w0d-8F-GmN"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="kmY-ot-lJW" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Qha-4I-go0" secondAttribute="trailing" constant="8" id="Lnx-hC-xOx"/>
<constraint firstItem="kmY-ot-lJW" firstAttribute="trailing" secondItem="d2v-vz-QIB" secondAttribute="trailingMargin" id="Ylz-D4-hz4"/>
<constraint firstItem="Qha-4I-go0" firstAttribute="centerY" secondItem="d2v-vz-QIB" secondAttribute="centerY" id="dKE-By-qEu"/>
<constraint firstItem="kmY-ot-lJW" firstAttribute="centerY" secondItem="Qha-4I-go0" secondAttribute="centerY" id="dgh-tx-Y8a"/>
<constraint firstItem="Qha-4I-go0" firstAttribute="leading" secondItem="d2v-vz-QIB" secondAttribute="leadingMargin" id="rHx-0D-DPX"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
@@ -235,10 +582,10 @@
<tableViewSection headerTitle="Logging Filter" id="EcH-KA-eLE">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="settingsIgnoredCell" textLabel="UdM-Zm-G9p" detailTextLabel="bHb-Tw-nPR" style="IBUITableViewCellStyleValue2" id="fZR-we-Y0k">
<rect key="frame" x="0.0" y="242" width="320" height="43.5"/>
<rect key="frame" x="0.0" y="155.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fZR-we-Y0k" id="eqc-fj-p0d">
<rect key="frame" x="0.0" y="0.0" width="261" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="261" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Ignore" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="UdM-Zm-G9p">
@@ -258,14 +605,14 @@
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="q3B-Yi-1bx" kind="show" identifier="segueFilterIgnored" id="EzT-Xq-wka"/>
<segue destination="q3B-Yi-1bx" kind="push" identifier="segueFilterIgnored" id="EzT-Xq-wka"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="settingsBlockedCell" textLabel="fI0-Nt-Ucf" detailTextLabel="CGG-47-cdc" style="IBUITableViewCellStyleValue2" id="3pw-7c-M6R">
<rect key="frame" x="0.0" y="285.5" width="320" height="43.5"/>
<rect key="frame" x="0.0" y="199.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3pw-7c-M6R" id="Smv-n1-917">
<rect key="frame" x="0.0" y="0.0" width="261" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="261" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Block" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="fI0-Nt-Ucf">
@@ -285,11 +632,80 @@
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="q3B-Yi-1bx" kind="show" identifier="segueFilterBlocked" id="cOY-j0-75m"/>
<segue destination="q3B-Yi-1bx" kind="push" identifier="segueFilterBlocked" id="cOY-j0-75m"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Other Settings" id="wLR-T2-Qxm">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
<rect key="frame" x="0.0" y="299.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qyy-0U-yhd" id="Mfs-fu-W5k">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Ko-sD-7x0">
<rect key="frame" x="125" y="7" width="70" height="30"/>
<state key="normal" title="Export DB"/>
<connections>
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerX" secondItem="Mfs-fu-W5k" secondAttribute="centerX" id="LzG-xg-XTg"/>
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerY" secondItem="Mfs-fu-W5k" secondAttribute="centerY" id="SXw-dC-2kl"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="wzU-8s-HGb">
<rect key="frame" x="0.0" y="343.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="wzU-8s-HGb" id="aNM-6U-bho">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="S6B-i8-CoC">
<rect key="frame" x="74.5" y="7" width="171" height="30"/>
<state key="normal" title="Reset Introduction Alerts"/>
<connections>
<action selector="resetTutorialAlerts:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="0GX-Ko-bk2"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerY" secondItem="aNM-6U-bho" secondAttribute="centerY" id="Wet-iT-mke"/>
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerX" secondItem="aNM-6U-bho" secondAttribute="centerX" id="qM6-0t-1m4"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="a9C-Qy-pOf">
<rect key="frame" x="0.0" y="387.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="a9C-Qy-pOf" id="cUk-4x-Weg">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="17e-nR-aCh">
<rect key="frame" x="111.5" y="7" width="97" height="30"/>
<state key="normal" title="Delete all logs">
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="clearDatabaseResults:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="Rep-Do-4OQ"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="17e-nR-aCh" firstAttribute="centerX" secondItem="cUk-4x-Weg" secondAttribute="centerX" id="dU5-1x-ETF"/>
<constraint firstItem="17e-nR-aCh" firstAttribute="centerY" secondItem="cUk-4x-Weg" secondAttribute="centerY" id="nLq-yi-u2E"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="qdB-ZO-LHY" id="RH3-xR-dpC"/>
@@ -305,13 +721,13 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="VNK-Z0-T0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="684" y="127"/>
<point key="canvasLocation" x="700" y="150"/>
</scene>
<!--Domains-->
<scene sceneID="218-uP-X7b">
<objects>
<tableViewController id="q3B-Yi-1bx" customClass="TVCFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
@@ -339,102 +755,25 @@
<outlet property="delegate" destination="q3B-Yi-1bx" id="02X-f0-d1a"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Domains" id="FWA-IG-VIb"/>
<navigationItem key="navigationItem" title="Domains" id="FWA-IG-VIb">
<barButtonItem key="rightBarButtonItem" systemItem="add" id="RFW-bp-wwH">
<connections>
<action selector="addNewFilter" destination="q3B-Yi-1bx" id="JID-eH-y0p"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Xzo-dO-WpK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1389" y="127"/>
</scene>
<!--Settings-->
<scene sceneID="OEQ-fb-haL">
<objects>
<navigationController id="dIk-JY-9vE" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="settings" id="dQu-wE-a8u"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yYW-rX-VnB">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="qdB-ZO-LHY" kind="relationship" relationship="rootViewController" id="qJW-Jc-O4D"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-23" y="127"/>
</scene>
<!--Recordings-->
<scene sceneID="ODR-PD-nTU">
<objects>
<viewController id="hm5-7q-Zfi" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JYr-yE-eGS">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="Jq8-ke-k0B"/>
</view>
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="mGk-aq-MRP"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wfy-Tp-A9o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-21" y="-560"/>
</scene>
<!--Main-->
<scene sceneID="7Rl-BK-ry5">
<objects>
<tabBarController id="sfA-EG-18J" customClass="TBCMain" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="qza-ey-Iaz">
<rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="cGm-zQ-NnO" kind="presentation" identifier="welcome" id="aF0-OB-Mwx"/>
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
<segue destination="hm5-7q-Zfi" kind="relationship" relationship="viewControllers" id="pfK-BR-9lf"/>
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="AwW-3j-iAg"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-831" y="127"/>
</scene>
<!--View Controller-->
<scene sceneID="8iq-nV-o0O">
<objects>
<viewController id="cGm-zQ-NnO" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="FlS-lu-XEg">
<rect key="frame" x="0.0" y="0.0" width="320" height="548"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" selectable="NO" id="QWn-iX-27k">
<rect key="frame" x="16" y="20" width="288" height="508"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
<bool key="isElement" value="YES"/>
</accessibility>
<string key="text">Your data belongs to you. Therefore, monitoring and analysis take place on your device only. The app does not share any data with us or any other third-party.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="SJX-Gb-WTN"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nve-Iu-WIa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-831" y="841"/>
<point key="canvasLocation" x="1400" y="150"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="EzT-Xq-wka"/>
</inferredMetricsTieBreakers>
<resources>
<image name="journal" width="25" height="25"/>
<image name="settings" width="25" height="25"/>
<image name="tag" width="25" height="25"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="cOY-j0-75m"/>
</inferredMetricsTieBreakers>
</document>

View File

@@ -19,7 +19,9 @@ extension EditableRows where Self: UITableViewController {
let userInfo = editableRowUserInfo(index)
return editableRowActions(index).compactMap { a,t in
let x = UITableViewRowAction(style: a == .delete ? .destructive : .normal, title: t) { self.editableRowCallback($1, a, userInfo) }
x.backgroundColor = editableRowActionColor(index, a)
if let color = editableRowActionColor(index, a) {
x.backgroundColor = color
}
return x
}
}
@@ -123,3 +125,23 @@ extension TVCFilter : EditableRows {
getRowActionsIOS11(indexPath)
}
}
extension TVCPreviousRecords : EditableRows {
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
getRowActionsIOS9(indexPath)
}
@available(iOS 11.0, *)
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
getRowActionsIOS11(indexPath)
}
}
extension TVCRecordingDetails : EditableRows {
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
getRowActionsIOS9(indexPath)
}
@available(iOS 11.0, *)
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
getRowActionsIOS11(indexPath)
}
}

View File

@@ -0,0 +1,70 @@
import UIKit
struct QuickUI {
static func button(_ title: String, target: Any? = nil, action: Selector? = nil) -> UIButton {
let x = UIButton(type: .roundedRect)
x.setTitle(title, for: .normal)
x.titleLabel?.font = .preferredFont(forTextStyle: .body)
x.sizeToFit()
if let a = action { x.addTarget(target, action: a, for: .touchUpInside) }
if #available(iOS 10.0, *) {
x.titleLabel?.adjustsFontForContentSizeCategory = true
}
return x
}
static func image(_ img: UIImage?, frame: CGRect = CGRect.zero) -> UIImageView {
let x = UIImageView(frame: frame)
x.contentMode = .scaleAspectFit
x.image = img
return x
}
static func text(_ str: String, frame: CGRect = CGRect.zero) -> UITextView {
let x = UITextView(frame: frame)
x.font = .preferredFont(forTextStyle: .body) // .systemFont(ofSize: UIFont.systemFontSize)
x.isSelectable = false
x.isEditable = false
x.text = str
if #available(iOS 10.0, *) {
x.adjustsFontForContentSizeCategory = true
}
return x
}
static func text(attributed: NSAttributedString, frame: CGRect = CGRect.zero) -> UITextView {
let txt = self.text("", frame: frame)
txt.attributedText = attributed
return txt
}
}
extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body)
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
func h1(_ str: String) -> Self { normal(str, .title1) }
func h2(_ str: String) -> Self { normal(str, .title2) }
func h3(_ str: String) -> Self { normal(str, .title3) }
private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [
.font : withFont,
.foregroundColor : UIColor.sysFg
]))
return self
}
}
extension UIFont {
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: 0) // keep size as is
}
func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) }
}

View File

@@ -0,0 +1,199 @@
import UIKit
fileprivate let margin: CGFloat = 20
fileprivate let cornerRadius: CGFloat = 15
fileprivate let uniRect = CGRect(x: 0, y: 0, width: 500, height: 500)
class TutorialSheet: UIViewController, UIScrollViewDelegate {
public var buttonTitleNext: String = "Next"
public var buttonTitleDone: String = "Close"
private var priorIndex: Int?
private var lastAnchor: NSLayoutConstraint?
private var shouldAnimate: Bool = true
private var shouldCloseBlock: (() -> Bool)? = nil
private var didCloseBlock: (() -> Void)? = nil
private let sheetBg: UIView = {
let x = UIView(frame: uniRect)
x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
x.backgroundColor = .sysBg
x.layer.cornerRadius = cornerRadius
x.layer.shadowColor = UIColor.black.cgColor
x.layer.shadowRadius = 10
x.layer.shadowOpacity = 0.75
x.layer.shadowOffset = CGSize(width: 0, height: 4)
return x
}()
private let pager: UIPageControl = {
let x = UIPageControl(frame: uniRect)
x.frame.size.height = x.size(forNumberOfPages: 1).height
x.currentPageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.5)
x.pageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.25)
x.numberOfPages = 0
x.hidesForSinglePage = true
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
return x
}()
private let pageScroll: UIScrollView = {
let x = UIScrollView(frame: uniRect)
x.bounces = false
x.isPagingEnabled = true
x.showsVerticalScrollIndicator = false
x.showsHorizontalScrollIndicator = false
let content = UIView()
x.addSubview(content)
content.translatesAutoresizingMaskIntoConstraints = false
content.anchor([.left, .right, .top, .bottom], to: x)
content.anchor([.width, .height], to: x) | .defaultLow
return x
}()
private let button: UIButton = {
let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
x.contentEdgeInsets = UIEdgeInsets(all: 8)
return x
}()
// MARK: Init
required init?(coder: NSCoder) { super.init(coder: coder) }
required init() {
super.init(nibName: nil, bundle: nil)
view = makeControlUI()
modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self)
}
/// Present Tutorial Sheet Controller
/// - Parameter viewController: If set to `nil`, use main application as canvas. (Default: `nil`)
/// - Parameter animate: Use `present` and `dismiss` animations. (Default: `true`)
/// - Parameter shouldClose: Called before the view controller is dismissed. Return `false` to prevent the dismissal.
/// Use this block to extract user data from input fields. (Default: `nil`)
/// - Parameter didClose: Called after the view controller is completely dismissed (with animations).
/// Use this block to update UI and visible changes. (Default: `nil`)
func present(in viewController: UIViewController? = nil, animate: Bool = true, shouldClose: (() -> Bool)? = nil, didClose: (() -> Void)? = nil) {
guard let vc = viewController ?? UIApplication.shared.keyWindow?.rootViewController else {
return
}
shouldCloseBlock = shouldClose
didCloseBlock = didClose
shouldAnimate = animate
vc.present(self, animated: animate)
}
// MARK: Dynamic UI
@discardableResult func addSheet(_ closure: ((UIStackView) -> Void)? = nil) -> UIStackView {
pager.numberOfPages += 1
updateButtonTitle()
let x = UIStackView(frame: pageScroll.bounds)
x.translatesAutoresizingMaskIntoConstraints = false
x.axis = .vertical
x.backgroundColor = UIColor.black
x.isOpaque = true
guard let content = pageScroll.subviews.first else {
return x
}
let prev = content.subviews.last
content.addSubview(x)
x.anchor([.top, .width, .height], to: pageScroll)
x.leadingAnchor =&= (prev==nil ? content.leadingAnchor : prev!.trailingAnchor)
lastAnchor?.isActive = false
lastAnchor = (x.trailingAnchor =&= pageScroll.trailingAnchor)
closure?(x)
return x
}
// MARK: Static UI
private func makeControlUI() -> UIView {
pageScroll.delegate = self
sheetBg.addSubview(pager)
sheetBg.addSubview(pageScroll)
sheetBg.addSubview(button)
for x in sheetBg.subviews { x.translatesAutoresizingMaskIntoConstraints = false }
pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: cornerRadius/2) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor
button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
// button.centerXAnchor =&= sheetBg.centerXAnchor
let bg = UIView(frame: uniRect)
bg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
bg.addSubview(sheetBg)
let h: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : UIApplication.shared.statusBarFrame.height
sheetBg.frame = bg.frame.inset(by: UIEdgeInsets(all: margin, top: margin + h))
return bg
}
// MARK: Delegates
override func viewWillLayoutSubviews() {
priorIndex = pager.currentPage
}
@objc private func didChangeOrientation() {
if let i = priorIndex {
priorIndex = nil
switchToSheet(i, animated: false)
}
for case let x as UIStackView in pageScroll.subviews.first!.subviews {
x.axis = (x.frame.width > x.frame.height) ? .horizontal : .vertical
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.width
let new = Int((scrollView.contentOffset.x + w/2) / w)
if pager.currentPage != new {
pager.currentPage = new
updateButtonTitle()
}
}
@objc private func pagerDidChange(sender: UIPageControl) {
switchToSheet(sender.currentPage, animated: true)
}
private func switchToSheet(_ i: Int, animated: Bool) {
pageScroll.setContentOffset(CGPoint(x: CGFloat(i) * pageScroll.bounds.width, y: 0), animated: animated)
}
private func updateButtonTitle() {
let last = (pager.currentPage == pager.numberOfPages - 1)
let title = last ? buttonTitleDone : buttonTitleNext
if button.title(for: .normal) != title {
button.setTitle(title, for: .normal)
}
}
@objc private func buttonTapped() {
let next = pager.currentPage + 1
if next < pager.numberOfPages {
switchToSheet(next, animated: true)
} else {
if shouldCloseBlock?() ?? true {
dismiss(animated: shouldAnimate, completion: didCloseBlock)
}
}
}
}

View File

@@ -1,6 +1,7 @@
import UIKit
let DBWrp = DBWrapper()
fileprivate var AppDB: SQLiteDatabase? { get { try? SQLiteDatabase.open() } }
class DBWrapper {
private var latestModification: Timestamp = 0
@@ -29,7 +30,7 @@ class DBWrapper {
}
func dataF_list(_ filter: FilterOptions) -> [String] {
Q.sync() { dataF.compactMap { $1.contains(filter) ? $0 : nil } }
Q.sync() { dataF.compactMap { $1.contains(filter) ? $0 : nil } }.sorted()
}
func dataF_counts() -> (blocked: Int, ignored: Int) {
@@ -47,13 +48,14 @@ class DBWrapper {
// MARK: - Init
func initContentOfDB() {
QLog.Debug("SQLite path: \(URL.internalDB())")
DispatchQueue.global().async {
#if IOS_SIMULATOR
// self.generateTestData()
// DispatchQueue.main.async {
// // dont know why main queue is needed, wont start otherwise
// Timer.repeating(2, call: #selector(self.insertRandomEntry), on: self)
// }
self.generateTestData()
DispatchQueue.main.async {
// dont know why main queue is needed, wont start otherwise
Timer.repeating(2, call: #selector(self.insertRandomEntry), on: self)
}
#endif
self.dataF_init()
self.dataAB_init()
@@ -100,7 +102,7 @@ class DBWrapper {
// MARK: - Partial Update History
@objc private func syncNewestLogs() {
QLog.Debug("\(#function)")
//QLog.Debug("\(#function)")
#if !IOS_SIMULATOR
guard currentVPNState == .on else { return }
#endif
@@ -220,6 +222,32 @@ class DBWrapper {
}
// MARK: - Recordings
func listOfRecordings() -> [Recording] { AppDB?.allRecordings() ?? [] }
func recordingGetCurrent() -> Recording? { AppDB?.ongoingRecording() }
func recordingStartNew() -> Recording? { try? AppDB?.startNewRecording() }
func recordingStop(_ r: inout Recording) { AppDB?.stopRecording(&r) }
func recordingPersist(_ r: Recording) { AppDB?.persistRecordingLogs(r) }
func recordingDetails(_ r: Recording) -> [RecordLog] { AppDB?.getRecordingsLogs(r) ?? [] }
func recordingUpdate(_ r: Recording) {
AppDB?.updateRecording(r)
NotifyRecordingChanged.post((r, false))
}
func recordingDelete(_ r: Recording) {
if (try? AppDB?.deleteRecording(r)) == true {
NotifyRecordingChanged.post((r, true))
}
}
func recordingDeleteDetails(_ r: Recording, domain: String?) -> Bool {
((try? AppDB?.deleteRecordingLogs(r.id, matchingDomain: domain)) ?? 0) > 0
}
// MARK: - Helper methods
private func dataA_index(of domain: String) -> Int? {
@@ -270,7 +298,7 @@ extension DBWrapper {
}
@objc private func insertRandomEntry() {
QLog.Debug("Inserting 1 periodic log entry")
//QLog.Debug("Inserting 1 periodic log entry")
try? AppDB?.insertDNSQuery("\(arc4random() % 5).count.test.com", blocked: true)
}
}

View File

@@ -9,7 +9,7 @@ struct GroupedDomain {
struct FilterOptions: OptionSet {
let rawValue: Int32
static let none = FilterOptions(rawValue: 0)
static let none = FilterOptions([])
static let blocked = FilterOptions(rawValue: 1 << 0)
static let ignored = FilterOptions(rawValue: 1 << 1)
static let any = FilterOptions(rawValue: 0b11)
@@ -25,12 +25,9 @@ enum SQLiteError: Error {
// MARK: - SQLiteDatabase
var AppDB: SQLiteDatabase? { get { try? SQLiteDatabase.open() } }
class SQLiteDatabase {
private let dbPointer: OpaquePointer?
private init(dbPointer: OpaquePointer?) {
// print("SQLite path: \(basePath!.absoluteString)")
self.dbPointer = dbPointer
}
@@ -133,18 +130,15 @@ private extension SQLiteDatabase {
sqlite3_bind_text(stmt, col, (value as NSString).utf8String, -1, nil) == SQLITE_OK
}
func bindTextOrNil(_ stmt: OpaquePointer, _ col: Int32, _ value: String?) -> Bool {
sqlite3_bind_text(stmt, col, (value == nil) ? nil : (value! as NSString).utf8String, -1, nil) == SQLITE_OK
}
func readText(_ stmt: OpaquePointer, _ col: Int32) -> String? {
let val = sqlite3_column_text(stmt, col)
return (val != nil ? String(cString: val!) : nil)
}
func readGroupedDomain(_ stmt: OpaquePointer) -> GroupedDomain {
GroupedDomain(domain: readText(stmt, 0) ?? "",
total: sqlite3_column_int(stmt, 1),
blocked: sqlite3_column_int(stmt, 2),
lastModified: sqlite3_column_int64(stmt, 3))
}
func allRows<T>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> T) -> [T] {
var r: [T] = []
while (sqlite3_step(stmt) == SQLITE_ROW) { r.append(fn(stmt)) }
@@ -162,6 +156,8 @@ extension SQLiteDatabase {
func initScheme() {
try? self.createTable(table: DNSQueryT.self)
try? self.createTable(table: DNSFilterT.self)
try? self.createTable(table: Recording.self)
try? self.createTable(table: RecordingLog.self)
}
}
@@ -176,9 +172,9 @@ private struct DNSQueryT: SQLTable {
static var createStatement: String {
return """
CREATE TABLE IF NOT EXISTS req(
ts BIGINT DEFAULT (strftime('%s','now')),
domain VARCHAR(255) NOT NULL,
logOpt INT DEFAULT 0
ts INTEGER DEFAULT (strftime('%s','now')),
domain TEXT NOT NULL,
logOpt INTEGER DEFAULT 0
);
"""
}
@@ -217,6 +213,13 @@ extension SQLiteDatabase {
// MARK: read
func readGroupedDomain(_ stmt: OpaquePointer) -> GroupedDomain {
GroupedDomain(domain: readText(stmt, 0) ?? "",
total: sqlite3_column_int(stmt, 1),
blocked: sqlite3_column_int(stmt, 2),
lastModified: sqlite3_column_int64(stmt, 3))
}
func domainList(since ts: Timestamp = 0) -> [GroupedDomain]? {
try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req \(ts == 0 ? "" : "WHERE ts > ?") GROUP BY domain ORDER BY 4 DESC;", bind: {
ts == 0 || self.bindInt64($0, 1, ts)
@@ -251,8 +254,8 @@ private struct DNSFilterT: SQLTable {
static var createStatement: String {
return """
CREATE TABLE IF NOT EXISTS filter(
domain VARCHAR(255) UNIQUE NOT NULL,
opt INT DEFAULT 0
domain TEXT UNIQUE NOT NULL,
opt INTEGER DEFAULT 0
);
"""
}
@@ -263,7 +266,7 @@ extension SQLiteDatabase {
// MARK: read
func loadFilters() -> [String : FilterOptions]? {
try? run(sql: "SELECT domain, opt FROM filter ORDER BY domain ASC;", bind: nil) {
try? run(sql: "SELECT domain, opt FROM filter;", bind: nil) {
allRowsKeyed($0) {
(key: readText($0, 0) ?? "",
value: FilterOptions(rawValue: sqlite3_column_int($0, 1)))
@@ -302,3 +305,160 @@ extension SQLiteDatabase {
do { try createFilter() } catch { updateFilter() }
}
}
// MARK: - Recordings
struct Recording: SQLTable {
let id: sqlite3_int64
let start: Timestamp
let stop: Timestamp?
var appId: String? = nil
var title: String? = nil
var notes: String? = nil
static var createStatement: String {
return """
CREATE TABLE IF NOT EXISTS rec(
id INTEGER PRIMARY KEY,
start INTEGER DEFAULT (strftime('%s','now')),
stop INTEGER,
appid TEXT,
title TEXT,
notes TEXT
);
"""
}
}
extension SQLiteDatabase {
// MARK: write
func startNewRecording() throws -> Recording {
try run(sql: "INSERT INTO rec (stop) VALUES (NULL);", bind: nil) { stmt -> Recording in
try ifStep(stmt, SQLITE_DONE)
return try getRecording(withID: sqlite3_last_insert_rowid(dbPointer))
}
}
func stopRecording(_ r: inout Recording) {
guard r.stop == nil else { return }
let theID = r.id
try? run(sql: "UPDATE rec SET stop = (strftime('%s','now')) WHERE id = ? LIMIT 1;", bind: {
self.bindInt64($0, 1, theID)
}) { stmt -> Void in
try ifStep(stmt, SQLITE_DONE)
r = try getRecording(withID: theID)
}
}
func updateRecording(_ r: Recording) {
try? run(sql: "UPDATE rec SET title = ?, appid = ?, notes = ? WHERE id = ? LIMIT 1;", bind: {
self.bindTextOrNil($0, 1, r.title) && self.bindTextOrNil($0, 2, r.appId)
&& self.bindTextOrNil($0, 3, r.notes) && self.bindInt64($0, 4, r.id)
}) { stmt -> Void in
sqlite3_step(stmt)
}
}
func deleteRecording(_ r: Recording) throws -> Bool {
_ = try? deleteRecordingLogs(r.id)
return try run(sql: "DELETE FROM rec WHERE id = ? LIMIT 1;", bind: {
self.bindInt64($0, 1, r.id)
}) {
try ifStep($0, SQLITE_DONE)
return sqlite3_changes(dbPointer) > 0
}
}
// MARK: read
func readRecording(_ stmt: OpaquePointer) -> Recording {
let end = sqlite3_column_int64(stmt, 2)
return Recording(id: sqlite3_column_int64(stmt, 0),
start: sqlite3_column_int64(stmt, 1),
stop: end == 0 ? nil : end,
appId: readText(stmt, 3),
title: readText(stmt, 4),
notes: readText(stmt, 5))
}
func ongoingRecording() -> Recording? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NULL LIMIT 1;", bind: nil) {
try ifStep($0, SQLITE_ROW)
return readRecording($0)
}
}
func allRecordings() -> [Recording]? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NOT NULL;", bind: nil) {
allRows($0) { readRecording($0) }
}
}
func getRecording(withID: sqlite3_int64) throws -> Recording {
try run(sql: "SELECT * FROM rec WHERE id = ? LIMIT 1;", bind: {
self.bindInt64($0, 1, withID)
}) {
try ifStep($0, SQLITE_ROW)
return readRecording($0)
}
}
}
// MARK:
private struct RecordingLog: SQLTable {
let rID: Int32
let ts: Timestamp
let domain: String
static var createStatement: String {
return """
CREATE TABLE IF NOT EXISTS recLog(
rid INTEGER REFERENCES rec(id) ON DELETE CASCADE,
ts INTEGER,
domain TEXT
);
"""
}
}
extension SQLiteDatabase {
// MARK: write
func persistRecordingLogs(_ r: Recording) {
guard let end = r.stop else {
return
}
try? run(sql: """
INSERT INTO recLog (rid, ts, domain) SELECT ?, ts, domain FROM req
WHERE req.ts >= ? AND req.ts <= ?
""", bind: {
self.bindInt64($0, 1, r.id) && self.bindInt64($0, 2, r.start) && self.bindInt64($0, 3, end)
}) {
try ifStep($0, SQLITE_DONE)
}
}
func deleteRecordingLogs(_ recId: sqlite3_int64, matchingDomain d: String? = nil) throws -> Int32 {
try run(sql: "DELETE FROM recLog WHERE rid = ? \(d==nil ? "" : "AND domain = ?");", bind: {
self.bindInt64($0, 1, recId) && (d==nil ? true : self.bindTextOrNil($0, 2,d))
}) {
try ifStep($0, SQLITE_DONE)
return sqlite3_changes(dbPointer)
}
}
// MARK: read
func getRecordingsLogs(_ r: Recording) -> [RecordLog]? {
try? run(sql: "SELECT domain, COUNT() FROM recLog WHERE rid = ? GROUP BY domain;", bind: {
self.bindInt64($0, 1, r.id)
}) {
allRows($0) { (readText($0, 0), sqlite3_column_int($0, 1)) }
}
}
}
typealias RecordLog = (domain: String?, count: Int32)

View File

@@ -1,50 +1,67 @@
import UIKit
// MARK: Basic Alerts
func Alert(title: String?, text: String?, buttonText: String = "Dismiss") -> UIAlertController {
let alert = UIAlertController(title: title, message: text, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: buttonText, style: .cancel, handler: nil))
return alert
}
func ErrorAlert(_ error: Error, buttonText: String = "Dismiss") -> UIAlertController {
return Alert(title: "Error", text: error.localizedDescription, buttonText: buttonText)
}
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping () -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: "Cancel")
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action() })
return alert
}
extension UIAlertController {
func presentIn(_ viewController: UIViewController?) {
viewController?.present(self, animated: true, completion: nil)
}
}
// MARK: Basic Alerts
/// - Parameters:
/// - buttonText: Default: `"Dismiss"`
func Alert(title: String?, text: String?, buttonText: String = "Dismiss") -> UIAlertController {
let alert = UIAlertController(title: title, message: text, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: buttonText, style: .cancel, handler: nil))
return alert
}
/// - Parameters:
/// - buttonText: Default:`"Dismiss"`
func ErrorAlert(_ error: Error, buttonText: String = "Dismiss") -> UIAlertController {
return Alert(title: "Error", text: error.localizedDescription, buttonText: buttonText)
}
/// - Parameters:
/// - buttonText: Default: `"Dismiss"`
func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> UIAlertController {
return Alert(title: "Error", text: errorDescription, buttonText: buttonText)
}
/// - Parameters:
/// - buttonText: Default: `"Continue"`
/// - buttonStyle: Default: `.default`
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: "Cancel")
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
return alert
}
// MARK: Alert with multiple options
func AlertWithOptions(title: String?, text: String?, buttons: [String], lastIsDestructive: Bool = false, callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
/// - Parameters:
/// - buttons: Default: `[]`
/// - lastIsDestructive: Default: `false`
/// - cancelButtonText: Default: `"Dismiss"`
func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDestructive: Bool = false, cancelButtonText: String = "Dismiss", callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
let alert = UIAlertController(title: title, message: text, preferredStyle: .actionSheet)
for (i, btn) in buttons.enumerated() {
let dangerous = (lastIsDestructive && i + 1 == buttons.count)
alert.addAction(UIAlertAction(title: btn, style: dangerous ? .destructive : .default) { _ in callback(i) })
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in callback(nil) })
alert.addAction(UIAlertAction(title: cancelButtonText, style: .cancel) { _ in callback(nil) })
return alert
}
func AlertDeleteLogs(_ domain: String, latest: Timestamp, success: @escaping (_ tsMin: Timestamp) -> Void) -> UIAlertController {
let sinceNow = TimestampNow() - latest
let sinceNow = Timestamp.now() - latest
var buttons = ["Last 5 minutes", "Last 15 minutes", "Last hour", "Last 24 hours", "Delete everything"]
var times: [Timestamp] = [300, 900, 3600, 86400]
while times.count > 0, times[0] < sinceNow {
buttons.removeFirst()
times.removeFirst()
}
return AlertWithOptions(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: buttons, lastIsDestructive: true) {
return BottomAlert(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: buttons, lastIsDestructive: true, cancelButtonText: "Cancel") {
guard let idx = $0 else {
return
}

View File

@@ -0,0 +1,82 @@
import UIKit
/*
Readable Auto Layout Constraints
Usage:
A.anchor =&= multiplier * B.anchor + constant | priority
*/
infix operator =&= : AdditionPrecedence
infix operator =<= : AdditionPrecedence
infix operator =>= : AdditionPrecedence
//infix operator | : AdditionPrecedence
/// Create and activate an `equal` constraint between left and right anchor. Format: `A.anchor =&= multiplier * B.anchor + constant | priority`
@discardableResult func =&= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(equalTo: r).on() }
/// Create and activate a `lessThan` constraint between left and right anchor. Format: `A.anchor =<= multiplier * B.anchor + constant | priority`
@discardableResult func =<= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(lessThanOrEqualTo: r).on() }
/// Create and activate a `greaterThan` constraint between left and right anchor. Format: `A.anchor =>= multiplier * B.anchor + constant | priority`
@discardableResult func =>= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualTo: r).on() }
extension NSLayoutDimension { // higher precedence, so multiply first
/// Create intermediate anchor multiplier result.
static func *(l: CGFloat, r: NSLayoutDimension) -> AnchorMultiplier { .init(anchor: r, m: l) }
}
/// Intermediate `NSLayoutConstraint` anchor with multiplier supplement
struct AnchorMultiplier {
let anchor: NSLayoutDimension, m: CGFloat
/// Create and activate an `equal` constraint between left and right anchor. Format: `A.anchor =&= multiplier * B.anchor + constant | priority`
@discardableResult static func =&=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(equalTo: r.anchor, multiplier: r.m).on() }
/// Create and activate a `lessThan` constraint between left and right anchor. Format: `A.anchor =<= multiplier * B.anchor + constant | priority`
@discardableResult static func =<=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(lessThanOrEqualTo: r.anchor, multiplier: r.m).on() }
/// Create and activate a `greaterThan` constraint between left and right anchor. Format: `A.anchor =>= multiplier * B.anchor + constant | priority`
@discardableResult static func =>=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualTo: r.anchor, multiplier: r.m).on() }
}
extension NSLayoutConstraint {
/// Change `isActive`to `true` and return `self`
func on() -> Self { isActive = true; return self }
/// Change `constant`attribute and return `self`
@discardableResult static func +(l: NSLayoutConstraint, r: CGFloat) -> NSLayoutConstraint { l.constant = r; return l }
/// Change `constant` attribute and return `self`
@discardableResult static func -(l: NSLayoutConstraint, r: CGFloat) -> NSLayoutConstraint { l.constant = -r; return l }
/// Change `priority` attribute and return `self`
@discardableResult static func |(l: NSLayoutConstraint, r: UILayoutPriority) -> NSLayoutConstraint { l.priority = r; return l }
}
/*
UIView extension to generate multiple constraints at once
Usage:
child.anchor([.width, .height], to: parent) | .defaultLow
*/
extension UIView {
/// Edges that need the relation to flip arguments. For these we need to inverse the constant value and relation.
private static let inverseItem: [NSLayoutConstraint.Attribute] = [.right, .bottom, .trailing, .lastBaseline, .rightMargin, .bottomMargin, .trailingMargin]
/// Create and active constraints for provided edges. Constraints will anchor the same edge on both `self` and `other`.
/// - Parameters:
/// - edges: List of constraint attributes, e.g. `[.top, .bottom, .left, .right]`
/// - other: Instance to bind to, e.g. `UIView` or `UILayoutGuide`
/// - margin: Used as constant value. Multiplier will always be `1.0`. If you need to change the multiplier, use single constraints instead. (Default: `0`)
/// - rel: Constraint relation. (Default: `.equal`)
/// - Returns: List of created and active constraints
@discardableResult func anchor(_ edges: [NSLayoutConstraint.Attribute], to other: Any, margin: CGFloat = 0, if rel: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
edges.map {
let (A, B) = UIView.inverseItem.contains($0) ? (other, self) : (self, other)
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on()
}
}
}
extension Array where Element: NSLayoutConstraint {
/// set `priority` on all elements and return same list
@discardableResult static func |(l: Self, r: UILayoutPriority) -> Self {
for x in l { x.priority = r }
return l
}
}

View File

@@ -18,3 +18,15 @@ extension Array where Element == GroupedDomain {
return GroupedDomain(domain: domain, total: t, blocked: b, lastModified: m, options: opt)
}
}
extension Recording {
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } }
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } }
var durationString: String? { get { stop == nil ? nil : TimeFormat.from(duration!) } }
}
extension Timestamp {
func asDateTime() -> String { dateTimeFormat.string(from: self) }
func toDate() -> Date { Date(timeIntervalSince1970: Double(self)) }
static func now() -> Timestamp { Timestamp(Date().timeIntervalSince1970) }
}

View File

@@ -1,4 +1,4 @@
import Foundation
import UIKit
struct QLog {
private init() {}
@@ -55,15 +55,11 @@ extension String {
ending = rld + "." + ending
}
return (domain: ending, host: parts.joined(separator: "."))
// var allDots = enumerated().compactMap { $1 == "." ? $0 : nil }
// let d1 = allDots.popLast() // we dont care about TLD
// guard let d2 = allDots.popLast() else {
// return (domain: self, host: nil) // no subdomains, just plain SLD
// }
// // TODO: check third level domains
//// let d3 = allDots.popLast()
// return (String(suffix(count - d2 - 1)), String(prefix(d2)))
}
/// Returns `true` if String matches list of known second level domains (e.g., `co.uk`).
func isKnownSLD() -> Bool {
let parts = components(separatedBy: ".")
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
}
}
@@ -88,4 +84,30 @@ extension DateFormatter {
}
}
func TimestampNow() -> Timestamp { Timestamp(Date().timeIntervalSince1970) }
struct TimeFormat {
static func from(_ duration: Timestamp) -> String {
String(format: "%02d:%02d", duration / 60, duration % 60)
}
static func from(_ duration: TimeInterval, millis: Bool = false) -> String {
let t = Int(duration)
if millis {
let mil = Int(duration * 1000) % 1000
return String(format: "%02d:%02d.%03d", t / 60, t % 60, mil)
}
return String(format: "%02d:%02d", t / 60, t % 60)
}
static func since(_ date: Date, millis: Bool = false) -> String {
from(Date().timeIntervalSince(date), millis: millis)
}
}
extension UIColor {
static var sysBg: UIColor { get { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }}
static var sysFg: UIColor { get { if #available(iOS 13.0, *) { return .label } else { return .black } }}
}
extension UIEdgeInsets {
init(all: CGFloat = 0, top: CGFloat? = nil, left: CGFloat? = nil, bottom: CGFloat? = nil, right: CGFloat? = nil) {
self.init(top: top ?? all, left: left ?? all, bottom: bottom ?? all, right: right ?? all)
}
}

View File

@@ -3,6 +3,7 @@ import Foundation
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState!
let NotifyFilterChanged = NSNotification.Name("PSIFilterSettingsChanged") // nil!
let NotifyLogHistoryReset = NSNotification.Name("PSILogHistoryReset") // nil!
let NotifyRecordingChanged = NSNotification.Name("PSIRecordingChanged") // (Recording, deleted: Bool)!
extension NSNotification.Name {

View File

@@ -3,8 +3,8 @@ import UIKit
extension GroupedDomain {
var detailCellText: String { get {
return blocked > 0
? "\(dateTimeFormat.string(from: lastModified))\(blocked)/\(total) blocked"
: "\(dateTimeFormat.string(from: lastModified))\(total)"
? "\(lastModified.asDateTime())\(blocked)/\(total) blocked"
: "\(lastModified.asDateTime())\(total)"
}
}
}
@@ -59,29 +59,29 @@ extension IncrementalDataSourceUpdate {
func insertRow(_ obj: GroupedDomain, at index: Int) {
dataSource.insert(obj, at: index)
ifDisplayed {
self.tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .left)
self.tableView.insertRows(at: [IndexPath(row: index)], with: .left)
}
}
func moveRow(_ obj: GroupedDomain, from: Int, to: Int) {
dataSource.remove(at: from)
dataSource.insert(obj, at: to)
ifDisplayed {
let source = IndexPath(row: from, section: 0)
let source = IndexPath(row: from)
let cell = self.tableView.cellForRow(at: source)
cell?.detailTextLabel?.text = obj.detailCellText
self.tableView.moveRow(at: source, to: IndexPath(row: to, section: 0))
self.tableView.moveRow(at: source, to: IndexPath(row: to))
}
}
func replaceRow(_ obj: GroupedDomain, at index: Int) {
dataSource[index] = obj
ifDisplayed {
self.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
self.tableView.reloadRows(at: [IndexPath(row: index)], with: .automatic)
}
}
func deleteRow(at index: Int) {
dataSource.remove(at: index)
ifDisplayed {
self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
self.tableView.deleteRows(at: [IndexPath(row: index)], with: .automatic)
}
}
func replaceData(with newData: [GroupedDomain]) {
@@ -91,3 +91,8 @@ extension IncrementalDataSourceUpdate {
}
}
}
extension IndexPath {
/// Convenience init with `section: 0`
public init(row: Int) { self.init(row: row, section: 0) }
}

View File

@@ -10,14 +10,10 @@ fileprivate extension FileManager {
func internalDB() -> URL {
appGroupDir().appendingPathComponent("dns-logs.sqlite")
}
func appGroupIPC() -> URL {
appGroupDir().appendingPathComponent("data-exchange.dat")
}
}
extension URL {
static func exportDir() -> URL { FileManager.default.exportDir() }
static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
static func internalDB() -> URL { FileManager.default.internalDB() }
static func appGroupIPC() -> URL { FileManager.default.appGroupIPC() }
}

View File

@@ -0,0 +1,83 @@
import UIKit
class TVCPreviousRecords: UITableViewController, EditActionsRemove {
private var dataSource: [Recording] = []
override func viewDidLoad() {
dataSource = DBWrp.listOfRecordings().reversed() // newest on top
NotifyRecordingChanged.observe(call: #selector(recordingDidChange(_:)), on: self)
}
func insertAndEditRecording(_ r: Recording) {
insertNewRecord(r)
editRecord(r, isNewRecording: true)
}
@objc private func recordingDidChange(_ notification: Notification) {
let (new, deleted) = notification.object as! (Recording, Bool)
if let i = dataSource.firstIndex(where: { $0.id == new.id }) {
if deleted {
dataSource.remove(at: i)
tableView.deleteRows(at: [IndexPath(row: i)], with: .automatic)
} else {
dataSource[i] = new
tableView.reloadRows(at: [IndexPath(row: i)], with: .automatic)
}
} else if !deleted {
insertNewRecord(new)
}
}
private func insertNewRecord(_ record: Recording) {
dataSource.insert(record, at: 0)
tableView.insertRows(at: [IndexPath(row: 0)], with: .top)
}
// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
editRecord(dataSource[indexPath.row])
}
private func editRecord(_ record: Recording, isNewRecording: Bool = false) {
performSegue(withIdentifier: "editRecordSegue", sender: (record, isNewRecording))
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editRecordSegue" {
let (record, newlyCreated) = sender as! (Recording, Bool)
let target = segue.destination as! VCEditRecording
target.record = record
target.deleteOnCancel = newlyCreated
} else if segue.identifier == "openRecordDetailsSegue" {
if let i = tableView.indexPathForSelectedRow {
(segue.destination as? TVCRecordingDetails)?.record = dataSource[i.row]
}
}
}
// MARK: - Table View Data Source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PreviousRecordCell")!
let x = dataSource[indexPath.row]
cell.textLabel?.text = x.title ?? x.fallbackTitle
cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil
cell.detailTextLabel?.text = "at \(x.start.asDateTime()), duration: \(x.durationString ?? "?")"
return cell
}
// MARK: - Editing
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
DBWrp.recordingDelete(self.dataSource[index.row])
return true
}
}

View File

@@ -0,0 +1,37 @@
import UIKit
class TVCRecordingDetails: UITableViewController, EditActionsRemove {
var record: Recording!
private var dataSource: [RecordLog]!
override func viewDidLoad() {
title = record.title ?? record.fallbackTitle
dataSource = DBWrp.recordingDetails(record)
}
// MARK: - Table View Data Source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PreviousRecordDetailCell")!
let x = dataSource[indexPath.row]
cell.textLabel?.text = x.domain
cell.detailTextLabel?.text = "\(x.count)"
return cell
}
// MARK: - Editing
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
if DBWrp.recordingDeleteDetails(record, domain: self.dataSource[index.row].domain) {
self.dataSource.remove(at: index.row)
self.tableView.deleteRows(at: [index], with: .automatic)
}
return true
}
}

View File

@@ -0,0 +1,125 @@
import UIKit
class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate {
var record: Recording!
var deleteOnCancel: Bool = false
@IBOutlet private var buttonCancel: UIBarButtonItem!
@IBOutlet private var buttonSave: UIBarButtonItem!
@IBOutlet private var inputTitle: UITextField!
@IBOutlet private var inputNotes: UITextView!
@IBOutlet private var inputDetails: UITextView!
@IBOutlet private var noteBottom: NSLayoutConstraint!
override func viewDidLoad() {
inputTitle.placeholder = record.fallbackTitle
inputTitle.text = record.title
inputNotes.text = record.notes
inputDetails.text = """
Start: \(record.start.asDateTime())
End: \(record.stop?.asDateTime() ?? "?")
Duration: \(record.durationString ?? "?")
"""
validateSaveButton()
if deleteOnCancel { // mark as destructive
buttonCancel.tintColor = .systemRed
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
}
UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self)
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)
}
// MARK: Save & Cancel Buttons
@IBAction func didTapSave(_ sender: UIBarButtonItem) {
if deleteOnCancel { // aka newly created
// if remains true, `viewDidDisappear` will delete the record
deleteOnCancel = false
}
QLog.Debug("updating record #\(record.id)")
record.title = (inputTitle.text == "") ? nil : inputTitle.text
record.notes = (inputNotes.text == "") ? nil : inputNotes.text
dismiss(animated: true) {
DBWrp.recordingUpdate(self.record)
DBWrp.recordingPersist(self.record)
}
}
@IBAction func didTapCancel(_ sender: UIBarButtonItem) {
QLog.Debug("discard edit of record #\(record.id)")
dismiss(animated: true)
}
override func viewDidDisappear(_ animated: Bool) {
if deleteOnCancel {
QLog.Debug("deleting record #\(record.id)")
DBWrp.recordingDelete(record)
deleteOnCancel = false
}
}
// MARK: Handle Keyboard & Notes Frame
private var isEditingNotes: Bool = false
private var keyboardHeight: CGFloat = 0
@IBAction func hideKeyboard() { view.endEditing(false) }
func textViewDidBeginEditing(_ textView: UITextView) {
if textView == inputNotes {
isEditingNotes = true
updateKeyboard()
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView == inputNotes {
isEditingNotes = false
updateKeyboard()
}
}
@objc func keyboardWillShow(_ notification: NSNotification) {
keyboardHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0
updateKeyboard()
}
@objc func keyboardWillHide(_ notification: NSNotification) {
keyboardHeight = 0
updateKeyboard()
}
private func updateKeyboard() {
guard let parent = inputNotes.superview, let stack = parent.superview else {
return
}
let adjust = (isEditingNotes && keyboardHeight > 0)
stack.subviews.forEach{ $0.isHidden = (adjust && $0 != parent) }
let title = parent.subviews.first as! UILabel
title.font = .preferredFont(forTextStyle: adjust ? .subheadline : .title2)
title.sizeToFit()
title.frame.size.width = parent.frame.width
noteBottom.constant = adjust ? view.frame.height - stack.frame.maxY - keyboardHeight : 0
}
// MARK: TextField & TextView Delegate
func textFieldDidChangeSelection(_ _: UITextField) { validateSaveButton() }
func textViewDidChange(_ _: UITextView) { validateSaveButton() }
private func validateSaveButton() {
let changed = (inputTitle.text != record.title ?? "" || inputNotes.text != record.notes ?? "")
buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField == inputTitle ? inputNotes.becomeFirstResponder() : true
}
}

View File

@@ -0,0 +1,133 @@
import UIKit
class VCRecordings: UIViewController, UINavigationControllerDelegate {
private var currentRecording: Recording?
private var recordingTimer: Timer?
@IBOutlet private var timeLabel: UILabel!
@IBOutlet private var startButton: UIButton!
@IBOutlet private var startNewRecView: UIView!
private var prevRecController: UINavigationController!
override func viewDidLoad() {
prevRecController = (children.first as! UINavigationController)
prevRecController.delegate = self
// Duplicate font attributes but set monospace
let traits = timeLabel.font.fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
let weight = traits[.weight] as? CGFloat ?? UIFont.Weight.regular.rawValue
timeLabel.font = UIFont.monospacedDigitSystemFont(ofSize: timeLabel.font.pointSize, weight: UIFont.Weight(rawValue: weight))
// hide timer if not running
updateUI(setRecording: false, animated: false)
currentRecording = DBWrp.recordingGetCurrent()
if !UserDefaults.standard.bool(forKey: "didShowTutorialRecordings") {
self.perform(#selector(showTutorial), with: nil, afterDelay: 0.5)
}
}
override func viewDidAppear(_ animated: Bool) {
if currentRecording != nil { startTimer(animate: false) }
}
override func viewWillDisappear(_ animated: Bool) {
stopTimer(animate: false)
}
func navigationController(_ navigationController: UINavigationController, willShow vc: UIViewController, animated: Bool) {
let isRoot = (vc == navigationController.viewControllers.first)
UIView.animate(withDuration: 0.3) {
self.startNewRecView.isHidden = !isRoot // hide "new recording" if details open
}
}
// MARK: Start New Recording
@IBAction private func startRecordingButtonTapped(_ sender: UIButton) {
if recordingTimer == nil {
currentRecording = DBWrp.recordingStartNew()
startTimer(animate: true)
} else {
stopTimer(animate: true)
DBWrp.recordingStop(&currentRecording!)
prevRecController.popToRootViewController(animated: true)
let editVC = (prevRecController.topViewController as! TVCPreviousRecords)
editVC.insertAndEditRecording(currentRecording!)
currentRecording = nil // otherwise it will restart
}
}
private func startTimer(animate: Bool) {
guard let r = currentRecording, r.stop == nil else {
return
}
recordingTimer = Timer.repeating(0.086, call: #selector(timerCallback(_:)), on: self, userInfo: r.start.toDate())
updateUI(setRecording: true, animated: animate)
}
@objc private func timerCallback(_ sender: Timer) {
timeLabel.text = TimeFormat.since(sender.userInfo as! Date, millis: true)
}
private func stopTimer(animate: Bool) {
recordingTimer?.invalidate()
recordingTimer = nil
updateUI(setRecording: false, animated: animate)
}
private func updateUI(setRecording: Bool, animated: Bool) {
let title = setRecording ? "Stop Recording" : "Start New Recording"
let color = setRecording ? UIColor.systemRed : nil
let yT = setRecording ? 0 : -timeLabel.frame.height
let yB = (setRecording ? 1 : 0.5) * (startButton.superview!.frame.height - startButton.frame.height)
if !animated { // else title will flash
startButton.titleLabel?.text = title
}
UIView.animate(withDuration: animated ? 0.3 : 0) {
self.timeLabel.frame.origin.y = yT
self.startButton.frame.origin.y = yB
self.startButton.setTitle(title, for: .normal)
self.startButton.setTitleColor(color, for: .normal)
}
}
// MARK: Tutorial View Controller
@objc private func showTutorial() {
let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("What are Recordings?\n")
.normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " +
"Recordings are usually 3  5 minutes long and cover a single application. " +
"You can utilize recordings for App analysis or to get a ground truth for background traffic." +
"\n\n" +
"Optionally, you can help us by providing app specific recordings. " +
"Together with your findings we can create a community driven privacy monitor. " +
"The research results will help you and others avoid Apps that unnecessarily share data with third-party providers.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How to record?\n")
.normal("\nBefore you begin a new recording make sure that you quit all running applications. " +
"Tap on the 'Start Recording' button and switch to the application you'd like to inspect. " +
"Use the App as you would normally. Try to get to all corners and functionality the App provides. " +
"When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording." +
"\n\n" +
"Upon completion you will find your recording in the 'Previous Recordings' section. " +
"You can review your results and remove user specific information if necessary.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("Share results\n")
.normal("\nThis step is completely ").bold("optional").normal(". " +
"You can choose to share your results with us. " +
"We can compare similar applications and suggest privacy friendly alternatives. " +
"Together with other likeminded individuals we can increase the awareness for privacy friendly design." +
"\n\n" +
"Thank you very much.")
))
x.buttonTitleDone = "Got it"
x.present {
UserDefaults.standard.set(true, forKey: "didShowTutorialRecordings")
}
}
}

View File

@@ -25,7 +25,7 @@ class TVCHostDetails: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")!
let src = dataSource[indexPath.row]
cell.textLabel?.text = dateTimeFormat.string(from: src.ts)
cell.textLabel?.text = src.ts.asDateTime()
cell.imageView?.image = (src.blocked ? UIImage(named: "shield-x") : nil)
return cell
}

View File

@@ -6,9 +6,9 @@ class TVCFilter: UITableViewController, EditActionsRemove {
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 10.0, *) {
tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self)
}
// if #available(iOS 10.0, *) {
// tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self)
// }
NotifyFilterChanged.observe(call: #selector(reloadDataSource), on: self)
reloadDataSource()
}
@@ -18,6 +18,27 @@ class TVCFilter: UITableViewController, EditActionsRemove {
tableView.reloadData()
}
@IBAction private func addNewFilter() {
let desc: String
switch currentFilter {
case .blocked: desc = "Enter the domain name you wish to block."
case .ignored: desc = "Enter the domain name you wish to ignore."
default: return
}
let alert = AskAlert(title: "Create new filter", text: desc, buttonText: "Add") {
guard let dom = $0.textFields?.first?.text else {
return
}
guard dom.contains("."), !dom.isKnownSLD() else {
ErrorAlert("Entered domain is not valid. Filter can't match country TLD only.").presentIn(self)
return
}
DBWrp.updateFilter(dom, add: self.currentFilter)
}
alert.addTextField { $0.placeholder = "cdn.domain.tld" }
alert.presentIn(self)
}
// MARK: - Table View Delegate
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count }
@@ -25,16 +46,47 @@ class TVCFilter: UITableViewController, EditActionsRemove {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DomainFilterCell")!
cell.textLabel?.text = dataSource[indexPath.row]
if cell.gestureRecognizers?.isEmpty ?? true {
cell.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongTap)))
}
return cell
}
// MARK: - Editing
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
let domain = self.dataSource[index.row]
let domain = dataSource[index.row]
DBWrp.updateFilter(domain, remove: currentFilter)
self.dataSource.remove(at: index.row)
self.tableView.deleteRows(at: [index], with: .automatic)
dataSource.remove(at: index.row)
tableView.deleteRows(at: [index], with: .automatic)
return true
}
// MARK: - Long Press Gesture
private var cellTitleCopy: String?
@objc private func didLongTap(_ sender: UILongPressGestureRecognizer) {
guard let cell = sender.view as? UITableViewCell else {
return
}
if sender.state == .began {
cellTitleCopy = cell.textLabel?.text
self.becomeFirstResponder()
let menu = UIMenuController.shared
// menu.setTargetRect(CGRect(origin: sender.location(in: cell), size: CGSize.zero), in: cell)
menu.setTargetRect(cell.bounds, in: cell)
menu.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool { get { true } }
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
action == #selector(UIResponderStandardEditActions.copy)
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = cellTitleCopy
cellTitleCopy = nil
}
}

View File

@@ -52,11 +52,18 @@ class TVCSettings: UITableViewController {
// }.presentIn(self)
}
@IBAction func resetTutorialAlerts(_ sender: UIButton) {
UserDefaults.standard.removeObject(forKey: "didShowTutorialAppWelcome")
UserDefaults.standard.removeObject(forKey: "didShowTutorialRecordings")
Alert(title: sender.titleLabel?.text,
text: "\nDone.\n\nYou may need to restart the application.").presentIn(self)
}
@IBAction func clearDatabaseResults(_ sender: Any) {
AskAlert(title: "Clear results?", text: """
You are about to delete all results that have been logged in the past. Your preference for blocked and ignored domains is preserved.
Continue?
""", buttonText: "Delete", buttonStyle: .destructive) {
AskAlert(title: "Clear results?", text:
"You are about to delete all results that have been logged in the past. " +
"Your preferences for blocked and ignored domains are preserved.\n" +
"Continue?", buttonText: "Delete", buttonStyle: .destructive) { _ in
DBWrp.deleteHistory()
}.presentIn(self)
}

View File

@@ -5,13 +5,40 @@ class TBCMain: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// perform(#selector(showWelcomeMessage), with: nil, afterDelay: 3)
NotifyVPNStateChanged.observe(call: #selector(vpnStateChanged(_:)), on: self)
changedState(currentVPNState)
if !UserDefaults.standard.bool(forKey: "didShowTutorialAppWelcome") {
self.perform(#selector(showWelcomeMessage), with: nil, afterDelay: 0.5)
}
}
@objc func showWelcomeMessage() {
performSegue(withIdentifier: "welcome", sender: nil)
let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("Welcome\n")
.normal("\nAppCheck helps you identify which applications communicate with third parties. " +
"It does so by logging network requests. " +
"AppCheck learns only the destination addresses, not the actual data that is exchanged." +
"\n\n" +
"Your data belongs to you. " +
"Therefore, monitoring and analysis take place on your device only. " +
"The app does not share any data with us or any other third-party. " +
"Unless you choose to.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How it works\n")
.normal("\nAppCheck creates a local VPN tunnel to intercept all network connections. " +
"For each connection AppCheck looks into the DNS headers only, namely the domain names. " +
"\n" +
"These domain names are logged in the background while the VPN is active. " +
"That means, AppCheck does not have to be active in the foreground. " +
"You can close the app and come back later to see the results."
)
))
x.present {
UserDefaults.standard.set(true, forKey: "didShowTutorialAppWelcome")
}
}
@objc func vpnStateChanged(_ notification: Notification) {