66 Commits

Author SHA1 Message Date
relikd
5d94fe3a0d Version 1.0.0 (28) 2020-09-08 11:56:02 +02:00
relikd
fb680d669b Fix crash on loading App Store search results 2020-09-08 11:52:07 +02:00
relikd
6409e5eaf3 Allow to contribute empty recordings 2020-09-08 04:28:07 +02:00
relikd
39ca9dbdb1 Persist recording logs before save operation (crash-safe) 2020-09-08 03:16:38 +02:00
relikd
27ab2a621a Set recording time as filter 2020-09-08 02:51:25 +02:00
relikd
3f572eeb15 Version 1.0.0 (27) 2020-09-06 10:13:01 +02:00
relikd
e83540d5de Fix empty json log 2020-09-05 23:32:57 +02:00
relikd
847556bec1 Add important notice to app recording 2020-09-05 22:26:04 +02:00
relikd
42b045fb85 Open co-occurrence from recording 2020-09-05 22:07:22 +02:00
relikd
35a211f87f Fix action target self-reference timing issues 2020-09-05 22:05:56 +02:00
relikd
d2fa67e0e3 Reduce redundant code, cell copy menu 2020-09-05 21:05:12 +02:00
relikd
b8660c9a35 Jump from Recordings to Requests tab 2020-09-05 20:08:37 +02:00
relikd
8cd3f7fb3a Fix iOS 10 layout issues 2020-09-04 09:14:35 +02:00
relikd
2ee0272a05 Improve recording contribution view. Replace TextView with interactive TableView. 2020-09-04 09:14:23 +02:00
relikd
4ae82fc763 Show recording how-to at least once after app install 2020-08-31 23:02:46 +02:00
relikd
aac42d7eff Fix duration 2020-08-31 22:42:55 +02:00
relikd
8bb77ef741 Tiny markdown parser, makes tutorial screens editing much simpler 2020-08-31 17:10:11 +02:00
relikd
ff4218981f Discard recording if time criteria not met 2020-08-31 12:18:36 +02:00
relikd
7b7c5f3d9a UI app recording vs. background recording 2020-08-30 00:03:15 +02:00
relikd
1c203e39c3 Fix iOS 10 Tutorial sheet top padding missing 2020-08-29 23:46:13 +02:00
relikd
7dbf21d564 Disable block & ignore filter during recording 2020-08-29 18:36:41 +02:00
relikd
8fcb5ad874 No VPN, no recording 2020-08-29 17:49:30 +02:00
relikd
b4bf705b7f Rename column uploadkey 2020-08-29 14:48:53 +02:00
relikd
69d8321180 check status 'ok' 2020-08-29 14:44:40 +02:00
relikd
b03daeca66 Store sharing key instead of just a bool 2020-08-28 23:41:08 +02:00
relikd
c502484bcf Indicate shared on recordings overview + move isShared check to sharing sheet 2020-08-28 23:02:10 +02:00
relikd
448d69c6d8 Show "no results" in recordings + mark recording as shared 2020-08-28 22:05:49 +02:00
relikd
42aa7cf926 Contribute recording 2020-08-28 18:36:52 +02:00
relikd
52fa2e460e Fix: Wait for busy lock instead of instantly dropping the operation 2020-08-24 00:58:50 +02:00
relikd
8855ae754a Fix: Auto-delete logs did not clear heap 2020-08-24 00:56:14 +02:00
relikd
908a909c87 Share results screen 2020-08-12 18:15:32 +02:00
relikd
41aee797a9 Split storyboard tabs 2020-08-11 20:10:13 +02:00
relikd
685f636d5b Recordings: Choose app instead of custom title 2020-08-11 19:21:07 +02:00
relikd
4af56b0cb1 Reverting to single step persist 2020-08-01 09:42:57 +02:00
relikd
a3973c7e9a Embed Recordings in navigation controller, not the other way around 2020-08-01 09:33:48 +02:00
relikd
b270f30f3c Version 1.0.0 (26) 2020-07-28 15:20:30 +02:00
relikd
03177cee0b Invalidate restart reminder when VPN is running 2020-07-28 15:19:47 +02:00
relikd
9ee094dc20 Version 1.0.0 (25) 2020-07-28 14:50:59 +02:00
relikd
b1d49c6765 Persist logs by renaming table (hopefully reduces lock time) 2020-07-27 21:16:14 +02:00
relikd
b774e2152c Don't perform notification open action if modal window is open 2020-07-27 19:23:16 +02:00
relikd
e398ac8bcd Let notification open domain 2020-07-27 19:06:44 +02:00
relikd
01523b250f Proper VPN simulator with notifications, etc. 2020-07-27 17:50:15 +02:00
relikd
a2b0f311d5 First version with app notifications 2020-07-26 22:32:11 +02:00
relikd
88a52fb92c Version 1.0.0 (24) 2020-07-02 14:12:26 +02:00
relikd
723f1665a7 fittingSize() 2020-07-02 12:26:34 +02:00
relikd
4f92d3d58d Co-Occurrence on domain level 2020-07-02 12:26:07 +02:00
relikd
05d06a4f31 Update readme 2020-07-01 13:31:29 +02:00
relikd
f9ab545e0f Version 1.0.0 (23) 2020-07-01 12:30:22 +02:00
relikd
b10d4c8b36 Show database file size in settings 2020-07-01 11:47:15 +02:00
relikd
5a3ca024f8 Vacuum before export 2020-07-01 10:57:50 +02:00
relikd
92216c0c03 CustomAlert refactoring. Using proper UIPresentationController with adaptive margins 2020-07-01 00:53:25 +02:00
relikd
9ece3474c6 Limit CustomAlert to screen size & cut padding in half if necessary 2020-06-29 00:34:15 +02:00
relikd
6dcc2086e6 Auto-delete logs finished + custom App-to-VPN messages 2020-06-28 23:55:08 +02:00
relikd
08483711e2 Remove two unimportant and verbose error logs 2020-06-28 21:17:37 +02:00
relikd
0e100006d3 Moving extensions around 2020-06-28 17:04:48 +02:00
relikd
710c617862 Move VPN manager logic into its own controller 2020-06-28 16:31:11 +02:00
relikd
3ed25c92cd Render assets as template image 2020-06-28 14:34:16 +02:00
relikd
f7644e6048 Rename Pref -> Prefs 2020-06-28 14:33:36 +02:00
relikd
80afa6aff1 Privacy: Auto-delete logs (no functionality yet) 2020-06-28 14:20:31 +02:00
relikd
43de81929f Alerts with custom views 2020-06-28 01:06:06 +02:00
relikd
e315e71d07 Storyboard constantly trying to replace floats with rounding error 2020-06-27 16:27:48 +02:00
relikd
416eb34799 Reverse context analysis sort order (oldest first) 2020-06-27 16:20:20 +02:00
relikd
b7b13f51b2 Recordings: Toggle between raw logs and summary 2020-06-27 16:13:58 +02:00
relikd
2312187670 DB readText -> col_text 2020-06-27 00:54:50 +02:00
relikd
c7d0dc7c5f UIColor.sysFg -> UIColor.sysLabel 2020-06-27 00:50:47 +02:00
relikd
895cabee80 Context analysis: +/-5min raw logs 2020-06-27 00:40:29 +02:00
125 changed files with 6889 additions and 2440 deletions

View File

@@ -7,31 +7,77 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; };
5404AEED24A95F3F003B2F54 /* SlideInAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */; };
5404AEEF24ACC089003B2F54 /* VCAnalysisBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */; };
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E677F242D2CF100871BBE /* VCRecordings.swift */; }; 540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E677F242D2CF100871BBE /* VCRecordings.swift */; };
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67812433483D00871BBE /* VCEditRecording.swift */; }; 540E67822433483D00871BBE /* VCEditRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67812433483D00871BBE /* VCEditRecording.swift */; };
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */; }; 540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */; };
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075CD24C9D43A00D6F1BF /* UNNotification.swift */; };
541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075CD24C9D43A00D6F1BF /* UNNotification.swift */; };
541075D124CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */; };
541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */; };
541075D524CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */; };
541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */; };
541075D924CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */; };
541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */; };
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */; }; 5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */; };
5412FCC224C628FA000DE429 /* TVCReminderAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */; };
5412FCC324C628FA000DE429 /* TVCChooseAlertTone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */; };
5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */; };
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 541A957523E602DF00C09C19 /* LaunchIcon.png */; }; 541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 541A957523E602DF00C09C19 /* LaunchIcon.png */; };
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541AC5D72399498A00A769D7 /* AppDelegate.swift */; }; 541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541AC5D72399498A00A769D7 /* AppDelegate.swift */; };
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DB2399498A00A769D7 /* Main.storyboard */; }; 541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DB2399498A00A769D7 /* Main.storyboard */; };
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DE2399498B00A769D7 /* Assets.xcassets */; }; 541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DE2399498B00A769D7 /* Assets.xcassets */; };
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */; }; 541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */; };
541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541DCA6024A6B0F6005F1A4B /* Color.swift */; };
541FC47624A12D01009154D8 /* IBViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47524A12D01009154D8 /* IBViews.swift */; }; 541FC47624A12D01009154D8 /* IBViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47524A12D01009154D8 /* IBViews.swift */; };
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */; }; 541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */; };
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC9862497D81C00962623 /* TheGreatDestroyer.swift */; }; 541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC9862497D81C00962623 /* TheGreatDestroyer.swift */; };
542E2A982404973F001462DC /* TBCMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A972404973F001462DC /* TBCMain.swift */; }; 542E2A982404973F001462DC /* TBCMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A972404973F001462DC /* TBCMain.swift */; };
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A9924051556001462DC /* TVCSettings.swift */; }; 543078AA24B5E12500278F2D /* snap2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5430789F24B5E12200278F2D /* snap2.caf */; };
543078AB24B5E12500278F2D /* snap2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5430789F24B5E12200278F2D /* snap2.caf */; };
543078AC24B5E12500278F2D /* typewriter2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A024B5E12200278F2D /* typewriter2.caf */; };
543078AD24B5E12500278F2D /* typewriter2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A024B5E12200278F2D /* typewriter2.caf */; };
543078AE24B5E12500278F2D /* wood1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A124B5E12300278F2D /* wood1.caf */; };
543078AF24B5E12500278F2D /* wood1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A124B5E12300278F2D /* wood1.caf */; };
543078B024B5E12500278F2D /* plop2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A224B5E12300278F2D /* plop2.caf */; };
543078B124B5E12500278F2D /* plop2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A224B5E12300278F2D /* plop2.caf */; };
543078B224B5E12500278F2D /* plop1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A324B5E12300278F2D /* plop1.caf */; };
543078B324B5E12500278F2D /* plop1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A324B5E12300278F2D /* plop1.caf */; };
543078B424B5E12500278F2D /* snap1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A424B5E12300278F2D /* snap1.caf */; };
543078B524B5E12500278F2D /* snap1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A424B5E12300278F2D /* snap1.caf */; };
543078B624B5E12500278F2D /* drum1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A524B5E12300278F2D /* drum1.caf */; };
543078B724B5E12500278F2D /* drum1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A524B5E12300278F2D /* drum1.caf */; };
543078B824B5E12500278F2D /* wood2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A624B5E12400278F2D /* wood2.caf */; };
543078B924B5E12500278F2D /* wood2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A624B5E12400278F2D /* wood2.caf */; };
543078BA24B5E12500278F2D /* typewriter1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A724B5E12400278F2D /* typewriter1.caf */; };
543078BB24B5E12500278F2D /* typewriter1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A724B5E12400278F2D /* typewriter1.caf */; };
543078BC24B5E12500278F2D /* clock.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A824B5E12400278F2D /* clock.caf */; };
543078BD24B5E12500278F2D /* clock.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A824B5E12400278F2D /* clock.caf */; };
543078BE24B5E12500278F2D /* drum2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A924B5E12500278F2D /* drum2.caf */; };
543078BF24B5E12500278F2D /* drum2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A924B5E12500278F2D /* drum2.caf */; };
543078C324B60F3B00278F2D /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 543078C124B60F3B00278F2D /* Settings.storyboard */; };
543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */; };
543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */; }; 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, ); }; }; 543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
54448A2E2486464F00771C96 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2D2486464F00771C96 /* Array.swift */; }; 54448A2E2486464F00771C96 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2D2486464F00771C96 /* Array.swift */; };
54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; }; 54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; };
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A3124899A4000771C96 /* SearchBarManager.swift */; }; 54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A3124899A4000771C96 /* SearchBarManager.swift */; };
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; }; 544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */; };
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; }; 5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; }; 545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; }; 545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; };
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; }; 545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; };
546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; }; 546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A7524F8062C0084934D /* NotificationBanner.swift */; };
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8424FD0A3F0084934D /* tut-recording-howto.md */; };
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A8624FD26410084934D /* TinyMarkdown.swift */; };
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8824FD31580084934D /* tut-welcome-1.md */; };
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8B24FD3F180084934D /* tut-welcome-2.md */; };
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8A24FD3F100084934D /* tut-recording-1.md */; };
54686A9024FD42950084934D /* tut-recording-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8924FD31630084934D /* tut-recording-2.md */; };
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8C24FD3F630084934D /* tut-cooccurrence.md */; };
54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; }; 54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; }; 54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; }; 54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
@@ -39,14 +85,20 @@
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; }; 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; };
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; }; 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; };
54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; }; 54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; };
549A96D62501198400C565FA /* VCEditText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549A96D52501198400C565FA /* VCEditText.swift */; };
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 549A96D8250419B200C565FA /* CoOccurrence.storyboard */; };
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */; };
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; };
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */; };
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */; };
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; }; 54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; };
54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; }; 54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; };
54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; }; 54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; };
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A8241BBA0B004C53CC /* Generic.swift */; }; 54B345A9241BBA0B004C53CC /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A8241BBA0B004C53CC /* Logging.swift */; };
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; }; 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; };
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.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 */; }; 54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; };
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; }; 54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */; };
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; }; 54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; };
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; }; 54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; };
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; }; 54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; };
@@ -128,6 +180,9 @@
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BD2426D4F3003A5E04 /* DDLog.swift */; }; 54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BD2426D4F3003A5E04 /* DDLog.swift */; };
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */; }; 54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */; };
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; }; 54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; };
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* TVCShareRecording.swift */; };
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; }; 54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; };
54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; }; 54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; };
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; }; 54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; };
@@ -135,10 +190,15 @@
54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */; }; 54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */; };
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */; }; 54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */; };
54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F1247C423200F7C34A /* DomainFilter.swift */; }; 54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F1247C423200F7C34A /* DomainFilter.swift */; };
54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* TestDataSource.swift */; }; 54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */; };
54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; }; 54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; };
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.swift */; }; 54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.swift */; };
54EFA4E6248EEE240022D618 /* DatePickerAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */; }; 54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; };
54E67E4924A8B1280025D261 /* Prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4824A8B1280025D261 /* Prefs.swift */; };
54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4A24A8C6370025D261 /* GlassVPN.swift */; };
54E67E4D24A8E20D0025D261 /* TVCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A9924051556001462DC /* TVCSettings.swift */; };
54E67E4F24A8E2910025D261 /* Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4E24A8E2910025D261 /* Equatable.swift */; };
54E67E5124A8E8820025D261 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E5024A8E8820025D261 /* View.swift */; };
54EFA4E82491A16A0022D618 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E72491A16A0022D618 /* Font.swift */; }; 54EFA4E82491A16A0022D618 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E72491A16A0022D618 /* Font.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -167,10 +227,19 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideInAnimation.swift; sourceTree = "<group>"; };
5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCAnalysisBar.swift; sourceTree = "<group>"; };
540E677F242D2CF100871BBE /* VCRecordings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCRecordings.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>"; }; 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>"; }; 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCPreviousRecords.swift; sourceTree = "<group>"; };
541075CD24C9D43A00D6F1BF /* UNNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotification.swift; sourceTree = "<group>"; };
541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThrottledBatchQueue.swift; sourceTree = "<group>"; };
541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedConnectionAlert.swift; sourceTree = "<group>"; };
541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassVPNHook.swift; sourceTree = "<group>"; };
5412F8ED24571B8100A63D7A /* VCDateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCDateFilter.swift; sourceTree = "<group>"; }; 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCDateFilter.swift; sourceTree = "<group>"; };
5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCReminderAlerts.swift; sourceTree = "<group>"; };
5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCChooseAlertTone.swift; sourceTree = "<group>"; };
5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCConnectionAlerts.swift; sourceTree = "<group>"; };
541A957523E602DF00C09C19 /* LaunchIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = LaunchIcon.png; 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; }; 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>"; }; 541AC5D72399498A00A769D7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -178,11 +247,25 @@
541AC5DE2399498B00A769D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 541AC5DE2399498B00A769D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
541AC5E12399498B00A769D7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 541AC5E12399498B00A769D7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
541AC5E32399498B00A769D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 541AC5E32399498B00A769D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
541DCA6024A6B0F6005F1A4B /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
541FC47524A12D01009154D8 /* IBViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBViews.swift; sourceTree = "<group>"; }; 541FC47524A12D01009154D8 /* IBViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBViews.swift; sourceTree = "<group>"; };
541FC47724A1453F009154D8 /* VCCoOccurrence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCCoOccurrence.swift; sourceTree = "<group>"; }; 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCCoOccurrence.swift; sourceTree = "<group>"; };
541FC9862497D81C00962623 /* TheGreatDestroyer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TheGreatDestroyer.swift; sourceTree = "<group>"; }; 541FC9862497D81C00962623 /* TheGreatDestroyer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TheGreatDestroyer.swift; sourceTree = "<group>"; };
542E2A972404973F001462DC /* TBCMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBCMain.swift; sourceTree = "<group>"; }; 542E2A972404973F001462DC /* TBCMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBCMain.swift; sourceTree = "<group>"; };
542E2A9924051556001462DC /* TVCSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCSettings.swift; sourceTree = "<group>"; }; 542E2A9924051556001462DC /* TVCSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCSettings.swift; sourceTree = "<group>"; };
5430789F24B5E12200278F2D /* snap2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = snap2.caf; sourceTree = "<group>"; };
543078A024B5E12200278F2D /* typewriter2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = typewriter2.caf; sourceTree = "<group>"; };
543078A124B5E12300278F2D /* wood1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = wood1.caf; sourceTree = "<group>"; };
543078A224B5E12300278F2D /* plop2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = plop2.caf; sourceTree = "<group>"; };
543078A324B5E12300278F2D /* plop1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = plop1.caf; sourceTree = "<group>"; };
543078A424B5E12300278F2D /* snap1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = snap1.caf; sourceTree = "<group>"; };
543078A524B5E12300278F2D /* drum1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = drum1.caf; sourceTree = "<group>"; };
543078A624B5E12400278F2D /* wood2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = wood2.caf; sourceTree = "<group>"; };
543078A724B5E12400278F2D /* typewriter1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = typewriter1.caf; sourceTree = "<group>"; };
543078A824B5E12400278F2D /* clock.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = clock.caf; sourceTree = "<group>"; };
543078A924B5E12500278F2D /* drum2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = drum2.caf; sourceTree = "<group>"; };
543078C224B60F3B00278F2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Settings.storyboard; sourceTree = "<group>"; };
543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAppOnly.swift; sourceTree = "<group>"; };
543CDB1D23EEE61900B7F323 /* GlassVPN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GlassVPN.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GlassVPN.appex; sourceTree = BUILT_PRODUCTS_DIR; };
543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; }; 543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
543CDB2123EEE61900B7F323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 543CDB2123EEE61900B7F323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -190,26 +273,40 @@
54448A2D2486464F00771C96 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; }; 54448A2D2486464F00771C96 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
54448A2F248647D900771C96 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; }; 54448A2F248647D900771C96 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
54448A3124899A4000771C96 /* SearchBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarManager.swift; sourceTree = "<group>"; }; 54448A3124899A4000771C96 /* SearchBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarManager.swift; sourceTree = "<group>"; };
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; }; 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCOccurrenceContext.swift; sourceTree = "<group>"; };
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.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>"; }; 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>"; }; 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>"; }; 545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; };
54686A7524F8062C0084934D /* NotificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBanner.swift; sourceTree = "<group>"; };
54686A8424FD0A3F0084934D /* tut-recording-howto.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-howto.md"; sourceTree = "<group>"; };
54686A8624FD26410084934D /* TinyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TinyMarkdown.swift; sourceTree = "<group>"; };
54686A8824FD31580084934D /* tut-welcome-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-1.md"; sourceTree = "<group>"; };
54686A8924FD31630084934D /* tut-recording-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-2.md"; sourceTree = "<group>"; };
54686A8A24FD3F100084934D /* tut-recording-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-1.md"; sourceTree = "<group>"; };
54686A8B24FD3F180084934D /* tut-welcome-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-2.md"; sourceTree = "<group>"; };
54686A8C24FD3F630084934D /* tut-cooccurrence.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-cooccurrence.md"; sourceTree = "<group>"; };
54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.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>"; }; 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>"; }; 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>"; }; 54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = "<group>"; }; 54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = "<group>"; };
54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
549A96D52501198400C565FA /* VCEditText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditText.swift; sourceTree = "<group>"; };
549A96D9250419B200C565FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CoOccurrence.storyboard; sourceTree = "<group>"; };
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCAppSearch.swift; sourceTree = "<group>"; };
549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; sourceTree = "<group>"; };
54A0CC0824E30C56009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Recordings.storyboard; sourceTree = "<group>"; };
54A0CC0B24E30D6F009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Requests.storyboard; sourceTree = "<group>"; };
54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; }; 54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; };
54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; }; 54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 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>"; }; 54B345A8241BBA0B004C53CC /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
54B345AA241BBA5B004C53CC /* AlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSheet.swift; sourceTree = "<group>"; }; 54B345AA241BBA5B004C53CC /* AlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSheet.swift; sourceTree = "<group>"; };
54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.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>"; }; 54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; }; 54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; };
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; }; 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSearch.swift; sourceTree = "<group>"; };
54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; }; 54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; };
54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; }; 54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; };
54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; }; 54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
@@ -294,16 +391,22 @@
54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncUdpSocket.m; sourceTree = "<group>"; }; 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncUdpSocket.m; sourceTree = "<group>"; };
54CA02C12426DCCD003A5E04 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; }; 54CA02C12426DCCD003A5E04 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; };
54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; }; 54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; };
54CE8BC324B1ED2100CC1756 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; };
54CFE86724E3F401001687DD /* TVCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCShareRecording.swift; sourceTree = "<group>"; };
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = "<group>"; }; 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = "<group>"; };
54D8B97B2471A7E000EB2414 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; }; 54D8B97B2471A7E000EB2414 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
54D8B97D2471B88900EB2414 /* DBCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCommon.swift; sourceTree = "<group>"; }; 54D8B97D2471B88900EB2414 /* DBCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCommon.swift; sourceTree = "<group>"; };
54D8B9822471BD8100EB2414 /* DBAppOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAppOnly.swift; sourceTree = "<group>"; }; 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAppOnly.swift; sourceTree = "<group>"; };
54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomainDataSource.swift; sourceTree = "<group>"; }; 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomainDataSource.swift; sourceTree = "<group>"; };
54E540F1247C423200F7C34A /* DomainFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainFilter.swift; sourceTree = "<group>"; }; 54E540F1247C423200F7C34A /* DomainFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainFilter.swift; sourceTree = "<group>"; };
54E540F3247D3F2600F7C34A /* TestDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataSource.swift; sourceTree = "<group>"; }; 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPN.swift; sourceTree = "<group>"; };
54E540F7247DB90F00F7C34A /* RecordingsDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsDB.swift; sourceTree = "<group>"; }; 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsDB.swift; sourceTree = "<group>"; };
54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.swift; sourceTree = "<group>"; }; 54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.swift; sourceTree = "<group>"; };
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerAlert.swift; sourceTree = "<group>"; }; 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsShared.swift; sourceTree = "<group>"; };
54E67E4824A8B1280025D261 /* Prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prefs.swift; sourceTree = "<group>"; };
54E67E4A24A8C6370025D261 /* GlassVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassVPN.swift; sourceTree = "<group>"; };
54E67E4E24A8E2910025D261 /* Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Equatable.swift; sourceTree = "<group>"; };
54E67E5024A8E8820025D261 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
54EFA4E72491A16A0022D618 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = "<group>"; }; 54EFA4E72491A16A0022D618 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -332,7 +435,8 @@
5412F8ED24571B8100A63D7A /* VCDateFilter.swift */, 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */,
54953E6023E0D69A0054345C /* TVCHosts.swift */, 54953E6023E0D69A0054345C /* TVCHosts.swift */,
54953E6E23E44CD00054345C /* TVCHostDetails.swift */, 54953E6E23E44CD00054345C /* TVCHostDetails.swift */,
541FC47424A12CE9009154D8 /* Analytics */, 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */,
541FC47424A12CE9009154D8 /* Analysis */,
); );
path = Requests; path = Requests;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -342,6 +446,9 @@
children = ( children = (
542E2A9924051556001462DC /* TVCSettings.swift */, 542E2A9924051556001462DC /* TVCSettings.swift */,
54B34593240E6343004C53CC /* TVCFilter.swift */, 54B34593240E6343004C53CC /* TVCFilter.swift */,
5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */,
5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */,
5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */,
); );
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -351,12 +458,27 @@
children = ( children = (
540E677F242D2CF100871BBE /* VCRecordings.swift */, 540E677F242D2CF100871BBE /* VCRecordings.swift */,
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */, 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */, 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
54CFE86724E3F401001687DD /* TVCShareRecording.swift */,
549A96D52501198400C565FA /* VCEditText.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */,
54B345B12422E029004C53CC /* App Icons */,
); );
path = Recordings; path = Recordings;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
541075D324CE284700D6F1BF /* Push Notifications */ = {
isa = PBXGroup;
children = (
541075CD24C9D43A00D6F1BF /* UNNotification.swift */,
54CE8BC324B1ED2100CC1756 /* PushNotification.swift */,
543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */,
541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */,
);
path = "Push Notifications";
sourceTree = "<group>";
};
541AC5CB2399498A00A769D7 = { 541AC5CB2399498A00A769D7 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -383,15 +505,16 @@
54E540F0247C386500F7C34A /* Data Source */, 54E540F0247C386500F7C34A /* Data Source */,
54B345A4241BB975004C53CC /* Extensions */, 54B345A4241BB975004C53CC /* Extensions */,
545DDDD224436A03003B6544 /* Common Classes */, 545DDDD224436A03003B6544 /* Common Classes */,
541075D324CE284700D6F1BF /* Push Notifications */,
548B1F9423D338EC005B047C /* main.entitlements */, 548B1F9423D338EC005B047C /* main.entitlements */,
541AC5D72399498A00A769D7 /* AppDelegate.swift */, 541AC5D72399498A00A769D7 /* AppDelegate.swift */,
54E67E4A24A8C6370025D261 /* GlassVPN.swift */,
541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */,
542E2A972404973F001462DC /* TBCMain.swift */, 542E2A972404973F001462DC /* TBCMain.swift */,
540C6454240D5BAE00E948F9 /* Requests */, 540C6454240D5BAE00E948F9 /* Requests */,
540E677E242D2CD200871BBE /* Recordings */, 540E677E242D2CD200871BBE /* Recordings */,
540C6455240D5BD200E948F9 /* Settings */, 540C6455240D5BD200E948F9 /* Settings */,
54B345B12422E029004C53CC /* unused */, 54A0CC0D24E314B6009B5EC1 /* GUI */,
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
541AC5DE2399498B00A769D7 /* Assets.xcassets */, 541AC5DE2399498B00A769D7 /* Assets.xcassets */,
541AC5E32399498B00A769D7 /* Info.plist */, 541AC5E32399498B00A769D7 /* Info.plist */,
54953E7023E473F10054345C /* Settings.bundle */, 54953E7023E473F10054345C /* Settings.bundle */,
@@ -399,23 +522,44 @@
path = main; path = main;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
541FC47424A12CE9009154D8 /* Analytics */ = { 541FC47424A12CE9009154D8 /* Analysis */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */,
541FC47724A1453F009154D8 /* VCCoOccurrence.swift */, 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */,
); );
path = Analytics; path = Analysis;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
542E2A9B24051F79001462DC /* media */ = { 542E2A9B24051F79001462DC /* media */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54686A8324FD0A3F0084934D /* tutorials */,
5430789E24B5E10E00278F2D /* sounds */,
541A957523E602DF00C09C19 /* LaunchIcon.png */, 541A957523E602DF00C09C19 /* LaunchIcon.png */,
54B345AF242264F8004C53CC /* third-level.txt */, 54B345AF242264F8004C53CC /* third-level.txt */,
); );
path = media; path = media;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5430789E24B5E10E00278F2D /* sounds */ = {
isa = PBXGroup;
children = (
543078A824B5E12400278F2D /* clock.caf */,
543078A524B5E12300278F2D /* drum1.caf */,
543078A924B5E12500278F2D /* drum2.caf */,
543078A324B5E12300278F2D /* plop1.caf */,
543078A224B5E12300278F2D /* plop2.caf */,
543078A424B5E12300278F2D /* snap1.caf */,
5430789F24B5E12200278F2D /* snap2.caf */,
543078A724B5E12400278F2D /* typewriter1.caf */,
543078A024B5E12200278F2D /* typewriter2.caf */,
543078A124B5E12300278F2D /* wood1.caf */,
543078A624B5E12400278F2D /* wood2.caf */,
);
path = sounds;
sourceTree = "<group>";
};
543CDB1E23EEE61900B7F323 /* GlassVPN */ = { 543CDB1E23EEE61900B7F323 /* GlassVPN */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -433,16 +577,48 @@
545DDDD224436A03003B6544 /* Common Classes */ = { 545DDDD224436A03003B6544 /* Common Classes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54E67E4824A8B1280025D261 /* Prefs.swift */,
54E67E4524A8B0FE0025D261 /* PrefsShared.swift */,
545DDDD024436983003B6544 /* QuickUI.swift */, 545DDDD024436983003B6544 /* QuickUI.swift */,
545DDDCE243E6267003B6544 /* TutorialSheet.swift */, 545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
54686A8624FD26410084934D /* TinyMarkdown.swift */,
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */, 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
54448A3124899A4000771C96 /* SearchBarManager.swift */, 54448A3124899A4000771C96 /* SearchBarManager.swift */,
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */, 549ECD9C24A7AD550097571C /* CustomAlert.swift */,
541FC47524A12D01009154D8 /* IBViews.swift */, 541FC47524A12D01009154D8 /* IBViews.swift */,
5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */,
541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */,
54686A7524F8062C0084934D /* NotificationBanner.swift */,
); );
path = "Common Classes"; path = "Common Classes";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54686A8324FD0A3F0084934D /* tutorials */ = {
isa = PBXGroup;
children = (
54686A8824FD31580084934D /* tut-welcome-1.md */,
54686A8B24FD3F180084934D /* tut-welcome-2.md */,
54686A8A24FD3F100084934D /* tut-recording-1.md */,
54686A8924FD31630084934D /* tut-recording-2.md */,
54686A8424FD0A3F0084934D /* tut-recording-howto.md */,
54686A8C24FD3F630084934D /* tut-cooccurrence.md */,
);
path = tutorials;
sourceTree = "<group>";
};
54A0CC0D24E314B6009B5EC1 /* GUI */ = {
isa = PBXGroup;
children = (
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */,
549A96D8250419B200C565FA /* CoOccurrence.storyboard */,
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */,
543078C124B60F3B00278F2D /* Settings.storyboard */,
);
path = GUI;
sourceTree = "<group>";
};
54B3459A2415651C004C53CC /* DB */ = { 54B3459A2415651C004C53CC /* DB */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -458,10 +634,12 @@
54B345A4241BB975004C53CC /* Extensions */ = { 54B345A4241BB975004C53CC /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
544C95252407B1C700AB89D0 /* SharedState.swift */, 54B345A8241BBA0B004C53CC /* Logging.swift */,
54B345A8241BBA0B004C53CC /* Generic.swift */, 54E67E4E24A8E2910025D261 /* Equatable.swift */,
54B345A5241BB982004C53CC /* Notifications.swift */, 54B345A5241BB982004C53CC /* Notifications.swift */,
54B345AA241BBA5B004C53CC /* AlertSheet.swift */, 54B345AA241BBA5B004C53CC /* AlertSheet.swift */,
54E67E5024A8E8820025D261 /* View.swift */,
541DCA6024A6B0F6005F1A4B /* Color.swift */,
54448A2F248647D900771C96 /* Time.swift */, 54448A2F248647D900771C96 /* Time.swift */,
54751E502423955000168273 /* URL.swift */, 54751E502423955000168273 /* URL.swift */,
54EFA4E72491A16A0022D618 /* Font.swift */, 54EFA4E72491A16A0022D618 /* Font.swift */,
@@ -473,13 +651,13 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54B345B12422E029004C53CC /* unused */ = { 54B345B12422E029004C53CC /* App Icons */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54C056DC23E9EEF700214A3F /* BundleIcon.swift */, 54C056DC23E9EEF700214A3F /* BundleIcon.swift */,
54C056DA23E9E36E00214A3F /* AppInfoType.swift */, 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */,
); );
path = unused; path = "App Icons";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = { 54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = {
@@ -708,7 +886,7 @@
54E540F0247C386500F7C34A /* Data Source */ = { 54E540F0247C386500F7C34A /* Data Source */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54E540F3247D3F2600F7C34A /* TestDataSource.swift */, 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */,
54E540F92482414800F7C34A /* SyncUpdate.swift */, 54E540F92482414800F7C34A /* SyncUpdate.swift */,
54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */, 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */,
54E540F1247C423200F7C34A /* DomainFilter.swift */, 54E540F1247C423200F7C34A /* DomainFilter.swift */,
@@ -811,11 +989,32 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
543078AC24B5E12500278F2D /* typewriter2.caf in Resources */,
54953E7123E473F10054345C /* Settings.bundle in Resources */, 54953E7123E473F10054345C /* Settings.bundle in Resources */,
54686A9024FD42950084934D /* tut-recording-2.md in Resources */,
543078B024B5E12500278F2D /* plop2.caf in Resources */,
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */, 541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */,
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */,
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */,
543078B824B5E12500278F2D /* wood2.caf in Resources */,
543078BE24B5E12500278F2D /* drum2.caf in Resources */,
543078B424B5E12500278F2D /* snap1.caf in Resources */,
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */, 541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */,
543078AE24B5E12500278F2D /* wood1.caf in Resources */,
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */,
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */, 541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */,
54B345B0242264F8004C53CC /* third-level.txt in Resources */, 54B345B0242264F8004C53CC /* third-level.txt in Resources */,
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */,
543078BA24B5E12500278F2D /* typewriter1.caf in Resources */,
543078B224B5E12500278F2D /* plop1.caf in Resources */,
543078B624B5E12500278F2D /* drum1.caf in Resources */,
543078BC24B5E12500278F2D /* clock.caf in Resources */,
543078C324B60F3B00278F2D /* Settings.storyboard in Resources */,
543078AA24B5E12500278F2D /* snap2.caf in Resources */,
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */,
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */,
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */,
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */,
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */, 541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -824,6 +1023,17 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
543078AD24B5E12500278F2D /* typewriter2.caf in Resources */,
543078BB24B5E12500278F2D /* typewriter1.caf in Resources */,
543078BF24B5E12500278F2D /* drum2.caf in Resources */,
543078AF24B5E12500278F2D /* wood1.caf in Resources */,
543078B124B5E12500278F2D /* plop2.caf in Resources */,
543078AB24B5E12500278F2D /* snap2.caf in Resources */,
543078B924B5E12500278F2D /* wood2.caf in Resources */,
543078B724B5E12500278F2D /* drum1.caf in Resources */,
543078B524B5E12500278F2D /* snap1.caf in Resources */,
543078B324B5E12500278F2D /* plop1.caf in Resources */,
543078BD24B5E12500278F2D /* clock.caf in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -834,40 +1044,59 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
54E67E4924A8B1280025D261 /* Prefs.swift in Sources */,
54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */, 54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */,
54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */, 54E67E4F24A8E2910025D261 /* Equatable.swift in Sources */,
54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */,
541075D924CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */,
541075D524CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */,
5404AEEF24ACC089003B2F54 /* VCAnalysisBar.swift in Sources */,
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */, 545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */,
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */, 54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */,
54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */, 54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */,
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */, 54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */,
54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */, 54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */,
5412FCC224C628FA000DE429 /* TVCReminderAlerts.swift in Sources */,
54E67E4D24A8E20D0025D261 /* TVCSettings.swift in Sources */,
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */, 540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */,
54B345A6241BB982004C53CC /* Notifications.swift in Sources */, 54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
54448A2E2486464F00771C96 /* Array.swift in Sources */, 54448A2E2486464F00771C96 /* Array.swift in Sources */,
54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */,
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */,
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */, 541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */,
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */, 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */, 541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */,
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */, 545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */,
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */, 54B345A9241BBA0B004C53CC /* Logging.swift in Sources */,
54B34596240F0513004C53CC /* TableView.swift in Sources */, 54B34596240F0513004C53CC /* TableView.swift in Sources */,
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */, 540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */,
541075D124CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */,
54953E3323DC752E0054345C /* DBCore.swift in Sources */, 54953E3323DC752E0054345C /* DBCore.swift in Sources */,
54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */,
5412FCC324C628FA000DE429 /* TVCChooseAlertTone.swift in Sources */,
544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */,
54448A30248647D900771C96 /* Time.swift in Sources */, 54448A30248647D900771C96 /* Time.swift in Sources */,
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */, 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */,
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */,
54751E512423955100168273 /* URL.swift in Sources */, 54751E512423955100168273 /* URL.swift in Sources */,
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */,
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */, 54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */, 54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */,
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */, 54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */,
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */, 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
54D8B97C2471A7E000EB2414 /* String.swift in Sources */, 54D8B97C2471A7E000EB2414 /* String.swift in Sources */,
54EFA4E6248EEE240022D618 /* DatePickerAlert.swift in Sources */, 54E67E5124A8E8820025D261 /* View.swift in Sources */,
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */, 54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
542E2A982404973F001462DC /* TBCMain.swift in Sources */, 542E2A982404973F001462DC /* TBCMain.swift in Sources */,
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */, 54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */,
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */, 5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
549A96D62501198400C565FA /* VCEditText.swift in Sources */,
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */, 541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */,
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */,
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */, 5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */,
543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */,
545DDDD124436983003B6544 /* QuickUI.swift in Sources */, 545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */, 541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
541FC47624A12D01009154D8 /* IBViews.swift in Sources */, 541FC47624A12D01009154D8 /* IBViews.swift in Sources */,
@@ -875,7 +1104,10 @@
54EFA4E82491A16A0022D618 /* Font.swift in Sources */, 54EFA4E82491A16A0022D618 /* Font.swift in Sources */,
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */, 54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */,
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */, 54B34594240E6343004C53CC /* TVCFilter.swift in Sources */,
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */,
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */,
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */, 54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */,
5404AEED24A95F3F003B2F54 /* SlideInAnimation.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -893,6 +1125,7 @@
54CA02722426B2FD003A5E04 /* IPInterval.swift in Sources */, 54CA02722426B2FD003A5E04 /* IPInterval.swift in Sources */,
54CA029A2426B2FD003A5E04 /* Observer.swift in Sources */, 54CA029A2426B2FD003A5E04 /* Observer.swift in Sources */,
54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */, 54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */,
5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */,
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */, 54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */,
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */, 54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */,
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */, 54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */,
@@ -916,6 +1149,7 @@
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */, 54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */,
54CA02792426B2FD003A5E04 /* Checksum.swift in Sources */, 54CA02792426B2FD003A5E04 /* Checksum.swift in Sources */,
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */, 54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */,
541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */,
54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */, 54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */,
54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */, 54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */,
54CA026B2426B2FD003A5E04 /* GCDTCPSocket.swift in Sources */, 54CA026B2426B2FD003A5E04 /* GCDTCPSocket.swift in Sources */,
@@ -926,16 +1160,19 @@
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */, 54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */,
54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */, 54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */,
54CA02742426B2FD003A5E04 /* IPMask.swift in Sources */, 54CA02742426B2FD003A5E04 /* IPMask.swift in Sources */,
541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */, 54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */,
54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */, 54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */,
54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */, 54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */,
54CA02942426B2FD003A5E04 /* DNSResolver.swift in Sources */, 54CA02942426B2FD003A5E04 /* DNSResolver.swift in Sources */,
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */, 54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */,
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */, 54CA02842426B2FD003A5E04 /* Rule.swift in Sources */,
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */,
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */, 54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */,
54751E522423955100168273 /* URL.swift in Sources */, 54751E522423955100168273 /* URL.swift in Sources */,
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */, 54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */,
54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */, 54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */,
541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */,
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */, 54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */,
54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */, 54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */,
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */, 54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */,
@@ -966,6 +1203,7 @@
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */, 54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */,
54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */, 54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */,
54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */, 54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */,
541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */,
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */, 54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -997,6 +1235,38 @@
name = LaunchScreen.storyboard; name = LaunchScreen.storyboard;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
543078C124B60F3B00278F2D /* Settings.storyboard */ = {
isa = PBXVariantGroup;
children = (
543078C224B60F3B00278F2D /* Base */,
);
name = Settings.storyboard;
sourceTree = "<group>";
};
549A96D8250419B200C565FA /* CoOccurrence.storyboard */ = {
isa = PBXVariantGroup;
children = (
549A96D9250419B200C565FA /* Base */,
);
name = CoOccurrence.storyboard;
sourceTree = "<group>";
};
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */ = {
isa = PBXVariantGroup;
children = (
54A0CC0824E30C56009B5EC1 /* Base */,
);
name = Recordings.storyboard;
sourceTree = "<group>";
};
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */ = {
isa = PBXVariantGroup;
children = (
54A0CC0B24E30D6F009B5EC1 /* Base */,
);
name = Requests.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@@ -1127,7 +1397,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 22; CURRENT_PROJECT_VERSION = 28;
INFOPLIST_FILE = main/Info.plist; INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1146,7 +1416,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 22; CURRENT_PROJECT_VERSION = 28;
INFOPLIST_FILE = main/Info.plist; INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1165,7 +1435,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements; CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22; CURRENT_PROJECT_VERSION = 28;
INFOPLIST_FILE = GlassVPN/Info.plist; INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN"; PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
@@ -1183,7 +1453,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements; CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22; CURRENT_PROJECT_VERSION = 28;
INFOPLIST_FILE = GlassVPN/Info.plist; INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN"; PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";

View File

@@ -1,50 +1,6 @@
import NetworkExtension import NetworkExtension
fileprivate var filterDomains: [String]! fileprivate var hook : GlassVPNHook!
fileprivate var filterOptions: [(block: Bool, ignore: Bool)]!
// MARK: Backward DNS Binary Tree Lookup
fileprivate func reloadDomainFilter() {
let tmp = AppDB?.loadFilters()?.map({
(String($0.reversed()), $1)
}).sorted(by: { $0.0 < $1.0 }) ?? []
filterDomains = tmp.map { $0.0 }
filterOptions = tmp.map { ($1.contains(.blocked), $1.contains(.ignored)) }
}
fileprivate func filterIndex(for domain: String) -> Int {
let reverseDomain = String(domain.reversed())
var lo = 0, hi = filterDomains.count - 1
while lo <= hi {
let mid = (lo + hi)/2
if filterDomains[mid] < reverseDomain {
lo = mid + 1
} else if reverseDomain < filterDomains[mid] {
hi = mid - 1
} else {
return mid
}
}
if lo > 0, reverseDomain.hasPrefix(filterDomains[lo - 1] + ".") {
return lo - 1
}
return -1
}
private let queue = DispatchQueue.init(label: "PSIGlassDNSQueue", qos: .userInteractive, target: .main)
private func logAsync(_ domain: String, blocked: Bool) {
queue.async {
do {
try AppDB?.logWrite(domain, blocked: blocked)
} catch {
DDLogWarn("Couldn't write: \(error)")
}
}
}
// MARK: ObserverFactory // MARK: ObserverFactory
@@ -59,15 +15,8 @@ class LDObserverFactory: ObserverFactory {
override func signal(_ event: ProxySocketEvent) { override func signal(_ event: ProxySocketEvent) {
switch event { switch event {
case .receivedRequest(let session, let socket): case .receivedRequest(let session, let socket):
let i = filterIndex(for: session.host) let kill = hook.processDNSRequest(session.host)
if i >= 0 { if kill { socket.forceDisconnect() }
let (block, ignore) = filterOptions[i]
if !ignore { logAsync(session.host, blocked: block) }
if block { socket.forceDisconnect() }
} else {
// TODO: disable filter during recordings
logAsync(session.host, blocked: false)
}
default: default:
break break
} }
@@ -80,25 +29,63 @@ class LDObserverFactory: ObserverFactory {
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
let proxyServerPort: UInt16 = 9090 private let proxyServerPort: UInt16 = 9090
let proxyServerAddress = "127.0.0.1" private let proxyServerAddress = "127.0.0.1"
var proxyServer: GCDHTTPProxyServer! private var proxyServer: GCDHTTPProxyServer!
// MARK: Delegate
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
DDLogVerbose("startTunnel with with options: \(String(describing: options))")
PrefsShared.registerDefaults()
do { do {
try SQLiteDatabase.open().initCommonScheme() try SQLiteDatabase.open().initCommonScheme()
} catch { } catch {
completionHandler(error) completionHandler(error) // if we cant open db, fail immediately
return return
} }
reloadDomainFilter() // stop previous if any
if proxyServer != nil { proxyServer.stop() }
if proxyServer != nil {
proxyServer.stop()
}
proxyServer = nil proxyServer = nil
// Create proxy willInitProxy()
self.setTunnelNetworkSettings(createProxy()) { error in
guard error == nil else {
DDLogError("setTunnelNetworkSettings error: \(error!)")
completionHandler(error)
return
}
self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort))
do {
try self.proxyServer.start()
self.didInitProxy()
completionHandler(nil)
} catch let proxyError {
DDLogError("Error starting proxy server \(proxyError)")
completionHandler(proxyError)
}
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
DDLogVerbose("stopTunnel with reason: \(reason)")
shutdown()
completionHandler()
exit(EXIT_SUCCESS)
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
hook.handleAppMessage(messageData)
}
// MARK: Helper
private func willInitProxy() {
hook = GlassVPNHook()
}
private func createProxy() -> NEPacketTunnelNetworkSettings {
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress) let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress)
settings.mtu = NSNumber(value: 1500) settings.mtu = NSNumber(value: 1500)
@@ -115,42 +102,29 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
settings.proxySettings = proxySettings; settings.proxySettings = proxySettings;
RawSocketFactory.TunnelProvider = self RawSocketFactory.TunnelProvider = self
ObserverFactory.currentFactory = LDObserverFactory() ObserverFactory.currentFactory = LDObserverFactory()
return settings
self.setTunnelNetworkSettings(settings) { error in }
guard error == nil else {
DDLogError("setTunnelNetworkSettings error: \(String(describing: error))") private func didInitProxy() {
completionHandler(error) if PrefsShared.RestartReminder.Enabled {
return PushNotification.scheduleRestartReminderBadge(on: false)
} PushNotification.cancel(.CantStopMeNowReminder)
completionHandler(nil)
self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort))
do {
try self.proxyServer.start()
completionHandler(nil)
}
catch let proxyError {
DDLogError("Error starting proxy server \(proxyError)")
completionHandler(proxyError)
}
} }
} }
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { private func shutdown() {
DDLogVerbose("stopTunnel with reason: \(reason)") // proxy
DNSServer.currentServer = nil DNSServer.currentServer = nil
RawSocketFactory.TunnelProvider = nil RawSocketFactory.TunnelProvider = nil
ObserverFactory.currentFactory = nil ObserverFactory.currentFactory = nil
proxyServer.stop() proxyServer.stop()
proxyServer = nil proxyServer = nil
filterDomains = nil // custom
filterOptions = nil hook.cleanUp()
completionHandler() hook = nil
exit(EXIT_SUCCESS) if PrefsShared.RestartReminder.Enabled {
} PushNotification.scheduleRestartReminderBadge(on: true)
PushNotification.scheduleRestartReminderBanner()
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { }
reloadDomainFilter()
} }
} }

View File

@@ -141,7 +141,14 @@ public class NWTCPSocket: NSObject, RawTCPSocketProtocol {
connection!.readMinimumLength(1, maximumLength: Opt.MAXNWTCPSocketReadDataSize) { data, error in connection!.readMinimumLength(1, maximumLength: Opt.MAXNWTCPSocketReadDataSize) { data, error in
guard error == nil else { guard error == nil else {
DDLogError("NWTCPSocket got an error when reading data: \(String(describing: error))") let e = error! as NSError
let ignore = (
e.domain == "kNWErrorDomainPOSIX" && e.code == POSIXError.ECANCELED.rawValue // Operation canceled
|| e.domain == NSPOSIXErrorDomain && e.code == POSIXError.ENOTCONN.rawValue // Socket is not connected
)
if !ignore {
DDLogError("NWTCPSocket got an error when reading data: \(String(describing: error))")
}
self.queueCall { self.queueCall {
self.disconnect() self.disconnect()
} }

View File

@@ -16,6 +16,8 @@ Your data belongs to you.
Therefore, monitoring and analysis take place on your device only. 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. The app does not share any data with us or any other third-party unless you choose to.
Join [Testflight beta](https://testflight.apple.com/join/9jjaFeHO)
### How does it work? ### How does it work?
@@ -31,19 +33,23 @@ That means, AppCheck does not have to be active in the foreground all the time.
- See history of previous connections - See history of previous connections
- Block unwanted traffic based on domain names - Block unwanted traffic based on domain names
- Record app specific activity<sup>1</sup> - Record app specific activity<sup>1</sup>
- Apply logging filters - Apply logging filters (block or ignore) and display filters (specific range or last x minutes)
- Sort results by time, name, or occurrence count
**… and soon:** - Context Analysis
- What other domains occur often at the same time?
- What happened immediately before or after the action?
- Export results for custom analysis
- Alert Monitor & reminder - Alert Monitor & reminder
- Occurrence Context Analysis
- Participate in privacy research - Participate in privacy research
- Contribute your results
- See what others have unveiled
- How much traffic does this app produce?
<sup>1</sup> Due to technical limitations, recording is not limited to any single application. Remember to force-quit all other applications before starting a recording. <sup>1</sup> Due to technical limitations, recordings can not be restricted to a single application. Remember to force-quit all other applications before starting a recording.
## Research Project ## Research Project
*information will be added soon* *information will be added soon*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -1,13 +1,9 @@
import UIKit import UIKit
import NetworkExtension
let VPNConfigBundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
var managerVPN: NETunnelProviderManager?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if UserDefaults.standard.bool(forKey: "kill_db") { if UserDefaults.standard.bool(forKey: "kill_db") {
@@ -19,123 +15,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
db.initAppOnlyScheme() db.initAppOnlyScheme()
} }
#if IOS_SIMULATOR Prefs.registerDefaults()
TestDataSource.load() PrefsShared.registerDefaults()
#endif
loadVPN { mgr in #if IOS_SIMULATOR
self.managerVPN = mgr SimulatorVPN.load()
self.postVPNState() #endif
}
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
sync.start() sync.start()
return true return true
} }
@objc private func vpnStatusChanged(_ notification: Notification) { func applicationDidBecomeActive(_ application: UIApplication) {
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid) TheGreatDestroyer.deleteLogs(olderThan: PrefsShared.AutoDeleteLogsDays)
} // FIXME: Does not reflect changes performed by GlassVPN auto-delete while app is open.
// It will update whenever app restarts or becomes active again (only if deleteLogs has something to delete!)
@objc private func didChangeDomainFilter() { // This is a known issue and tolerated.
// Notify VPN extension about changes
if let session = self.managerVPN?.connection as? NETunnelProviderSession,
session.status == .connected {
try? session.sendProviderMessage("filter-update".data(using: .ascii)!, responseHandler: nil)
}
}
func setProxyEnabled(_ newState: Bool) {
guard let mgr = self.managerVPN else {
self.createNewVPN { manager in
self.managerVPN = manager
self.setProxyEnabled(newState)
}
return
}
let state = mgr.isEnabled && (mgr.connection.status == .connected)
if state != newState {
self.updateVPN({ mgr.isEnabled = true }) {
newState ? try? mgr.connection.startVPNTunnel() : mgr.connection.stopVPNTunnel()
}
}
}
// MARK: VPN
private func createNewVPN(_ success: @escaping (_ manager: NETunnelProviderManager) -> Void) {
let mgr = NETunnelProviderManager()
mgr.localizedDescription = "AppCheck Monitor"
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = VPNConfigBundleIdentifier
proto.serverAddress = "127.0.0.1"
mgr.protocolConfiguration = proto
mgr.isEnabled = true
mgr.saveToPreferences { error in
guard error == nil else {
self.postProcessedVPNState(.off)
//ErrorAlert(error!).presentIn(self.window?.rootViewController)
return
}
success(mgr)
}
}
private func loadVPN(_ finally: @escaping (_ manager: NETunnelProviderManager?) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { managers, error in
guard let mgrs = managers, mgrs.count > 0 else {
finally(nil)
return
}
for mgr in mgrs {
if let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) {
if proto.providerBundleIdentifier == VPNConfigBundleIdentifier {
finally(mgr)
return
}
}
}
finally(nil)
}
}
private func updateVPN(_ body: @escaping () -> Void, _ onSuccess: @escaping () -> Void) {
self.managerVPN?.loadFromPreferences { error in
guard error == nil else { return }
body()
self.managerVPN?.saveToPreferences { error in
guard error == nil else { return }
onSuccess()
}
}
}
private func postVPNState() {
guard let mgr = self.managerVPN else {
self.postRawVPNState(.invalid)
return
}
mgr.loadFromPreferences { _ in
self.postRawVPNState(mgr.connection.status)
}
}
// MARK: Notifications
private func postRawVPNState(_ origState: NEVPNStatus) {
let state: VPNState
switch origState {
case .connected: state = .on
case .connecting, .disconnecting, .reasserting: state = .inbetween
case .invalid, .disconnected: fallthrough
@unknown default: state = .off
}
postProcessedVPNState(state)
}
private func postProcessedVPNState(_ state: VPNState) {
currentVPNState = state
NotifyVPNStateChanged.post(state)
} }
} }
extension URL {
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
}

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -19,5 +19,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@@ -19,5 +19,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@@ -19,5 +19,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
import UIKit
class CustomAlert<CustomView: UIView>: UIViewController {
private let alertTitle: String?
private let alertDetail: String?
private let customView: CustomView
private var callback: ((CustomView) -> Void)?
/// Default: `[Cancel, Save]`
lazy var buttonsBar: UIStackView = {
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
save.titleLabel?.font = save.titleLabel?.font.bold()
let bar = UIStackView(arrangedSubviews: [cancel, save])
bar.axis = .horizontal
bar.distribution = .equalSpacing
return bar
}()
// MARK: - Init
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
init(title: String? = nil, detail: String? = nil, view custom: CustomView) {
alertTitle = title
alertDetail = detail
customView = custom
super.init(nibName: nil, bundle: nil)
}
override var isModalInPresentation: Bool { set{} get{true} }
override var modalPresentationStyle: UIModalPresentationStyle { set{} get{.custom} }
override var transitioningDelegate: UIViewControllerTransitioningDelegate? {
set {} get {
SlideInTransitioningDelegate(for: .bottom, modal: true)
}
}
internal override func loadView() {
let control = UIView()
control.backgroundColor = .sysBackground
view = control
var tmpPrevivous: UIView? = nil
func adaptive(margin: CGFloat, _ fn: () -> NSLayoutConstraint) {
regularConstraints.append(fn() + margin)
compactConstraints.append(fn() + margin/2)
}
func addLabel(_ lbl: UILabel) {
lbl.numberOfLines = 0
control.addSubview(lbl)
lbl.anchor([.leading, .trailing], to: control.layoutMarginsGuide)
if let p = tmpPrevivous {
adaptive(margin: 16) { lbl.topAnchor =&= p.bottomAnchor }
} else {
adaptive(margin: 12) { lbl.topAnchor =&= control.layoutMarginsGuide.topAnchor }
}
tmpPrevivous = lbl
}
// Alert title & description
if let t = alertTitle {
let lbl = QuickUI.label(t, align: .center, style: .subheadline)
lbl.font = lbl.font.bold()
addLabel(lbl)
}
if let d = alertDetail {
addLabel(QuickUI.label(d, align: .center, style: .footnote))
}
// User content
control.addSubview(customView)
customView.anchor([.leading, .trailing], to: control)
if let p = tmpPrevivous {
customView.topAnchor =&= p.bottomAnchor | .defaultHigh
} else {
customView.topAnchor =&= control.layoutMarginsGuide.topAnchor
}
// Action buttons
control.addSubview(buttonsBar)
buttonsBar.anchor([.leading, .trailing], to: control.layoutMarginsGuide, margin: 8)
buttonsBar.topAnchor =&= customView.bottomAnchor | .defaultHigh
adaptive(margin: 12) { control.layoutMarginsGuide.bottomAnchor =&= buttonsBar.bottomAnchor }
adaptToNewTraits(traitCollection)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
}
// MARK: - Adaptive Traits
private var compactConstraints: [NSLayoutConstraint] = []
private var regularConstraints: [NSLayoutConstraint] = []
private func adaptToNewTraits(_ traits: UITraitCollection) {
let flag = traits.verticalSizeClass == .compact
NSLayoutConstraint.deactivate(flag ? regularConstraints : compactConstraints)
NSLayoutConstraint.activate(flag ? compactConstraints : regularConstraints)
view.setNeedsLayout()
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
adaptToNewTraits(newCollection)
}
// MARK: - User Interaction
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
}
@objc private func didTapCancel() {
callback = nil
dismiss(animated: true)
}
@objc private func didTapSave() {
dismiss(animated: true) {
self.callback?(self.customView)
self.callback = nil
}
}
// MARK: - Present & Dismiss
func present(in viewController: UIViewController, onSuccess: @escaping (CustomView) -> Void) {
callback = onSuccess
viewController.present(self, animated: true)
}
}
// ###################################
// #
// # MARK: - Date Picker Alert
// #
// ###################################
class DatePickerAlert : CustomAlert<UIDatePicker> {
let datePicker = UIDatePicker()
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
init(title: String? = nil, detail: String? = nil, initial date: Date? = nil) {
if let date = date {
datePicker.setDate(date, animated: false)
}
super.init(title: title, detail: detail, view: datePicker)
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
now.titleLabel?.font = now.titleLabel?.font.bold()
now.setTitleColor(.sysLabel, for: .normal)
buttonsBar.insertArrangedSubview(now, at: 1)
}
@objc private func didTapNow() {
datePicker.date = Date()
}
func present(in viewController: UIViewController, onSuccess: @escaping (UIDatePicker, Date) -> Void) {
super.present(in: viewController) {
onSuccess($0, $0.date)
}
}
}
// #######################################
// #
// # MARK: - Duration Picker Alert
// #
// #######################################
class DurationPickerAlert: CustomAlert<UIPickerView>, UIPickerViewDataSource, UIPickerViewDelegate {
let pickerView = UIPickerView()
private let dataSource: [[String]]
private let compWidths: [CGFloat]
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
/// - Parameter options: [[List of labels] per component]
/// - Parameter widths: If `nil` set all components to equal width
init(title: String? = nil, detail: String? = nil, options: [[String]], widths: [CGFloat]? = nil) {
assert(widths == nil || widths!.count == options.count, "widths.count != options.count")
dataSource = options
compWidths = widths ?? options.map { _ in 1 / CGFloat(options.count) }
super.init(title: title, detail: detail, view: pickerView)
pickerView.dataSource = self
pickerView.delegate = self
}
func numberOfComponents(in _: UIPickerView) -> Int {
dataSource.count
}
func pickerView(_: UIPickerView, numberOfRowsInComponent c: Int) -> Int {
dataSource[c].count
}
func pickerView(_: UIPickerView, titleForRow r: Int, forComponent c: Int) -> String? {
dataSource[c][r]
}
func pickerView(_ pickerView: UIPickerView, widthForComponent c: Int) -> CGFloat {
compWidths[c] * pickerView.frame.width
}
func present(in viewController: UIViewController, onSuccess: @escaping (UIPickerView, [Int]) -> Void) {
super.present(in: viewController) {
onSuccess($0, $0.selection)
}
}
}
extension UIPickerView {
var selection: [Int] {
get { (0..<numberOfComponents).map { selectedRow(inComponent: $0) } }
set { setSelection(newValue) }
}
/// - Warning: Does not check for boundaries!
func setSelection(_ selection: [Int], animated: Bool = false) {
assert(selection.count == numberOfComponents, "selection.count != components.count")
for (c, i) in selection.enumerated() {
selectRow(i, inComponent: c, animated: animated)
}
}
}

View File

@@ -1,102 +0,0 @@
import UIKit
class DatePickerAlert: UIViewController {
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
}
private var callback: (Date) -> Void
private let picker: UIDatePicker = {
let x = UIDatePicker()
let h = x.sizeThatFits(.zero).height
x.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: h)
return x
}()
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@discardableResult required init(presentIn viewController: UIViewController, configure: ((UIDatePicker) -> Void)? = nil, onSuccess: @escaping (Date) -> Void) {
callback = onSuccess
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
presentIn(viewController, configure)
}
internal override func loadView() {
let cancel = QuickUI.button("Discard", target: self, action: #selector(didTapCancel))
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
save.titleLabel?.font = save.titleLabel?.font.bold()
now.titleLabel?.font = now.titleLabel?.font.bold()
now.setTitleColor(.sysFg, for: .normal)
//cancel.setTitleColor(.systemRed, for: .normal)
let buttons = UIStackView(arrangedSubviews: [cancel, now, save])
buttons.axis = .horizontal
buttons.distribution = .equalSpacing
let bg = UIView(frame: picker.frame)
bg.frame.size.height += buttons.frame.height + 15
bg.frame.origin.y = UIScreen.main.bounds.height - bg.frame.height - 15
bg.backgroundColor = .sysBg
bg.addSubview(picker)
bg.addSubview(buttons)
let clearBg = UIView()
clearBg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
clearBg.addSubview(bg)
picker.anchor([.leading, .trailing, .top], to: bg)
picker.bottomAnchor =&= buttons.topAnchor
buttons.anchor([.leading, .trailing], to: bg, margin: 25)
buttons.bottomAnchor =&= bg.bottomAnchor - 15
bg.anchor([.leading, .trailing, .bottom], to: clearBg)
view = clearBg
view.isHidden = true // otherwise picker will flash on present
}
@objc private func didTapNow() {
picker.date = Date()
}
@objc private func didTapSave() {
dismiss(animated: true) {
self.callback(self.picker.date)
}
}
@objc private func didTapCancel() {
dismiss(animated: true)
}
private func presentIn(_ viewController: UIViewController, _ configure: ((UIDatePicker) -> Void)? = nil) {
viewController.present(self, animated: false) {
let control = self.view.subviews.first!
let prev = control.frame.origin.y
control.frame.origin.y += control.frame.height
self.view.isHidden = false
configure?(self.picker)
UIView.animate(withDuration: 0.3) {
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
control.frame.origin.y = prev
}
}
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.3, animations: {
let control = self.view.subviews.first!
self.view.backgroundColor = .clear
control.frame.origin.y += control.frame.height
}) { _ in
super.dismiss(animated: false, completion: completion)
}
}
}

View File

@@ -66,7 +66,7 @@ class TagLabel: UILabel {
@IBDesignable @IBDesignable
class MeterBar: UIView { class MeterBar: UIView {
@IBInspectable var percent: CGFloat = 0 { didSet { setNeedsDisplay() } } @IBInspectable var percent: CGFloat = 0 { didSet { setNeedsDisplay() } }
@IBInspectable var barColor: UIColor = .sysFg @IBInspectable var barColor: UIColor = .sysLink
@IBInspectable var horizontal: Bool = false @IBInspectable var horizontal: Bool = false
private var normPercent: CGFloat { 1 - max(0, min(percent, 1)) } private var normPercent: CGFloat { 1 - max(0, min(percent, 1)) }

View File

@@ -0,0 +1,69 @@
import UIKit
struct NotificationBanner {
enum Style {
case fail, ok
}
let view: UIView
init(_ msg: String, style: Style) {
let bg, fg: UIColor
let imgName: String
switch style {
case .fail:
bg = .systemRed
fg = UIColor.black.withAlphaComponent(0.80)
imgName = "circle-x"
case .ok:
bg = .systemGreen
fg = UIColor.black.withAlphaComponent(0.65)
imgName = "circle-check"
}
view = UIView()
view.backgroundColor = bg
let lbl = QuickUI.label(msg, style: .callout)
lbl.textColor = fg
lbl.numberOfLines = 0
lbl.font = lbl.font.bold()
let img = QuickUI.image(UIImage(named: imgName))
img.tintColor = fg
view.addSubview(lbl)
view.addSubview(img)
img.anchor([.centerY], to: view.layoutMarginsGuide)
lbl.anchor([.top, .bottom, .trailing], to: view.layoutMarginsGuide)
img.widthAnchor =&= 25
img.heightAnchor =&= 25
if #available(iOS 11, *) {
img.leadingAnchor =&= view.layoutMarginsGuide.leadingAnchor
} else {
img.leadingAnchor =&= view.leadingAnchor + 8
}
lbl.leadingAnchor =&= img.trailingAnchor + 8
img.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
lbl.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
}
/// Animate header banner from the top of the view. Show for `delay` seconds and then hide again.
/// - Parameter onClose: Run after the close animation finishes.
func present(in vc: UIViewController, hideAfter delay: TimeInterval = 3, onClose: (() -> Void)? = nil) {
vc.view.addSubview(view)
view.anchor([.leading, .trailing], to: vc.view!)
view.widthAnchor =&= vc.view!.widthAnchor // Bug? left-right is not sufficient
vc.view.layoutIfNeeded() // sets the height
let h = view.frame.height
let constraint = view.topAnchor =&= vc.view.topAnchor - h
vc.view.layoutIfNeeded() // hide view
UIView.animate(withDuration: 0.3, animations: {
constraint.constant = 0
vc.view.layoutIfNeeded() // animate view
UIView.animate(withDuration: 0.3, delay: delay, options: .curveLinear, animations: {
constraint.constant = -h
vc.view.layoutIfNeeded() // hide again
}, completion: { _ in
self.view.removeFromSuperview()
onClose?()
})
})
}
}

View File

@@ -0,0 +1,126 @@
import Foundation
enum Prefs {
private static var suite: UserDefaults { UserDefaults.standard }
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key) }
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key) }
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key) }
private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) }
private static func Obj(_ key: String, _ val: Any?) { suite.set(val, forKey: key) }
static func registerDefaults() {
suite.register(defaults: [
"RecordingReminderEnabled" : true,
"contextAnalyisCoOccurrenceTime" : 5,
])
}
}
// MARK: - Tutorial
extension Prefs {
enum DidShowTutorial {
static var Welcome: Bool {
get { Prefs.Bool("didShowTutorialAppWelcome") }
set { Prefs.Bool("didShowTutorialAppWelcome", newValue) }
}
static var Recordings: Bool {
get { Prefs.Bool("didShowTutorialRecordings") }
set { Prefs.Bool("didShowTutorialRecordings", newValue) }
}
static var RecordingHowTo: Bool {
get { Prefs.Bool("didShowTutorialRecordingHowTo") }
set { Prefs.Bool("didShowTutorialRecordingHowTo", newValue) }
}
}
}
// MARK: - Date Filter
enum DateFilterKind: Int {
case Off = 0, LastXMin = 1, ABRange = 2;
}
enum DateFilterOrderBy: Int {
case Date = 0, Name = 1, Count = 2;
}
extension Prefs {
enum DateFilter {
static var Kind: DateFilterKind {
get { DateFilterKind(rawValue: Prefs.Int("dateFilterType"))! }
set { Prefs.Int("dateFilterType", newValue.rawValue) }
}
/// Default: `0` (disabled)
static var LastXMin: Int {
get { Prefs.Int("dateFilterLastXMin") }
set { Prefs.Int("dateFilterLastXMin", newValue) }
}
/// Default: `nil` (disabled)
static var RangeA: Timestamp? {
get { Prefs.Obj("dateFilterRangeA") as? Timestamp }
set { Prefs.Obj("dateFilterRangeA", newValue) }
}
/// Default: `nil` (disabled)
static var RangeB: Timestamp? {
get { Prefs.Obj("dateFilterRangeB") as? Timestamp }
set { Prefs.Obj("dateFilterRangeB", newValue) }
}
/// default: `.Date`
static var OrderBy: DateFilterOrderBy {
get { DateFilterOrderBy(rawValue: Prefs.Int("dateFilterOderType"))! }
set { Prefs.Int("dateFilterOderType", newValue.rawValue) }
}
/// default: `false` (Desc)
static var OrderAsc: Bool {
get { Prefs.Bool("dateFilterOderAsc") }
set { Prefs.Bool("dateFilterOderAsc", newValue) }
}
/// - Returns: Timestamp restriction depending on current selected date filter.
/// - `Off` : `(nil, nil)`
/// - `LastXMin` : `(now-LastXMin, nil)`
/// - `ABRange` : `(RangeA, RangeB)`
static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) {
let type = Kind
switch type {
case .Off: return (type, nil, nil)
case .LastXMin: return (type, Timestamp.past(minutes: Prefs.DateFilter.LastXMin), nil)
case .ABRange: return (type, Prefs.DateFilter.RangeA, Prefs.DateFilter.RangeB)
}
}
}
}
// MARK: - ContextAnalyis
extension Prefs {
enum ContextAnalyis {
static var CoOccurrenceTime: Int {
get { Prefs.Int("contextAnalyisCoOccurrenceTime") }
set { Prefs.Int("contextAnalyisCoOccurrenceTime", newValue) }
}
}
}
// MARK: - Notifications
extension Prefs {
enum RecordingReminder {
static var Enabled: Bool {
get { Prefs.Bool("RecordingReminderEnabled") }
set { Prefs.Bool("RecordingReminderEnabled", newValue) }
}
static var Sound: String {
get { Prefs.Str("RecordingReminderSound") ?? "#default" }
set { Prefs.Str("RecordingReminderSound", newValue) }
}
}
}

View File

@@ -0,0 +1,96 @@
import Foundation
enum PrefsShared {
private static var suite: UserDefaults { UserDefaults(suiteName: "group.de.uni-bamberg.psi.AppCheck")! }
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key); suite.synchronize() }
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key); suite.synchronize() }
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key); suite.synchronize() }
static func registerDefaults() {
suite.register(defaults: [
"RestartReminderEnabled" : true,
"RestartReminderWithText" : true,
"RestartReminderWithBadge" : true,
"ConnectionAlertsListsElse" : true,
])
}
static var AutoDeleteLogsDays: Int {
get { Int("AutoDeleteLogsDays") }
set { Int("AutoDeleteLogsDays", newValue) }
}
}
// MARK: - Recording State
enum CurrentRecordingState : Int {
case Off = 0, App = 1, Background = 2
}
extension PrefsShared {
static var CurrentlyRecording: CurrentRecordingState {
get { CurrentRecordingState(rawValue: Int("CurrentlyRecording")) ?? .Off }
set { Int("CurrentlyRecording", newValue.rawValue) }
}
}
// MARK: - Notifications
extension PrefsShared {
enum RestartReminder {
static var Enabled: Bool {
get { PrefsShared.Bool("RestartReminderEnabled") }
set { PrefsShared.Bool("RestartReminderEnabled", newValue) }
}
static var WithText: Bool {
get { PrefsShared.Bool("RestartReminderWithText") }
set { PrefsShared.Bool("RestartReminderWithText", newValue) }
}
static var WithBadge: Bool {
get { PrefsShared.Bool("RestartReminderWithBadge") }
set { PrefsShared.Bool("RestartReminderWithBadge", newValue) }
}
static var Sound: String {
get { PrefsShared.Str("RestartReminderSound") ?? "#default" }
set { PrefsShared.Str("RestartReminderSound", newValue) }
}
}
enum ConnectionAlerts {
static var Enabled: Bool {
get { PrefsShared.Bool("ConnectionAlertsEnabled") }
set { PrefsShared.Bool("ConnectionAlertsEnabled", newValue) }
}
static var Sound: String {
get { PrefsShared.Str("ConnectionAlertsSound") ?? "#default" }
set { PrefsShared.Str("ConnectionAlertsSound", newValue) }
}
static var ExcludeMode: Bool {
get { PrefsShared.Bool("ConnectionAlertsExcludeMode") }
set { PrefsShared.Bool("ConnectionAlertsExcludeMode", newValue) }
}
enum Lists {
static var CustomA: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsCustomA") }
set { PrefsShared.Bool("ConnectionAlertsListsCustomA", newValue) }
}
static var CustomB: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsCustomB") }
set { PrefsShared.Bool("ConnectionAlertsListsCustomB", newValue) }
}
static var Blocked: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsBlocked") }
set { PrefsShared.Bool("ConnectionAlertsListsBlocked", newValue) }
}
static var Else: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsElse") }
set { PrefsShared.Bool("ConnectionAlertsListsElse", newValue) }
}
}
}
}

View File

@@ -2,10 +2,25 @@ import UIKit
struct QuickUI { struct QuickUI {
static func label(_ str: String, frame: CGRect = CGRect.zero, align: NSTextAlignment = .natural, style: UIFont.TextStyle = .body) -> UILabel {
let x = UILabel(frame: frame)
x.text = str
x.textAlignment = align
x.font = .preferredFont(forTextStyle: style)
x.constrainHuggingCompression(.horizontal, .defaultLow)
x.constrainHuggingCompression(.vertical, .defaultHigh)
x.sizeToFit()
if #available(iOS 10.0, *) {
x.adjustsFontForContentSizeCategory = true
}
return x
}
static func button(_ title: String, target: Any? = nil, action: Selector? = nil) -> UIButton { static func button(_ title: String, target: Any? = nil, action: Selector? = nil) -> UIButton {
let x = UIButton(type: .roundedRect) let x = UIButton(type: .roundedRect)
x.setTitle(title, for: .normal) x.setTitle(title, for: .normal)
x.titleLabel?.font = .preferredFont(forTextStyle: .body) x.titleLabel?.font = .preferredFont(forTextStyle: .body)
x.constrainHuggingCompression(.vertical, .defaultHigh)
x.sizeToFit() x.sizeToFit()
if let a = action { x.addTarget(target, action: a, for: .touchUpInside) } if let a = action { x.addTarget(target, action: a, for: .touchUpInside) }
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {

View File

@@ -33,6 +33,13 @@ class SearchBarManager: NSObject, UISearchResultsUpdating {
if #available(iOS 11.0, *) { if #available(iOS 11.0, *) {
tvc?.navigationItem.searchController = controller tvc?.navigationItem.searchController = controller
} else { } else {
let thv = tvc?.tableView.tableHeaderView
guard thv == nil || thv is UISearchBar else {
// Don't overwrite actions bar (co-occurrence, etc.)
// FIXME: find alternative or iOS 9-10 users can't search in hosts
tvc = nil
return
}
controller.loadViewIfNeeded() // Fix: "Attempting to load the view of a view controller while it is deallocating" controller.loadViewIfNeeded() // Fix: "Attempting to load the view of a view controller while it is deallocating"
tvc?.definesPresentationContext = true // make search bar disappear if user changes scene (eg. select cell) tvc?.definesPresentationContext = true // make search bar disappear if user changes scene (eg. select cell)
//tvc?.tableView.backgroundView = UIView() // iOS 11+ bug: bright white background in dark mode //tvc?.tableView.backgroundView = UIView() // iOS 11+ bug: bright white background in dark mode
@@ -42,7 +49,7 @@ class SearchBarManager: NSObject, UISearchResultsUpdating {
} }
/// Search callback /// Search callback
func updateSearchResults(for controller: UISearchController) { internal func updateSearchResults(for controller: UISearchController) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil) NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
perform(#selector(performSearch), with: nil, afterDelay: 0.2) perform(#selector(performSearch), with: nil, afterDelay: 0.2)
} }

View File

@@ -0,0 +1,182 @@
import UIKit
enum PresentationEdge { case left, top, right, bottom }
// ########################################
// #
// # MARK: - Transitioning Delegate
// #
// ########################################
class SlideInTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
private var edge: PresentationEdge
private var modal: Bool
private var dismissable: Bool
private var shadow: UIColor?
init(for edge: PresentationEdge, modal: Bool, tapAnywhereToDismiss: Bool = false, modalBackgroundColor color: UIColor? = nil) {
self.edge = edge
self.dismissable = tapAnywhereToDismiss
self.shadow = color
self.modal = modal
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
StickyPresentationController(presented: presented, presenting: presenting, stickTo: edge, modal: modal, tapAnywhereToDismiss: dismissable, modalBackgroundColor: shadow)
}
func animationController(forPresented _: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
SlideInAnimationController(from: edge, isPresentation: true)
}
func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
SlideInAnimationController(from: edge, isPresentation: false)
}
}
// ########################################
// #
// # MARK: - Animated Transitioning
// #
// ########################################
private final class SlideInAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let edge: PresentationEdge
let appear: Bool
init(from edge: PresentationEdge, isPresentation: Bool) {
self.edge = edge
self.appear = isPresentation
super.init()
}
func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval {
(context?.isAnimated ?? true) ? 0.3 : 0.0
}
func animateTransition(using context: UIViewControllerContextTransitioning) {
guard let vc = context.viewController(forKey: appear ? .to : .from) else { return }
var to = context.finalFrame(for: vc)
var from = to
switch edge {
case .left: from.origin.x = -to.width
case .right: from.origin.x = context.containerView.frame.width
case .top: from.origin.y = -to.height
case .bottom: from.origin.y = context.containerView.frame.height
}
if appear { context.containerView.addSubview(vc.view) }
else { swap(&from, &to) }
vc.view.frame = from
UIView.animate(withDuration: transitionDuration(using: context), animations: {
vc.view.frame = to
}, completion: { finished in
if !self.appear { vc.view.removeFromSuperview() }
context.completeTransition(finished)
})
}
}
// #########################################
// #
// # MARK: - Presentation Controller
// #
// #########################################
private class StickyPresentationController: UIPresentationController {
private let stickTo: PresentationEdge
private let isModal: Bool
private let bg = UIView()
private var availableSize: CGSize = .zero // save original size when resizing the container
override var shouldPresentInFullscreen: Bool { false }
override var frameOfPresentedViewInContainerView: CGRect { fittedContentFrame() }
required init(presented: UIViewController, presenting: UIViewController?, stickTo edge: PresentationEdge, modal: Bool = true, tapAnywhereToDismiss: Bool = false, modalBackgroundColor bgColor: UIColor? = nil) {
self.stickTo = edge
self.isModal = modal
super.init(presentedViewController: presented, presenting: presenting)
bg.backgroundColor = bgColor ?? .init(white: 0, alpha: 0.5)
if modal, tapAnywhereToDismiss {
bg.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(didTapBackground))
)
}
}
// MARK: Present
override func presentationTransitionWillBegin() {
availableSize = containerView!.frame.size
guard isModal else { return }
containerView!.insertSubview(bg, at: 0)
bg.alpha = 0.0
if presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.bg.alpha = 1.0
}) != true { bg.alpha = 1.0 }
}
@objc func didTapBackground(_ sender: UITapGestureRecognizer) {
presentingViewController.dismiss(animated: true)
}
// MARK: Dismiss
override func dismissalTransitionWillBegin() {
if presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.bg.alpha = 0.0
}) != true { bg.alpha = 0.0 }
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed { bg.removeFromSuperview() }
}
// MARK: Update
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
availableSize = size
super.viewWillTransition(to: size, with: coordinator)
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
bg.frame = containerView!.bounds
if isModal {
presentedView!.frame = fittedContentFrame()
} else {
containerView!.frame = fittedContentFrame()
presentedView!.frame = containerView!.bounds
}
}
/// Calculate `fittedContentSize()` then offset frame to sticky edge respecting *available* container size .
func fittedContentFrame() -> CGRect {
var frame = CGRect(origin: .zero, size: fittedContentSize())
switch stickTo {
case .right: frame.origin.x = availableSize.width - frame.width
case .bottom: frame.origin.y = availableSize.height - frame.height
default: break
}
return frame
}
/// Calculate best fitting size for available container size and presentation sticky edge.
func fittedContentSize() -> CGSize {
guard let target = presentedView else { return availableSize }
let full = availableSize
let preferred = presentedViewController.preferredContentSize
switch stickTo {
case .left, .right:
let fitted = target.fittingSize(fixedHeight: full.height, preferredWidth: preferred.width)
return CGSize(width: min(fitted.width, full.width), height: full.height)
case .top, .bottom:
let fitted = target.fittingSize(fixedWidth: full.width, preferredHeight: preferred.height)
return CGSize(width: full.width, height: min(fitted.height, full.height))
}
}
}

View File

@@ -0,0 +1,31 @@
import Foundation
class ThrottledBatchQueue<T> {
private var cache: [T] = []
private var scheduled: Bool = false
private let queue: DispatchQueue
private let delay: Double
init(_ delay: Double, using queue: DispatchQueue) {
self.queue = queue
self.delay = delay
}
func addDelayed(_ elem: T, afterDelay closure: @escaping ([T]) -> Void) {
queue.sync {
cache.append(elem)
guard !scheduled else {
return
}
scheduled = true
queue.asyncAfter(deadline: .now() + delay) {
let aCopy = self.cache
self.cache.removeAll(keepingCapacity: true)
self.scheduled = false
DispatchQueue.main.async {
closure(aCopy)
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
import UIKit
struct TinyMarkdown {
/// Load markdown file and run through a (very) simple parser (see below).
/// - Parameters:
/// - filename: Will automatically append `.md` extension
/// - replacements: Replace a single occurrence of search string with an attributed replacement.
static func load(_ filename: String, replacements: [String : NSMutableAttributedString] = [:]) -> UITextView {
let url = Bundle.main.url(forResource: filename, withExtension: "md")!
let str = NSMutableAttributedString(withMarkdown: try! String(contentsOf: url))
for (key, val) in replacements {
guard let r = str.string.range(of: key) else {
QLog.Debug("WARN: markdown key '\(key)' does not exist in \(filename)")
continue
}
str.replaceCharacters(in: NSRange(r, in: str.string), with: val)
}
return QuickUI.text(attributed: str)
}
}
extension NSMutableAttributedString {
/// Supports only: `#h1`, `##h2`, `###h3`, `_italic_`, `__bold__`, `___boldItalic___`
convenience init(withMarkdown content: String) {
self.init()
let emph = try! NSRegularExpression(pattern: #"(?<=(^|\W))(_{1,3})(\S|\S.*?\S)\2"#, options: [])
beginEditing()
content.enumerateLines { (line, _) in
if line.starts(with: "#") {
var h = 0
for char in line {
if char == "#" { h += 1 }
else { break }
}
var line = line
line.removeFirst(h)
line = line.trimmingCharacters(in: CharacterSet(charactersIn: " "))
switch h {
case 1: self.h1(line + "\n")
case 2: self.h2(line + "\n")
default: self.h3(line + "\n")
}
} else {
let nsline = line as NSString
let range = NSRange(location: 0, length: nsline.length)
var i = 0
for x in emph.matches(in: line, options: [], range: range) {
let r = x.range
self.normal(nsline.substring(from: i, to: r.location))
i = r.upperBound
let before = nsline.substring(with: r)
let after = before.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
switch (before.count - after.count) / 2 {
case 1: self.italic(after)
case 2: self.bold(after)
default: self.boldItalic(after)
}
}
if i < range.length {
self.normal(nsline.substring(from: i, to: range.length) + "\n")
} else {
self.normal("\n")
}
}
}
endEditing()
}
}

View File

@@ -25,7 +25,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
private let sheetBg: UIView = { private let sheetBg: UIView = {
let x = UIView(frame: uniRect) let x = UIView(frame: uniRect)
x.autoresizingMask = [.flexibleWidth, .flexibleHeight] x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
x.backgroundColor = .sysBg x.backgroundColor = .sysBackground
x.layer.cornerRadius = cornerRadius x.layer.cornerRadius = cornerRadius
x.layer.shadowColor = UIColor.black.cgColor x.layer.shadowColor = UIColor.black.cgColor
x.layer.shadowRadius = 10 x.layer.shadowRadius = 10
@@ -37,8 +37,8 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
private let pager: UIPageControl = { private let pager: UIPageControl = {
let x = UIPageControl(frame: uniRect) let x = UIPageControl(frame: uniRect)
x.frame.size.height = x.size(forNumberOfPages: 1).height x.frame.size.height = x.size(forNumberOfPages: 1).height
x.currentPageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.5) x.currentPageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.5)
x.pageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.25) x.pageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.25)
x.numberOfPages = 0 x.numberOfPages = 0
x.hidesForSinglePage = true x.hidesForSinglePage = true
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged) x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
@@ -59,7 +59,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
return x return x
}() }()
private let button: UIButton = { private lazy var button: UIButton = {
let x = QuickUI.button("", target: self, action: #selector(buttonTapped)) let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
x.contentEdgeInsets = UIEdgeInsets(all: 8) x.contentEdgeInsets = UIEdgeInsets(all: 8)
return x return x
@@ -132,9 +132,9 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
sheetBg.addSubview(button) sheetBg.addSubview(button)
pager.anchor([.top, .left, .right], to: sheetBg) pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor pageScroll.topAnchor =&= pager.bottomAnchor | .defaultHigh
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor button.topAnchor =&= pageScroll.bottomAnchor | .defaultHigh
button.anchor([.bottom, .centerX], to: sheetBg) button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30 // button.bottomAnchor =&= sheetBg.bottomAnchor - 30
// button.centerXAnchor =&= sheetBg.centerXAnchor // button.centerXAnchor =&= sheetBg.centerXAnchor

View File

@@ -20,27 +20,19 @@ extension SQLiteDatabase {
try ifStep(stmt, SQLITE_ROW) try ifStep(stmt, SQLITE_ROW)
return sqlite3_column_int(stmt, 0) return sqlite3_column_int(stmt, 0)
} }
if version != 1 { if version != 2 {
QLog.Info("migrate db \(version) -> 2")
// version 0 -> 1: req(domain) -> heap(fqdn, domain) // version 0 -> 1: req(domain) -> heap(fqdn, domain)
if version == 0 { // version 1 -> 2: rec(+subtitle, +opt)
try tempMigrate() if version == 1 {
transaction("""
ALTER TABLE rec ADD COLUMN subtitle TEXT;
ALTER TABLE rec ADD COLUMN uploadkey TEXT;
""")
} }
try run(sql: "PRAGMA user_version = 1;") try run(sql: "PRAGMA user_version = 2;")
} }
} }
private func tempMigrate() throws { // TODO: remove with next internal release
do {
try run(sql: "SELECT 1 FROM req LIMIT 1;") // fails if req doesnt exist
createFunction("domainof") { ($0.first as! String).extractDomain() }
try run(sql: """
BEGIN TRANSACTION;
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,domain,domainof(domain),nullif(logOpt,0) FROM req;
DROP TABLE req;
COMMIT;
""")
} catch { /* no need to migrate */ }
}
} }
private enum TableName: String { private enum TableName: String {
@@ -54,6 +46,10 @@ extension SQLiteDatabase {
return sqlite3_column_int64($0, 0) return sqlite3_column_int64($0, 0)
}) ?? 0 }) ?? 0
} }
fileprivate func col_ts(_ stmt: OpaquePointer, _ col: Int32) -> Timestamp {
sqlite3_column_int64(stmt, col)
}
} }
class WhereClauseBuilder: CustomStringConvertible { class WhereClauseBuilder: CustomStringConvertible {
@@ -105,6 +101,7 @@ struct GroupedDomain {
var options: FilterOptions? = nil var options: FilterOptions? = nil
} }
typealias GroupedTsOccurrence = (ts: Timestamp, total: Int32, blocked: Int32) typealias GroupedTsOccurrence = (ts: Timestamp, total: Int32, blocked: Int32)
typealias DomainTsPair = (domain: String, ts: Timestamp)
extension SQLiteDatabase { extension SQLiteDatabase {
@@ -116,11 +113,9 @@ extension SQLiteDatabase {
guard lastRowId(.cache) > 0 else { return nil } guard lastRowId(.cache) > 0 else { return nil }
let before = lastRowId(.heap) + 1 let before = lastRowId(.heap) + 1
createFunction("domainof") { ($0.first as! String).extractDomain() } createFunction("domainof") { ($0.first as! String).extractDomain() }
try? run(sql:""" transaction("""
BEGIN TRANSACTION;
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache; INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache;
DELETE FROM cache; DELETE FROM cache;
COMMIT;
""") """)
let after = lastRowId(.heap) let after = lastRowId(.heap)
return (before > after) ? nil : (before, after) return (before > after) ? nil : (before, after)
@@ -150,7 +145,7 @@ extension SQLiteDatabase {
func dnsLogsMinDate() -> Timestamp? { func dnsLogsMinDate() -> Timestamp? {
try? run(sql:"SELECT min(ts) FROM heap") { try? run(sql:"SELECT min(ts) FROM heap") {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return sqlite3_column_int64($0, 0) return col_ts($0, 0)
} }
} }
@@ -164,8 +159,19 @@ extension SQLiteDatabase {
let Where = WhereClauseBuilder().and(in: range).and(min: ts1, max: ts2) let Where = WhereClauseBuilder().and(in: range).and(min: ts1, max: ts2)
return try? run(sql:"SELECT min(rowid), max(rowid) FROM heap \(Where);", bind: Where.bindings) { return try? run(sql:"SELECT min(rowid), max(rowid) FROM heap \(Where);", bind: Where.bindings) {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
let max = sqlite3_column_int64($0, 1) let max = col_ts($0, 1)
return (max == 0) ? nil : (sqlite3_column_int64($0, 0), max) return (max == 0) ? nil : (col_ts($0, 0), max)
}
}
/// Get raw logs between two timestamps. `ts >= ? AND ts <= ?`
/// - Returns: List sorted by `ts` in descending order (newest entries first).
func dnsLogs(between ts1: Timestamp, and ts2: Timestamp) -> [DomainTsPair]? {
try? run(sql: "SELECT fqdn, ts FROM heap WHERE ts >= ? AND ts <= ? ORDER BY ts DESC, rowid ASC;",
bind: [BindInt64(ts1), BindInt64(ts2)]) {
allRows($0) {
(col_text($0, 0) ?? "", col_ts($0, 1))
}
} }
} }
@@ -189,10 +195,10 @@ extension SQLiteDatabase {
} }
return try? run(sql: "SELECT \(col), COUNT(*), COUNT(opt), MAX(ts) FROM heap \(Where) GROUP BY \(col);", bind: Where.bindings) { return try? run(sql: "SELECT \(col), COUNT(*), COUNT(opt), MAX(ts) FROM heap \(Where) GROUP BY \(col);", bind: Where.bindings) {
allRows($0) { allRows($0) {
GroupedDomain(domain: readText($0, 0) ?? "", GroupedDomain(domain: col_text($0, 0) ?? "",
total: sqlite3_column_int($0, 1), total: sqlite3_column_int($0, 1),
blocked: sqlite3_column_int($0, 2), blocked: sqlite3_column_int($0, 2),
lastModified: sqlite3_column_int64($0, 3)) lastModified: col_ts($0, 3))
} }
} }
} }
@@ -206,7 +212,7 @@ extension SQLiteDatabase {
let Where = WhereClauseBuilder().and(in: range).and("fqdn = ?", BindText(fqdn)) let Where = WhereClauseBuilder().and(in: range).and("fqdn = ?", BindText(fqdn))
return try? run(sql: "SELECT ts, COUNT(ts), COUNT(opt) FROM heap \(Where) GROUP BY ts ORDER BY ts DESC;", bind: Where.bindings) { return try? run(sql: "SELECT ts, COUNT(ts), COUNT(opt) FROM heap \(Where) GROUP BY ts ORDER BY ts DESC;", bind: Where.bindings) {
allRows($0) { allRows($0) {
(sqlite3_column_int64($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2)) (col_ts($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2))
} }
} }
} }
@@ -228,9 +234,10 @@ extension SQLiteDatabase {
} }
/// Get sorted, unique list of `ts` with given `fqdn`. /// Get sorted, unique list of `ts` with given `fqdn`.
func dnsLogsUniqTs(_ fqdn: String) -> [Timestamp]? { func dnsLogsUniqTs(_ domain: String, isFQDN flag: Bool) -> [Timestamp]? {
try? run(sql: "SELECT DISTINCT ts FROM heap WHERE fqdn = ? ORDER BY ts;", bind: [BindText(fqdn)]) { try? run(sql: "SELECT DISTINCT ts FROM heap WHERE \(flag ? "fqdn" : "domain") = ? ORDER BY ts;",
allRows($0) { sqlite3_column_int64($0, 0) } bind: [BindText(domain)]) {
allRows($0) { col_ts($0, 0) }
} }
} }
@@ -241,7 +248,7 @@ extension SQLiteDatabase {
/// - dt: Search for `ts - dt <= X <= ts + dt` /// - dt: Search for `ts - dt <= X <= ts + dt`
/// - fqdn: Rows matching this domain will be excluded from the result set. /// - fqdn: Rows matching this domain will be excluded from the result set.
/// - Returns: List of tuples ordered by rank (ASC). /// - Returns: List of tuples ordered by rank (ASC).
func contextAnalysis(coOccurrence times: [Timestamp], plusMinus dt: Timestamp, exclude fqdn: String) -> [ContextAnalysisResult]? { func contextAnalysis(coOccurrence times: [Timestamp], plusMinus dt: Timestamp, exclude domain: String, isFQDN flag: Bool) -> [ContextAnalysisResult]? {
guard times.count > 0 else { return nil } guard times.count > 0 else { return nil }
createFunction("fnDist") { createFunction("fnDist") {
let x = $0.first as! Timestamp let x = $0.first as! Timestamp
@@ -266,12 +273,12 @@ extension SQLiteDatabase {
SELECT fqdn, count, avg, (\(fnRank)) rank FROM ( SELECT fqdn, count, avg, (\(fnRank)) rank FROM (
SELECT fqdn, COUNT(*) count, AVG(dist) avg FROM ( SELECT fqdn, COUNT(*) count, AVG(dist) avg FROM (
SELECT fqdn, fnDist(ts) dist FROM heap SELECT fqdn, fnDist(ts) dist FROM heap
WHERE ts BETWEEN ? AND ? AND fqdn != ? AND dist <= ? WHERE ts BETWEEN ? AND ? AND \(flag ? "fqdn" : "domain") != ? AND dist <= ?
) GROUP BY fqdn ) GROUP BY fqdn
) ORDER BY rank ASC LIMIT 99; ) ORDER BY rank ASC LIMIT 99;
""", bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(fqdn), BindInt64(dt)]) { """, bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(domain), BindInt64(dt)]) {
allRows($0) { allRows($0) {
(readText($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3)) (col_text($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3))
} }
} }
} }
@@ -282,7 +289,7 @@ extension SQLiteDatabase {
// MARK: - Recordings // MARK: - Recordings
extension CreateTable { extension CreateTable {
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `notes`: String /// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `subtitle`: String, `notes`: String
static var rec: String {""" static var rec: String {"""
CREATE TABLE IF NOT EXISTS rec( CREATE TABLE IF NOT EXISTS rec(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@@ -290,19 +297,25 @@ extension CreateTable {
stop INTEGER, stop INTEGER,
appid TEXT, appid TEXT,
title TEXT, title TEXT,
notes TEXT subtitle TEXT,
notes TEXT,
uploadkey TEXT
); );
"""} """}
} }
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, uploadkey"
struct Recording { struct Recording {
let id: sqlite3_int64 let id: sqlite3_int64
let start: Timestamp let start: Timestamp
let stop: Timestamp? let stop: Timestamp?
var appId: String? = nil var appId: String? = nil
var title: String? = nil var title: String? = nil
var subtitle: String? = nil
var notes: String? = nil var notes: String? = nil
var uploadkey: String? = nil
} }
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
extension SQLiteDatabase { extension SQLiteDatabase {
@@ -329,8 +342,9 @@ extension SQLiteDatabase {
/// Update given recording by replacing `title`, `appid`, and `notes` with new values. /// Update given recording by replacing `title`, `appid`, and `notes` with new values.
func recordingUpdate(_ r: Recording) { func recordingUpdate(_ r: Recording) {
try? run(sql: "UPDATE rec SET title = ?, appid = ?, notes = ? WHERE id = ? LIMIT 1;", try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ?, uploadkey = ? WHERE id = ? LIMIT 1;",
bind: [BindTextOrNil(r.title), BindTextOrNil(r.appId), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle),
BindTextOrNil(r.notes), BindTextOrNil(r.uploadkey), BindInt64(r.id)]) { stmt -> Void in
sqlite3_step(stmt) sqlite3_step(stmt)
} }
} }
@@ -348,37 +362,55 @@ extension SQLiteDatabase {
// MARK: read // MARK: read
private func readRecording(_ stmt: OpaquePointer) -> Recording { private func readRecording(_ stmt: OpaquePointer) -> Recording {
let end = sqlite3_column_int64(stmt, 2) let end = col_ts(stmt, 2)
return Recording(id: sqlite3_column_int64(stmt, 0), return Recording(id: sqlite3_column_int64(stmt, 0),
start: sqlite3_column_int64(stmt, 1), start: col_ts(stmt, 1),
stop: end == 0 ? nil : end, stop: end == 0 ? nil : end,
appId: readText(stmt, 3), appId: col_text(stmt, 3),
title: readText(stmt, 4), title: col_text(stmt, 4),
notes: readText(stmt, 5)) subtitle: col_text(stmt, 5),
notes: col_text(stmt, 6),
uploadkey: col_text(stmt, 7))
} }
/// `WHERE stop IS NULL` /// `WHERE stop IS NULL`
func recordingGetOngoing() -> Recording? { func recordingGetOngoing() -> Recording? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NULL LIMIT 1;") { try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NULL LIMIT 1;") {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return readRecording($0) return readRecording($0)
} }
} }
/// Get `Timestamp` of last recording.
func recordingLastTimestamp() -> Timestamp? {
try? run(sql: "SELECT stop FROM rec WHERE stop IS NOT NULL ORDER BY rowid DESC LIMIT 1;") {
try ifStep($0, SQLITE_ROW)
return col_ts($0, 0)
}
}
/// `WHERE stop IS NOT NULL` /// `WHERE stop IS NOT NULL`
func recordingGetAll() -> [Recording]? { func recordingGetAll() -> [Recording]? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NOT NULL;") { try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NOT NULL;") {
allRows($0) { readRecording($0) } allRows($0) { readRecording($0) }
} }
} }
/// `WHERE id = ?` /// `WHERE id = ?`
private func recordingGet(withID: sqlite3_int64) throws -> Recording { private func recordingGet(withID: sqlite3_int64) throws -> Recording {
try run(sql: "SELECT * FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) { try run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return readRecording($0) return readRecording($0)
} }
} }
func appBundleList() -> [AppBundleInfo]? {
try? run(sql: "SELECT appid, title, subtitle FROM rec WHERE appid IS NOT NULL GROUP BY appid ORDER BY title ASC;") {
allRows($0) {
AppBundleInfo(col_text($0, 0)!, col_text($0, 1), col_text($0, 2))
}
}
}
} }
@@ -396,8 +428,6 @@ extension CreateTable {
"""} """}
} }
typealias RecordLog = (domain: String, count: Int32)
extension SQLiteDatabase { extension SQLiteDatabase {
// MARK: write // MARK: write
@@ -426,13 +456,24 @@ extension SQLiteDatabase {
} }
} }
/// Delete one recording log entry with given `recording id`, matching `domain`, and `ts`.
/// - Returns: `true` if row was deleted
func recordingLogsDelete(_ recId: sqlite3_int64, singleEntry ts: Timestamp, domain: String) throws -> Bool {
try run(sql: "DELETE FROM recLog WHERE rid = ? AND ts = ? AND domain = ? LIMIT 1;",
bind: [BindInt64(recId), BindInt64(ts), BindText(domain)]) {
try ifStep($0, SQLITE_DONE)
return numberOfChanges > 0
}
}
// MARK: read // MARK: read
/// List of domains and count occurences for given recording. /// List of domains and count occurences for given recording.
func recordingLogsGetGrouped(_ r: Recording) -> [RecordLog]? { /// - Returns: List of `(domain, ts)` pairs. Sorted by `ts` in ascending order (oldest first)
try? run(sql: "SELECT domain, COUNT() FROM recLog WHERE rid = ? GROUP BY domain;", func recordingLogsGet(_ r: Recording) -> [DomainTsPair]? {
try? run(sql: "SELECT domain, ts FROM recLog WHERE rid = ? ORDER BY ts ASC, rowid DESC;",
bind: [BindInt64(r.id)]) { bind: [BindInt64(r.id)]) {
allRows($0) { (readText($0, 0) ?? "", sqlite3_column_int($0, 1)) } allRows($0) { (col_text($0, 0) ?? "", col_ts($0, 1)) }
} }
} }
} }

View File

@@ -39,9 +39,26 @@ extension SQLiteDatabase {
/// `INSERT INTO cache (dns, opt) VALUES (?, ?);` /// `INSERT INTO cache (dns, opt) VALUES (?, ?);`
func logWrite(_ domain: String, blocked: Bool = false) throws { func logWrite(_ domain: String, blocked: Bool = false) throws {
try self.run(sql: "INSERT INTO cache (dns, opt) VALUES (?, ?);", try self.run(sql: "INSERT INTO cache (dns, opt) VALUES (?, ?);",
bind: [BindText(domain), BindInt32(blocked ? 1 : 0)]) bind: [BindText(domain), BindInt32(blocked ? 1 : 0)])
{ try ifStep($0, SQLITE_DONE) } { try ifStep($0, SQLITE_DONE) }
} }
/// `DELETE FROM cache WHERE ts < (now - ? days);`
/// - Parameter days: if `0` or negative, this function does nothing.
/// - Returns: `true` if at least one row was deleted.
@discardableResult func dnsLogsDeleteOlderThan(days: Int) throws -> Bool {
guard days > 0 else { return false }
func delFrom(_ table: String) throws -> Bool {
return try self.run(sql: "DELETE FROM \(table) WHERE ts < strftime('%s', 'now', ?);",
bind: [BindText("-\(days) days")]) {
try ifStep($0, SQLITE_DONE)
return numberOfChanges > 0
}
}
let didDelHeap = try delFrom("heap")
let didDelCache = try delFrom("cache")
return didDelHeap || didDelCache
}
} }
@@ -62,7 +79,9 @@ struct FilterOptions: OptionSet {
static let none = FilterOptions([]) static let none = FilterOptions([])
static let blocked = FilterOptions(rawValue: 1 << 0) static let blocked = FilterOptions(rawValue: 1 << 0)
static let ignored = FilterOptions(rawValue: 1 << 1) static let ignored = FilterOptions(rawValue: 1 << 1)
static let any = FilterOptions(rawValue: 0b11) static let customA = FilterOptions(rawValue: 1 << 2)
static let customB = FilterOptions(rawValue: 1 << 3)
static let any = FilterOptions(rawValue: 0b1111)
} }
extension SQLiteDatabase { extension SQLiteDatabase {
@@ -71,7 +90,7 @@ extension SQLiteDatabase {
return try? run(sql: "SELECT domain, opt FROM filter \(rv>0 ? "WHERE opt & ?" : "");", return try? run(sql: "SELECT domain, opt FROM filter \(rv>0 ? "WHERE opt & ?" : "");",
bind: rv>0 ? [BindInt32(rv)] : []) { bind: rv>0 ? [BindInt32(rv)] : []) {
allRowsKeyed($0) { allRowsKeyed($0) {
(key: readText($0, 0) ?? "", (key: col_text($0, 0) ?? "",
value: FilterOptions(rawValue: sqlite3_column_int($0, 1))) value: FilterOptions(rawValue: sqlite3_column_int($0, 1)))
} }
} }

View File

@@ -48,6 +48,7 @@ class SQLiteDatabase {
static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase { static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase {
var db: OpaquePointer? var db: OpaquePointer?
if sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK { if sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK {
sqlite3_busy_timeout(db, 800)
return SQLiteDatabase(dbPointer: db) return SQLiteDatabase(dbPointer: db)
} else { } else {
defer { sqlite3_close_v2(db) } defer { sqlite3_close_v2(db) }
@@ -91,15 +92,20 @@ class SQLiteDatabase {
} }
} }
/// `BEGIN TRANSACTION; \(sql); COMMIT;` on exception rollback.
func transaction(_ sql: String) {
do { try run(sql: "BEGIN TRANSACTION; \(sql); COMMIT;") }
catch { rollback() }
}
func ifStep(_ stmt: OpaquePointer, _ expected: Int32) throws { func ifStep(_ stmt: OpaquePointer, _ expected: Int32) throws {
guard sqlite3_step(stmt) == expected else { guard sqlite3_step(stmt) == expected else {
throw SQLiteError.Step(message: errorMessage) throw SQLiteError.Step(message: errorMessage)
} }
} }
func vacuum() { func vacuum() { NSLog("[SQL] VACUUM"); try? run(sql: "VACUUM;"); }
try? run(sql: "VACUUM;") func rollback() { NSLog("[SQL] ROLLBACK"); try? run(sql: "ROLLBACK;"); }
}
} }
@@ -163,6 +169,10 @@ protocol DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32
} }
struct BindNull : DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_null(stmt, col) }
}
struct BindInt32 : DBBinding { struct BindInt32 : DBBinding {
let raw: Int32 let raw: Int32
init(_ value: Int32) { raw = value } init(_ value: Int32) { raw = value }
@@ -193,7 +203,7 @@ extension SQLiteDatabase {
var numberOfChanges: Int32 { get { sqlite3_changes(dbPointer) } } var numberOfChanges: Int32 { get { sqlite3_changes(dbPointer) } }
var lastInsertedRow: SQLiteRowID { get { sqlite3_last_insert_rowid(dbPointer) } } var lastInsertedRow: SQLiteRowID { get { sqlite3_last_insert_rowid(dbPointer) } }
func readText(_ stmt: OpaquePointer, _ col: Int32) -> String? { func col_text(_ stmt: OpaquePointer, _ col: Int32) -> String? {
let val = sqlite3_column_text(stmt, col) let val = sqlite3_column_text(stmt, col)
return (val != nil ? String(cString: val!) : nil) return (val != nil ? String(cString: val!) : nil)
} }

View File

@@ -33,8 +33,11 @@ extension FilterOptions {
} }
extension Recording { extension Recording {
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } } var fallbackTitle: String { get {
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } } isLongTerm ? "Background Recording" : "Unnamed Recording #\(id)"
var durationString: String? { get { stop == nil ? nil : TimeFormat.from(duration!) } } } }
var duration: Timestamp { get { (stop ?? .now()) - start } }
var isLongTerm: Bool { duration > Timestamp.hours(1) }
var isShared: Bool { uploadkey?.count ?? 0 > 0}
} }

View File

@@ -15,7 +15,7 @@ struct TheGreatDestroyer {
} }
} }
/// Fired when user taps on Settings -> Delete All Logs /// Fired when user taps on Settings -> "Delete All Logs"
static func deleteAllLogs() { static func deleteAllLogs() {
sync.pause() sync.pause()
DispatchQueue.global().async { DispatchQueue.global().async {
@@ -26,4 +26,21 @@ struct TheGreatDestroyer {
} catch {} } catch {}
} }
} }
/// Fired when user changes Settings -> "Auto-delete logs" and every time the App enters foreground
static func deleteLogs(olderThan days: Int) {
guard days > 0 else { return }
sync.pause()
DispatchQueue.global().async {
defer { sync.continue() }
QLog.Info("Auto-delete logs")
do {
if try AppDB!.dnsLogsDeleteOlderThan(days: days) {
sync.needsReloadDB()
}
} catch {
QLog.Warning("Couldn't auto-delete logs, \(error)")
}
}
}
} }

View File

@@ -21,10 +21,13 @@ enum DomainFilter {
} }
/// Get total number of blocked and ignored domains. Shown in settings overview. /// Get total number of blocked and ignored domains. Shown in settings overview.
static func counts() -> (blocked: Int, ignored: Int) { static func counts() -> (blocked: Int, ignored: Int, listCustomA: Int, listCustomB: Int) {
data.reduce(into: (0, 0)) { data.reduce(into: (0, 0, 0, 0)) {
if $1.1.contains(.blocked) { $0.0 += 1 } if $1.1.contains(.blocked) { $0.0 += 1 }
if $1.1.contains(.ignored) { $0.1 += 1 } } if $1.1.contains(.ignored) { $0.1 += 1 }
if $1.1.contains(.customA) { $0.2 += 1 }
if $1.1.contains(.customB) { $0.3 += 1 }
}
} }
/// Union `filter` with set. /// Union `filter` with set.

View File

@@ -55,8 +55,8 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
/// Read user defaults and apply new sorting order. Either by setting a new or reversing the current. /// Read user defaults and apply new sorting order. Either by setting a new or reversing the current.
/// - Parameter force: If `true` set new sorting even if the type does not differ. /// - Parameter force: If `true` set new sorting even if the type does not differ.
private func resetSortingOrder(force: Bool = false) { private func resetSortingOrder(force: Bool = false) {
let orderAscChanged = (orderAsc <-? Pref.DateFilter.OrderAsc) let orderAscChanged = (orderAsc <-? Prefs.DateFilter.OrderAsc)
let orderTypChanged = (currentOrder <-? Pref.DateFilter.OrderBy) let orderTypChanged = (currentOrder <-? Prefs.DateFilter.OrderBy)
if orderTypChanged || force { if orderTypChanged || force {
switch currentOrder { switch currentOrder {
case .Date: case .Date:

View File

@@ -13,6 +13,9 @@ enum RecordingsDB {
/// Get list of all recordings /// Get list of all recordings
static func list() -> [Recording] { AppDB?.recordingGetAll() ?? [] } static func list() -> [Recording] { AppDB?.recordingGetAll() ?? [] }
/// Get `Timestamp` of latest recording
static func lastTimestamp() -> Timestamp? { AppDB?.recordingLastTimestamp() }
/// Copy log entries from generic `heap` table to recording specific `recLog` table /// Copy log entries from generic `heap` table to recording specific `recLog` table
static func persist(_ r: Recording) { static func persist(_ r: Recording) {
sync.syncNow { // persist changes in cache before copying recording details sync.syncNow { // persist changes in cache before copying recording details
@@ -21,8 +24,20 @@ enum RecordingsDB {
} }
/// Get list of domains that occured during the recording /// Get list of domains that occured during the recording
static func details(_ r: Recording) -> [RecordLog] { static func details(_ r: Recording) -> [DomainTsPair] {
AppDB?.recordingLogsGetGrouped(r) ?? [] AppDB?.recordingLogsGet(r) ?? []
}
/// Get dictionary of domains with `ts` in ascending order.
static func detailCluster(_ r: Recording) -> [String : [Timestamp]] {
var cluster: [String : [Timestamp]] = [:]
for (dom, ts) in details(r) {
if cluster[dom] == nil {
cluster[dom] = []
}
cluster[dom]!.append(ts - r.start)
}
return cluster
} }
/// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification. /// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification.
@@ -43,5 +58,16 @@ enum RecordingsDB {
static func deleteDetails(_ r: Recording, domain: String) -> Bool { static func deleteDetails(_ r: Recording, domain: String) -> Bool {
((try? AppDB?.recordingLogsDelete(r.id, matchingDomain: domain)) ?? 0) > 0 ((try? AppDB?.recordingLogsDelete(r.id, matchingDomain: domain)) ?? 0) > 0
} }
/// Delete individual entries from recording while keeping the recording alive.
/// - Returns: `true` if at least one row is deleted.
static func deleteSingle(_ r: Recording, domain: String, ts: Timestamp) -> Bool {
(try? AppDB?.recordingLogsDelete(r.id, singleEntry: ts, domain: domain)) ?? false
}
/// Return list of previously used apps found in all recordings.
static func appList() -> [AppBundleInfo] {
AppDB?.appBundleList() ?? []
}
} }

View File

@@ -2,7 +2,10 @@ import Foundation
#if IOS_SIMULATOR #if IOS_SIMULATOR
class TestDataSource { fileprivate var hook : GlassVPNHook!
class SimulatorVPN {
static var timer: Timer?
static func load() { static func load() {
QLog.Debug("SQLite path: \(URL.internalDB())") QLog.Debug("SQLite path: \(URL.internalDB())")
@@ -27,13 +30,36 @@ class TestDataSource {
db.setFilter("bi.test.com", [.blocked, .ignored]) db.setFilter("bi.test.com", [.blocked, .ignored])
QLog.Debug("Done") QLog.Debug("Done")
}
Timer.repeating(2, call: #selector(insertRandom), on: self)
static func start() {
hook = GlassVPNHook()
timer = Timer.repeating(2, call: #selector(insertRandom), on: self)
}
static func stop() {
timer?.invalidate()
timer = nil
hook.cleanUp()
hook = nil
} }
@objc static func insertRandom() { @objc static func insertRandom() {
//QLog.Debug("Inserting 1 periodic log entry") //QLog.Debug("Inserting 1 periodic log entry")
try? AppDB?.logWrite("\(arc4random() % 5).count.test.com", blocked: true) let rand = arc4random() % 8
let domain: String
switch rand {
case 6: domain = "tmp.b.test.com"
case 7: domain = "tmp.i.test.com"
case 8: domain = "tmp.bi.test.com"
default: domain = "\(rand).count.test.com"
}
let kill = hook.processDNSRequest(domain)
if kill { QLog.Info("Blocked: \(domain)") }
}
static func sendMsg(_ messageData: Data) {
hook.handleAppMessage(messageData)
} }
} }
#endif #endif

View File

@@ -1,5 +1,7 @@
import UIKit import UIKit
let sync = SyncUpdate(periodic: 7)
class SyncUpdate { class SyncUpdate {
private var lastSync: TimeInterval = 0 private var lastSync: TimeInterval = 0
private var timer: Timer! private var timer: Timer!
@@ -18,8 +20,8 @@ class SyncUpdate {
private(set) var tsLatest: Timestamp? // as set per user, not actual latest private(set) var tsLatest: Timestamp? // as set per user, not actual latest
init(periodic interval: TimeInterval) { fileprivate init(periodic interval: TimeInterval) {
(filterType, tsEarliest, tsLatest) = Pref.DateFilter.restrictions() (filterType, tsEarliest, tsLatest) = Prefs.DateFilter.restrictions()
reloadRangeFromDB() reloadRangeFromDB()
NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self) NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self)
@@ -33,7 +35,7 @@ class SyncUpdate {
/// Callback fired when user changes `DateFilter` on root tableView controller /// Callback fired when user changes `DateFilter` on root tableView controller
@objc private func didChangeDateFilter() { @objc private func didChangeDateFilter() {
self.pause() self.pause()
let filter = Pref.DateFilter.restrictions() let filter = Prefs.DateFilter.restrictions()
filterType = filter.type filterType = filter.type
DispatchQueue.global().async { DispatchQueue.global().async {
// Not necessary, but improve execution order (delete then insert). // Not necessary, but improve execution order (delete then insert).
@@ -109,7 +111,7 @@ class SyncUpdate {
} }
} }
if filterType == .LastXMin { if filterType == .LastXMin {
set(newEarliest: Timestamp.past(minutes: Pref.DateFilter.LastXMin)) set(newEarliest: Timestamp.past(minutes: Prefs.DateFilter.LastXMin))
} }
// TODO: periodic hard delete old logs (will reset rowids!) // TODO: periodic hard delete old logs (will reset rowids!)
} }

View File

@@ -31,12 +31,21 @@ func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> U
/// - Parameters: /// - Parameters:
/// - buttonText: Default: `"Continue"` /// - buttonText: Default: `"Continue"`
/// - buttonStyle: Default: `.default` /// - buttonStyle: Default: `.default`
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController { func AskAlert(title: String?, text: String?, buttonText: String = "Continue", cancelButton: String = "Cancel", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: "Cancel") let alert = Alert(title: title, text: text, buttonText: cancelButton)
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) }) alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
return alert return alert
} }
/// Show alert hinting the user to go to system settings and re-enable notifications.
func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
AskAlert(title: "Notifications Disabled",
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.",
buttonText: "Open settings") { _ in
URL(string: UIApplication.openSettingsURLString)?.open()
}.presentIn(viewController)
}
// MARK: Alert with multiple options // MARK: Alert with multiple options
/// - Parameters: /// - Parameters:

View File

@@ -47,6 +47,15 @@ extension NSLayoutConstraint {
@discardableResult static func |(l: NSLayoutConstraint, r: UILayoutPriority) -> NSLayoutConstraint { l.priority = r; return l } @discardableResult static func |(l: NSLayoutConstraint, r: UILayoutPriority) -> NSLayoutConstraint { l.priority = r; return l }
} }
extension NSLayoutDimension {
/// Create and activate an `equal` constraint with constant value. Format: `A.anchor =&= constant | priority`
@discardableResult static func =&= (l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(equalToConstant: r).on() }
/// Create and activate a `lessThan` constraint with constant value. Format: `A.anchor =<= constant | priority`
@discardableResult static func =<= (l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(lessThanOrEqualToConstant: r).on() }
/// Create and activate a `greaterThan` constraint with constant value. Format: `A.anchor =>= constant | priority`
@discardableResult static func =>= (l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualToConstant: r).on() }
}
/* /*
UIView extension to generate multiple constraints at once UIView extension to generate multiple constraints at once
@@ -73,6 +82,12 @@ extension UIView {
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on() return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on()
} }
} }
/// Sets the priority with which a view resists being made smaller and larger than its intrinsic size.
func constrainHuggingCompression(_ axis: NSLayoutConstraint.Axis, _ priotity: UILayoutPriority) {
setContentHuggingPriority(priotity, for: axis)
setContentCompressionResistancePriority(priotity, for: axis)
}
} }
extension Array where Element: NSLayoutConstraint { extension Array where Element: NSLayoutConstraint {

View File

@@ -0,0 +1,25 @@
import UIKit
// See: https://noahgilmore.com/blog/dark-mode-uicolor-compatibility/
extension UIColor {
/// `.systemBackground ?? .white`
static var sysBackground: UIColor { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }
/// `.link ?? .systemBlue`
static var sysLink: UIColor { if #available(iOS 13.0, *) { return .link } else { return .systemBlue } }
/// `.label ?? .black`
static var sysLabel: UIColor { if #available(iOS 13.0, *) { return .label } else { return .black } }
/// `.secondaryLabel ?? rgba(60, 60, 67, 0.6)`
static var sysLabel2: UIColor { if #available(iOS 13.0, *) { return .secondaryLabel } else { return .init(red: 60/255.0, green: 60/255.0, blue: 67/255.0, alpha: 0.6) } }
/// `.tertiaryLabel ?? rgba(60, 60, 67, 0.3)`
static var sysLabel3: UIColor { if #available(iOS 13.0, *) { return .tertiaryLabel } else { return .init(red: 60/255.0, green: 60/255.0, blue: 67/255.0, alpha: 0.3) } }
}
extension NSMutableAttributedString {
func withColor(_ color: UIColor, fromBack: Int) -> Self {
let l = length - fromBack
let r = (l < 0) ? NSMakeRange(0, length) : NSMakeRange(l, fromBack)
self.addAttribute(.foregroundColor, value: color, range: r)
return self
}
}

View File

@@ -0,0 +1,33 @@
precedencegroup CompareAssignPrecedence {
assignment: true
associativity: left
higherThan: ComparisonPrecedence
}
infix operator <-? : CompareAssignPrecedence
infix operator <-/ : CompareAssignPrecedence
extension Equatable {
/// Assign a new value to `lhs` if `newValue` differs from the previous value. Return `false` if they are equal.
/// - Returns: `true` if `lhs` was overwritten with another value
static func <-?(lhs: inout Self, newValue: Self) -> Bool {
if lhs != newValue {
lhs = newValue
return true
}
return false
}
/// Assign a new value to `lhs` if `newValue` differs from the previous value.
/// Return tuple with both values. Or `nil` if they are equal.
/// - Returns: `nil` if `previousValue == newValue`
static func <-/(lhs: inout Self, newValue: Self) -> (previousValue: Self, newValue: Self)? {
let previousValue = lhs
if previousValue != newValue {
lhs = newValue
return (previousValue, newValue)
}
return nil
}
}

View File

@@ -6,6 +6,7 @@ extension UIFont {
} }
func bold() -> UIFont { withTraits(traits: .traitBold) } func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) } func italic() -> UIFont { withTraits(traits: .traitItalic) }
func boldItalic() -> UIFont { withTraits(traits: [.traitBold, .traitItalic]) }
func monoSpace() -> UIFont { func monoSpace() -> UIFont {
let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:] let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue
@@ -13,39 +14,35 @@ extension UIFont {
} }
} }
extension NSAttributedString { extension NSMutableAttributedString {
static func image(_ img: UIImage) -> Self { convenience init(image: UIImage, centered: Bool = false) {
self.init()
let att = NSTextAttachment() let att = NSTextAttachment()
att.image = img att.image = image
return self.init(attachment: att) append(.init(attachment: att))
if centered {
let ps = NSMutableParagraphStyle()
ps.alignment = .center
addAttribute(.paragraphStyle, value: ps, range: .init(location: 0, length: length))
}
} }
} }
extension NSMutableAttributedString { extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body) @discardableResult func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
@discardableResult func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
@discardableResult func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
@discardableResult func boldItalic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).boldItalic()) }
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) } @discardableResult func h1(_ str: String) -> Self { normal(str, .title1) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) } @discardableResult func h2(_ str: String) -> Self { normal(str, .title2) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) } @discardableResult func h3(_ str: String) -> Self { normal(str, .title3) }
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 { private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [ append(NSAttributedString(string: str, attributes: [
.font : withFont, .font : withFont,
.foregroundColor : UIColor.sysFg .foregroundColor : UIColor.sysLabel
])) ]))
return self return self
} }
func centered(_ content: NSAttributedString) -> Self {
let before = length
append(content)
let ps = NSMutableParagraphStyle()
ps.alignment = .center
addAttribute(.paragraphStyle, value: ps, range: .init(location: before, length: content.length))
return self
}
} }

View File

@@ -1,60 +0,0 @@
import UIKit
struct QLog {
private init() {}
static func m(_ message: String) { write("", message) }
static func Info(_ message: String) { write("[INFO] ", message) }
#if DEBUG
static func Debug(_ message: String) { write("[DEBUG] ", message) }
#else
static func Debug(_ _: String) {}
#endif
static func Error(_ message: String) { write("[ERROR] ", message) }
static func Warning(_ message: String) { write("[WARN] ", message) }
private static func write(_ tag: String, _ message: String) {
print(String(format: "%1.3f %@%@", Date().timeIntervalSince1970, tag, message))
}
}
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)
}
}
precedencegroup CompareAssignPrecedence {
assignment: true
associativity: left
higherThan: ComparisonPrecedence
}
infix operator <-? : CompareAssignPrecedence
infix operator <-/ : CompareAssignPrecedence
extension Equatable {
/// Assign a new value to `lhs` if `newValue` differs from the previous value. Return `false` if they are equal.
/// - Returns: `true` if `lhs` was overwritten with another value
static func <-?(lhs: inout Self, newValue: Self) -> Bool {
if lhs != newValue {
lhs = newValue
return true
}
return false
}
/// Assign a new value to `lhs` if `newValue` differs from the previous value.
/// Return tuple with both values. Or `nil` if they are equal.
/// - Returns: `nil` if `previousValue == newValue`
static func <-/(lhs: inout Self, newValue: Self) -> (previousValue: Self, newValue: Self)? {
let previousValue = lhs
if previousValue != newValue {
lhs = newValue
return (previousValue, newValue)
}
return nil
}
}

View File

@@ -0,0 +1,17 @@
import Foundation
struct QLog {
private init() {}
static func m(_ message: String) { write("", message) }
static func Info(_ message: String) { write("[INFO] ", message) }
#if DEBUG
static func Debug(_ message: String) { write("[DEBUG] ", message) }
#else
static func Debug(_ _: String) {}
#endif
static func Error(_ message: String) { write("[ERROR] ", message) }
static func Warning(_ message: String) { write("[WARN] ", message) }
private static func write(_ tag: String, _ message: String) {
print(String(format: "%1.3f %@%@", Date().timeIntervalSince1970, tag, message))
}
}

View File

@@ -1,6 +1,6 @@
import Foundation import Foundation
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState! let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // nil!
let NotifyDNSFilterChanged = NSNotification.Name("PSIDNSFilterSettingsChanged") // domain: String! let NotifyDNSFilterChanged = NSNotification.Name("PSIDNSFilterSettingsChanged") // domain: String!
let NotifyDateFilterChanged = NSNotification.Name("PSIDateFilterSettingsChanged") // nil! let NotifyDateFilterChanged = NSNotification.Name("PSIDateFilterSettingsChanged") // nil!
let NotifySortOrderChanged = NSNotification.Name("PSIDateFilterSortOrderChanged") // nil! let NotifySortOrderChanged = NSNotification.Name("PSIDateFilterSortOrderChanged") // nil!

View File

@@ -1,84 +0,0 @@
import Foundation
var currentVPNState: VPNState = .off
let sync = SyncUpdate(periodic: 7)
public enum VPNState : Int {
case on = 1, inbetween, off
}
enum Pref {
static func Int(_ key: String) -> Int { UserDefaults.standard.integer(forKey: key) }
static func Int(_ val: Int, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
static func Bool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) }
static func Bool(_ val: Bool, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
static func `Any`(_ key: String) -> Any? { UserDefaults.standard.object(forKey: key) }
static func `Any`(_ val: Any?, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
enum DidShowTutorial {
static var Welcome: Bool {
get { Pref.Bool("didShowTutorialAppWelcome") }
set { Pref.Bool(newValue, "didShowTutorialAppWelcome") }
}
static var Recordings: Bool {
get { Pref.Bool("didShowTutorialRecordings") }
set { Pref.Bool(newValue, "didShowTutorialRecordings") }
}
}
enum ContextAnalyis {
static var CoOccurrenceTime: Int? {
get { Pref.Any("contextAnalyisCoOccurrenceTime") as? Int }
set { Pref.Any(newValue, "contextAnalyisCoOccurrenceTime") }
}
}
enum DateFilter {
static var Kind: DateFilterKind {
get { DateFilterKind(rawValue: Pref.Int("dateFilterType"))! }
set { Pref.Int(newValue.rawValue, "dateFilterType") }
}
/// Default: `0` (disabled)
static var LastXMin: Int {
get { Pref.Int("dateFilterLastXMin") }
set { Pref.Int(newValue, "dateFilterLastXMin") }
}
/// Default: `nil` (disabled)
static var RangeA: Timestamp? {
get { Pref.Any("dateFilterRangeA") as? Timestamp }
set { Pref.Any(newValue, "dateFilterRangeA") }
}
/// Default: `nil` (disabled)
static var RangeB: Timestamp? {
get { Pref.Any("dateFilterRangeB") as? Timestamp }
set { Pref.Any(newValue, "dateFilterRangeB") }
}
/// default: `.Date`
static var OrderBy: DateFilterOrderBy {
get { DateFilterOrderBy(rawValue: Pref.Int("dateFilterOderType"))! }
set { Pref.Int(newValue.rawValue, "dateFilterOderType") }
}
/// default: `false` (Desc)
static var OrderAsc: Bool {
get { Pref.Bool("dateFilterOderAsc") }
set { Pref.Bool(newValue, "dateFilterOderAsc") }
}
/// - Returns: Timestamp restriction depending on current selected date filter.
/// - `Off` : `(nil, nil)`
/// - `LastXMin` : `(now-LastXMin, nil)`
/// - `ABRange` : `(RangeA, RangeB)`
static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) {
let type = Kind
switch type {
case .Off: return (type, nil, nil)
case .LastXMin: return (type, Timestamp.past(minutes: Pref.DateFilter.LastXMin), nil)
case .ABRange: return (type, Pref.DateFilter.RangeA, Pref.DateFilter.RangeB)
}
}
}
}
enum DateFilterKind: Int {
case Off = 0, LastXMin = 1, ABRange = 2;
}
enum DateFilterOrderBy: Int {
case Date = 0, Name = 1, Count = 2;
}

View File

@@ -1,14 +1,5 @@
import UIKit import UIKit
extension NSMutableAttributedString {
func withColor(_ color: UIColor, fromBack: Int) -> Self {
let l = length - fromBack
let r = (l < 0) ? NSMakeRange(0, length) : NSMakeRange(l, fromBack)
self.addAttribute(.foregroundColor, value: color, range: r)
return self
}
}
extension String { extension String {
/// Check if string is equal to `domain` or ends with `.domain` /// Check if string is equal to `domain` or ends with `.domain`
func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) } func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) }
@@ -34,6 +25,13 @@ extension String {
let parts = components(separatedBy: ".") let parts = components(separatedBy: ".")
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
} }
func isValidBundleId() -> Bool {
let regex = try! NSRegularExpression(pattern: #"^[A-Za-z0-9\.\-]{1,155}$"#, options: .anchorsMatchLines)
let range = NSRange(location: 0, length: self.utf16.count)
let matches = regex.matches(in: self, options: .anchored, range: range)
return matches.count == 1
}
} }
private var listOfSLDs: [String : [String : Bool]] = { private var listOfSLDs: [String : [String : Bool]] = {
@@ -49,3 +47,9 @@ private var listOfSLDs: [String : [String : Bool]] = {
} }
return res return res
}() }()
extension NSString {
func substring(from: Int, to: Int) -> String {
substring(with: NSRange(location: from, length: to - from))
}
}

View File

@@ -43,14 +43,6 @@ extension UITableView {
func safeMoveRow(_ from: Int, to: Int) { func safeMoveRow(_ from: Int, to: Int) {
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData() isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
} }
/// Recalculate and apply new `tableHeaderView` height.
func sizeHeaderToFit() {
if let head = tableHeaderView {
head.frame.size.height = head.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
tableHeaderView = head
}
}
} }
@@ -98,3 +90,34 @@ extension EditActionsRemove where Self: UITableViewController {
func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] } func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] }
func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil } func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil }
} }
// MARK: - Table Cell Tap Menu
struct TableCellTapMenu {
private var index: Int = Int.max
mutating func reset() { index = Int.max }
/// Create a new tap manu and shows it immediatelly. With optional buttons.
mutating func start(_ tableView: UITableView, _ indexPath: IndexPath, items: [UIMenuItem]? = nil) -> Bool {
let menu = UIMenuController.shared
if index == indexPath.row {
menu.setMenuVisible(false, animated: true)
reset()
return false
}
index = indexPath.row
let cell = tableView.cellForRow(at: indexPath)!
menu.setTargetRect(cell.bounds, in: cell)
menu.menuItems = items
menu.setMenuVisible(true, animated: true)
return true
}
/// Returns the item if the array index is in bounds.
func getSelected<T>(_ source: [T]) -> T? {
guard index < source.count else { return nil }
return source[index]
}
}

View File

@@ -19,6 +19,12 @@ extension Timestamp {
static func now() -> Timestamp { Date().timestamp } static func now() -> Timestamp { Date().timestamp }
/// Create `Timestamp` with `now() - minutes * 60` /// Create `Timestamp` with `now() - minutes * 60`
static func past(minutes: Int) -> Timestamp { now() - Timestamp(minutes * 60) } static func past(minutes: Int) -> Timestamp { now() - Timestamp(minutes * 60) }
/// Create `Timestamp` with `m * 60` seconds
static func minutes(_ m: Int) -> Timestamp { Timestamp(m * 60) }
/// Create `Timestamp` with `h * 3600` seconds
static func hours(_ h: Int) -> Timestamp { Timestamp(h * 3600) }
/// Create `Timestamp` with `d * 86400` seconds
static func days(_ d: Int) -> Timestamp { Timestamp(d * 86400) }
} }
extension Timer { extension Timer {
@@ -68,23 +74,33 @@ struct TimeFormat {
// MARK: static // MARK: static
/// Time string with format `HH:mm` /// Time string with format `[HH:]mm:ss` (hours prepended only if duration is 1h+)
static func from(_ duration: Timestamp) -> String { static func from(_ duration: Timestamp) -> String {
String(format: "%02d:%02d", duration / 60, duration % 60) let min = duration / 60
let sec = duration % 60
if min >= 60 {
return String(format: "%02d:%02d:%02d", min / 60, min % 60, sec)
} else {
return String(format: "%02d:%02d", min, sec)
}
} }
/// Duration string with format `HH:mm` or `HH:mm.sss` /// Duration string with format `mm:ss` or `mm:ss.SSS`
static func from(_ duration: TimeInterval, millis: Bool = false) -> String { static func from(_ duration: TimeInterval, millis: Bool = false, hours: Bool = false) -> String {
let t = Int(duration) let t = Int(duration)
let min = t / 60
let sec = t % 60
if millis { if millis {
let mil = Int(duration * 1000) % 1000 let mil = Int(duration * 1000) % 1000
return String(format: "%02d:%02d.%03d", t / 60, t % 60, mil) return String(format: "%02d:%02d.%03d", min, sec, mil)
} else if hours {
return String(format: "%02d:%02d:%02d", min / 60, min % 60, sec)
} }
return String(format: "%02d:%02d", t / 60, t % 60) return String(format: "%02d:%02d", min, sec)
} }
/// Duration string with format `HH:mm` or `HH:mm.sss` since reference date /// Duration string with format `mm:ss` or `mm:ss.SSS` or `HH:mm:ss` since reference date
static func since(_ date: Date, millis: Bool = false) -> String { static func since(_ date: Date, millis: Bool = false, hours: Bool = false) -> String {
from(Date().timeIntervalSince(date), millis: millis) from(Date().timeIntervalSince(date), millis: millis, hours: hours)
} }
} }

View File

@@ -1,9 +1,9 @@
import Foundation import Foundation
fileprivate extension FileManager { fileprivate extension FileManager {
// func exportDir() -> URL { func documentDir() -> URL {
// try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
// } }
func appGroupDir() -> URL { func appGroupDir() -> URL {
containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck")! containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck")!
} }
@@ -12,8 +12,47 @@ fileprivate extension FileManager {
} }
} }
extension FileManager {
func sizeOf(path: String) -> Int64? {
try? attributesOfItem(atPath: path)[.size] as? Int64
}
func readableSizeOf(path: String) -> String? {
guard let fSize = sizeOf(path: path) else { return nil }
let bcf = ByteCountFormatter()
bcf.countStyle = .file
return bcf.string(fromByteCount: fSize)
}
}
extension URL { extension URL {
// static func exportDir() -> URL { FileManager.default.exportDir() } static func documentDir() -> URL { FileManager.default.documentDir() }
static func appGroupDir() -> URL { FileManager.default.appGroupDir() } static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
static func internalDB() -> URL { FileManager.default.internalDB() } static func internalDB() -> URL { FileManager.default.internalDB() }
static func make(_ base: String, params: [String : String]) -> URL? {
guard var components = URLComponents(string: base) else {
return nil
}
components.queryItems = params.map {
URLQueryItem(name: $0, value: $1)
}
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
return components.url
}
@discardableResult func download(to file: URL, onSuccess: @escaping () -> Void) -> URLSessionDownloadTask {
let task = URLSession.shared.downloadTask(with: self) { location, response, error in
if let loc = location {
try? FileManager.default.removeItem(at: file)
do {
try FileManager.default.moveItem(at: loc, to: file)
onSuccess()
} catch {
NSLog("[VPN.ERROR] \(error)")
}
}
}
task.resume()
return task
}
} }

View File

@@ -0,0 +1,44 @@
import UIKit
extension UIView {
func asImage(insets: UIEdgeInsets = .zero) -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds.inset(by: insets))
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(bounds.inset(by: insets).size)
let ctx = UIGraphicsGetCurrentContext()!
ctx.translateBy(x: -insets.left, y: -insets.top)
layer.render(in:ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
/// Find size that fits into frame with given `width` as precondition.
/// - Parameter preferredHeight:If unset, find smallest possible size.
func fittingSize(fixedWidth: CGFloat, preferredHeight: CGFloat = 0) -> CGSize {
systemLayoutSizeFitting(CGSize(width: fixedWidth, height: preferredHeight), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
/// Find size that fits into frame with given `height` as precondition.
/// - Parameter preferredWidth:If unset, find smallest possible size.
func fittingSize(fixedHeight: CGFloat, preferredWidth: CGFloat = 0) -> CGSize {
systemLayoutSizeFitting(CGSize(width: preferredWidth, height: fixedHeight), withHorizontalFittingPriority: .fittingSizeLevel, verticalFittingPriority: .required)
}
}
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)
}
}
extension UIStoryboard {
func load<T: UIViewController>(_ identifier: String) -> T {
instantiateViewController(withIdentifier: identifier) as! T
}
}

View File

@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="W5Q-oz-bFb">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<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>
<!--Co Occurrence-->
<scene sceneID="Gbm-AP-b72">
<objects>
<viewController storyboardIdentifier="IBCoOccurrence" id="W5Q-oz-bFb" customClass="VCCoOccurrence" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="f34-NO-d8f">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rvt-nC-2Zr">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<items>
<navigationItem title="Co-Occurrence" id="csY-x8-Rpe">
<barButtonItem key="leftBarButtonItem" systemItem="done" id="eg9-p3-Xas">
<connections>
<action selector="didClose:" destination="W5Q-oz-bFb" id="wyw-vo-6xL"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" image="detail-help" id="RTh-uI-ST6">
<connections>
<action selector="showInfoScreen" destination="W5Q-oz-bFb" id="xoS-z7-PHr"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="PGb-pB-cfO">
<rect key="frame" x="0.0" y="44" width="320" height="524"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<segmentedControl key="tableHeaderView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" id="7ye-tU-pdo">
<rect key="frame" x="0.0" y="0.0" width="320" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="10s"/>
<segment title="30s"/>
</segments>
<connections>
<action selector="didChangeTime:" destination="W5Q-oz-bFb" eventType="valueChanged" id="c5h-JG-S19"/>
</connections>
</segmentedControl>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CoOccurrenceCell" rowHeight="72" id="2qH-Bh-644" customClass="CoOccurrenceCell" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="0.0" y="60" width="320" height="72"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2qH-Bh-644" id="Lwk-Uj-viQ">
<rect key="frame" x="0.0" y="0.0" width="320" height="72"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="99." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qaw-ql-zIB" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="15" y="39.5" width="32" height="21.5"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" secondItem="qaw-ql-zIB" secondAttribute="height" multiplier="3:2" id="VOJ-f5-xhk"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" translatesAutoresizingMaskIntoConstraints="NO" id="zbU-wC-qJG">
<rect key="frame" x="15" y="11" width="290" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalCompressionResistancePriority="500" insetsLayoutMarginsFromSafeArea="NO" text="Count" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JWp-6l-HTJ">
<rect key="frame" x="109.5" y="42.5" width="37" height="16"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<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>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="5900" textAlignment="natural" lineBreakMode="clip" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="q5v-FM-iGo" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="150.5" y="39.5" width="42.5" height="21.5"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="padRight">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="10.35s" textAlignment="natural" lineBreakMode="clip" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zCg-I0-4Tz" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="255.5" y="39.5" width="49.5" height="21.5"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="padRight">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalHuggingPriority="750" horizontalCompressionResistancePriority="400" insetsLayoutMarginsFromSafeArea="NO" text="Diverge" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T4X-cn-msT">
<rect key="frame" x="205" y="42.5" width="46.5" height="16"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<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>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Bb-e5-D3O" customClass="MeterBar" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="190" y="39.5" width="3" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="3" id="wWb-VG-Kqa"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="percent">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="barColor">
<color key="value" systemColor="systemOrangeColor" red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JwY-mq-rYZ" customClass="MeterBar" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="302" y="39.5" width="3" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="3" id="Tta-m5-vwa"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="percent">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="barColor">
<color key="value" systemColor="systemOrangeColor" red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<constraints>
<constraint firstItem="qaw-ql-zIB" firstAttribute="height" secondItem="q5v-FM-iGo" secondAttribute="height" id="2Ug-qN-ido"/>
<constraint firstItem="zbU-wC-qJG" firstAttribute="leading" secondItem="Lwk-Uj-viQ" secondAttribute="leadingMargin" id="2Zo-jC-08y"/>
<constraint firstItem="JwY-mq-rYZ" firstAttribute="top" secondItem="zCg-I0-4Tz" secondAttribute="top" id="3MU-gk-eUU"/>
<constraint firstItem="T4X-cn-msT" firstAttribute="height" secondItem="zCg-I0-4Tz" secondAttribute="height" multiplier="0.75" id="AwE-JC-MFF"/>
<constraint firstAttribute="bottomMargin" secondItem="q5v-FM-iGo" secondAttribute="bottom" id="B2M-MQ-kAw"/>
<constraint firstItem="9Bb-e5-D3O" firstAttribute="bottom" secondItem="q5v-FM-iGo" secondAttribute="bottom" id="Efb-Ud-lxb"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="height" secondItem="q5v-FM-iGo" secondAttribute="height" multiplier="0.75" id="Gfb-up-g1b"/>
<constraint firstItem="JwY-mq-rYZ" firstAttribute="trailing" secondItem="zCg-I0-4Tz" secondAttribute="trailing" id="RlS-DQ-pdh"/>
<constraint firstItem="zCg-I0-4Tz" firstAttribute="leading" secondItem="T4X-cn-msT" secondAttribute="trailing" constant="4" id="VpT-5w-aKh"/>
<constraint firstAttribute="trailingMargin" secondItem="zCg-I0-4Tz" secondAttribute="trailing" id="ai7-PW-ISq"/>
<constraint firstItem="qaw-ql-zIB" firstAttribute="leading" secondItem="Lwk-Uj-viQ" secondAttribute="leadingMargin" id="bGT-hc-lSG"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="height" secondItem="T4X-cn-msT" secondAttribute="height" id="cKO-4d-ikl"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="centerY" secondItem="q5v-FM-iGo" secondAttribute="centerY" id="dZr-0G-1sp"/>
<constraint firstAttribute="trailingMargin" secondItem="zbU-wC-qJG" secondAttribute="trailing" id="e7x-RS-YWo"/>
<constraint firstItem="JwY-mq-rYZ" firstAttribute="bottom" secondItem="zCg-I0-4Tz" secondAttribute="bottom" id="fAV-yh-H1r"/>
<constraint firstItem="9Bb-e5-D3O" firstAttribute="trailing" secondItem="q5v-FM-iGo" secondAttribute="trailing" id="fFF-y3-qOe"/>
<constraint firstItem="qaw-ql-zIB" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="fgw-q8-YRD"/>
<constraint firstItem="9Bb-e5-D3O" firstAttribute="top" secondItem="q5v-FM-iGo" secondAttribute="top" id="idg-nm-vIj"/>
<constraint firstItem="T4X-cn-msT" firstAttribute="leading" secondItem="q5v-FM-iGo" secondAttribute="trailing" constant="12" id="kZj-Tn-BQ3"/>
<constraint firstItem="zbU-wC-qJG" firstAttribute="top" secondItem="Lwk-Uj-viQ" secondAttribute="top" constant="11" id="o7o-M0-sA2"/>
<constraint firstItem="q5v-FM-iGo" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="peW-Pg-5WC"/>
<constraint firstItem="zCg-I0-4Tz" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="ttp-yA-tsi"/>
<constraint firstItem="T4X-cn-msT" firstAttribute="centerY" secondItem="zCg-I0-4Tz" secondAttribute="centerY" id="tz9-Vr-fB6"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="qaw-ql-zIB" secondAttribute="trailing" constant="8" symbolic="YES" id="xFl-RU-Ynw"/>
<constraint firstItem="q5v-FM-iGo" firstAttribute="leading" secondItem="JWp-6l-HTJ" secondAttribute="trailing" constant="4" id="xHw-Pf-daH"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="avgdiff" destination="zCg-I0-4Tz" id="Jno-Yc-ngL"/>
<outlet property="avgdiffMeter" destination="JwY-mq-rYZ" id="QNx-rP-17Z"/>
<outlet property="count" destination="q5v-FM-iGo" id="AFk-93-mhs"/>
<outlet property="countMeter" destination="9Bb-e5-D3O" id="zqt-dT-ecT"/>
<outlet property="rank" destination="qaw-ql-zIB" id="q6Y-JS-NFU"/>
<outlet property="title" destination="zbU-wC-qJG" id="hgV-L0-blX"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="W5Q-oz-bFb" id="7lD-aQ-QhQ"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="PGb-pB-cfO" firstAttribute="top" secondItem="rvt-nC-2Zr" secondAttribute="bottom" id="Edp-lx-Xld"/>
<constraint firstItem="4eZ-5P-8sz" firstAttribute="bottom" secondItem="PGb-pB-cfO" secondAttribute="bottom" id="OAG-HL-4N4"/>
<constraint firstItem="PGb-pB-cfO" firstAttribute="leading" secondItem="4eZ-5P-8sz" secondAttribute="leading" id="V6d-HM-JzJ"/>
<constraint firstItem="4eZ-5P-8sz" firstAttribute="trailing" secondItem="rvt-nC-2Zr" secondAttribute="trailing" id="cmE-iH-06W"/>
<constraint firstItem="rvt-nC-2Zr" firstAttribute="top" secondItem="4eZ-5P-8sz" secondAttribute="top" id="epT-LW-CJV"/>
<constraint firstItem="4eZ-5P-8sz" firstAttribute="trailing" secondItem="PGb-pB-cfO" secondAttribute="trailing" id="j8i-8q-qGS"/>
<constraint firstItem="rvt-nC-2Zr" firstAttribute="leading" secondItem="4eZ-5P-8sz" secondAttribute="leading" id="skN-SN-Wu7"/>
</constraints>
<viewLayoutGuide key="safeArea" id="4eZ-5P-8sz"/>
</view>
<connections>
<outlet property="tableView" destination="PGb-pB-cfO" id="5gT-KC-ce5"/>
<outlet property="timeSegment" destination="7ye-tU-pdo" id="2ys-X4-Jff"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yYY-5U-gct" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="detail-help" width="22" height="22"/>
</resources>
</document>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" 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="16087"/>
<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="xaF-Q9-CPX" kind="relationship" relationship="viewControllers" id="bi1-vo-w1d"/>
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="ude-bF-ump"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" 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="H0N-TG-Ck1" kind="relationship" relationship="rootViewController" id="jDh-HG-7ke"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-700" y="650"/>
</scene>
<!--Requests-->
<scene sceneID="kwb-zk-i3S">
<objects>
<viewControllerPlaceholder storyboardName="Requests" id="H0N-TG-Ck1" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="cLA-5B-TZx"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="rwq-DI-GUl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-700" y="1150"/>
</scene>
<!--Recordings-->
<scene sceneID="V1I-GW-1gY">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="df6-nz-nUq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<navigationController id="xaF-Q9-CPX" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="5ww-wt-MOH"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="ZFR-QK-Ofl">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="thF-Z0-CBq" kind="relationship" relationship="rootViewController" id="BuU-Uy-Zhv"/>
</connections>
</navigationController>
</objects>
<point key="canvasLocation" x="0.0" y="650"/>
</scene>
<!--Recordings-->
<scene sceneID="oLG-Zp-Qm7">
<objects>
<viewControllerPlaceholder storyboardName="Recordings" id="thF-Z0-CBq" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="kMV-TC-eKI"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="B2d-DP-1Yp" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="1150"/>
</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="cV2-If-0fV" kind="relationship" relationship="rootViewController" id="vQq-KE-MOO"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="650"/>
</scene>
<!--Settings-->
<scene sceneID="l0f-fL-3tG">
<objects>
<viewControllerPlaceholder storyboardName="Settings" id="cV2-If-0fV" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="wlO-Ea-6he"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="i3W-Ff-rJL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="1150"/>
</scene>
</scenes>
<resources>
<image name="journal" width="25" height="25"/>
<image name="settings" width="25" height="25"/>
<image name="tag" width="25" height="25"/>
</resources>
</document>

View File

@@ -0,0 +1,742 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="hm5-7q-Zfi">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<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>
<!--Recordings-->
<scene sceneID="ODR-PD-nTU">
<objects>
<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="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Wz5-zb-gwz">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ppJ-js-Wwz">
<rect key="frame" x="0.0" y="0.0" width="320" height="40"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Start new recording" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sYd-b2-Puz">
<rect key="frame" x="20" y="8" width="280" height="24"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="By4-Qr-Zg2">
<rect key="frame" x="278" y="9" width="22" height="22"/>
<state key="normal" image="detail-help"/>
<connections>
<action selector="showInfo:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="AiT-rb-53G"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="sYd-b2-Puz" firstAttribute="leading" secondItem="ppJ-js-Wwz" secondAttribute="leading" constant="20" symbolic="YES" id="35n-12-YFH"/>
<constraint firstAttribute="bottom" secondItem="sYd-b2-Puz" secondAttribute="bottom" priority="999" constant="8" id="4We-J8-dH8"/>
<constraint firstAttribute="trailing" secondItem="sYd-b2-Puz" secondAttribute="trailing" constant="20" symbolic="YES" id="7eW-5L-F4Z"/>
<constraint firstItem="sYd-b2-Puz" firstAttribute="top" secondItem="ppJ-js-Wwz" secondAttribute="top" priority="999" constant="8" id="S3K-8P-agX"/>
<constraint firstItem="By4-Qr-Zg2" firstAttribute="centerY" secondItem="sYd-b2-Puz" secondAttribute="centerY" id="ScC-FQ-oU3"/>
<constraint firstAttribute="trailing" secondItem="By4-Qr-Zg2" secondAttribute="trailing" constant="20" symbolic="YES" id="yLs-7S-Uwz"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="La3-9e-6TK">
<rect key="frame" x="0.0" y="40" width="320" height="55"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" momentary="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2MI-6l-YQt">
<rect key="frame" x="20" y="8" width="280" height="32"/>
<segments>
<segment title="App"/>
<segment title="Background"/>
</segments>
<connections>
<action selector="startRecording:" destination="hm5-7q-Zfi" eventType="valueChanged" id="4p5-9Q-clW"/>
</connections>
</segmentedControl>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="2MI-6l-YQt" firstAttribute="top" secondItem="La3-9e-6TK" secondAttribute="top" priority="999" constant="8" id="TTg-gG-wEP"/>
<constraint firstAttribute="bottom" secondItem="2MI-6l-YQt" secondAttribute="bottom" priority="999" constant="16" id="WPY-VT-xfo"/>
<constraint firstAttribute="trailing" secondItem="2MI-6l-YQt" secondAttribute="trailing" constant="20" id="hhi-TT-2VS"/>
<constraint firstItem="2MI-6l-YQt" firstAttribute="leading" secondItem="La3-9e-6TK" secondAttribute="leading" constant="20" id="p5F-L6-whI"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Yj-FX-eFd">
<rect key="frame" x="0.0" y="95" width="320" height="54.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00.000" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rbR-np-cXD">
<rect key="frame" x="8" y="8" width="200" height="38.5"/>
<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" translatesAutoresizingMaskIntoConstraints="NO" id="vAq-EZ-Gmx">
<rect key="frame" x="212" y="8" width="100" height="38.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<state key="normal" title="Stop">
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="stopRecording:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="CWK-Tg-Tb8"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="rbR-np-cXD" firstAttribute="leading" secondItem="9Yj-FX-eFd" secondAttribute="leading" constant="8" id="610-aO-U6Y"/>
<constraint firstItem="vAq-EZ-Gmx" firstAttribute="top" secondItem="9Yj-FX-eFd" secondAttribute="top" priority="999" constant="8" id="CKZ-8m-kmt"/>
<constraint firstItem="vAq-EZ-Gmx" firstAttribute="leading" secondItem="rbR-np-cXD" secondAttribute="trailing" constant="4" id="Cya-vS-90h"/>
<constraint firstAttribute="trailing" secondItem="vAq-EZ-Gmx" secondAttribute="trailing" constant="8" id="DhM-H2-4I7"/>
<constraint firstItem="rbR-np-cXD" firstAttribute="width" secondItem="vAq-EZ-Gmx" secondAttribute="width" multiplier="2" id="REp-Ug-zuV"/>
<constraint firstAttribute="bottom" secondItem="rbR-np-cXD" secondAttribute="bottom" priority="999" constant="8" id="Ukk-3a-Vsi"/>
<constraint firstAttribute="bottom" secondItem="vAq-EZ-Gmx" secondAttribute="bottom" priority="999" constant="8" id="dV2-g0-7gH"/>
<constraint firstItem="rbR-np-cXD" firstAttribute="top" secondItem="9Yj-FX-eFd" secondAttribute="top" priority="999" constant="8" id="mrE-Ej-dqM"/>
</constraints>
</view>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v3Z-HR-abM">
<rect key="frame" x="0.0" y="149.5" width="320" height="369.5"/>
<connections>
<segue destination="Fln-DD-aId" kind="embed" id="eFH-Rl-aBL"/>
</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>
<navigationItem key="navigationItem" id="ceR-rC-rur"/>
<nil key="simulatedTopBarMetrics"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
<connections>
<outlet property="buttonView" destination="La3-9e-6TK" id="UMg-xx-6OV"/>
<outlet property="headerView" destination="ppJ-js-Wwz" id="68u-8M-R2Q"/>
<outlet property="runningView" destination="9Yj-FX-eFd" id="L2C-YR-2HN"/>
<outlet property="stopButton" destination="vAq-EZ-Gmx" id="XiW-1H-I9y"/>
<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="0.0"/>
</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="369.5"/>
<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="57.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="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="hr0-Xt-5gV">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Xav-Ub-clj">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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="700" y="0.0"/>
</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" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="cLV-Db-JxM">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailCountedCell" 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" minimumScaleFactor="0.80000000000000004" id="rN0-kA-Eln">
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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" style="UICTFontTextStyleBody"/>
<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>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailShortCell" textLabel="rIc-r4-6pg" detailTextLabel="0pW-ZC-wmh" style="IBUITableViewCellStyleValue2" id="hzU-cx-nIs">
<rect key="frame" x="0.0" y="71.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="hzU-cx-nIs" id="scX-pQ-E7z">
<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="right" lineBreakMode="headTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="rIc-r4-6pg">
<rect key="frame" x="16" y="12" width="91" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="0pW-ZC-wmh">
<rect key="frame" x="113" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailLongCell" textLabel="xDy-8J-JFT" detailTextLabel="kgF-BN-FdV" style="IBUITableViewCellStyleSubtitle" id="Q4T-JJ-fqY">
<rect key="frame" x="0.0" y="115" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q4T-JJ-fqY" id="8hy-Rg-b6Q">
<rect key="frame" x="0.0" y="0.0" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="xDy-8J-JFT">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" adjustsFontSizeToFit="NO" id="kgF-BN-FdV">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordNoResultsCell" textLabel="bmQ-Cn-BOm" style="IBUITableViewCellStyleDefault" id="JZ4-vZ-MnG">
<rect key="frame" x="0.0" y="172.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="JZ4-vZ-MnG" id="TWb-p9-EMM">
<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=" no results " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000001192092896" adjustsFontSizeToFit="NO" id="bmQ-Cn-BOm">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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">
<rightBarButtonItems>
<barButtonItem systemItem="action" id="UkE-Wi-JjW">
<connections>
<segue destination="1e2-YP-lHV" kind="push" id="JL4-tN-vZg"/>
</connections>
</barButtonItem>
<barButtonItem image="line-expand" id="xLc-O7-KVB">
<connections>
<action selector="toggleDisplayStyle:" destination="50g-BI-Q6S" id="3wo-9O-7gV"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="0.0"/>
</scene>
<!--Contribute-->
<scene sceneID="np1-8y-nci">
<objects>
<tableViewController id="1e2-YP-lHV" customClass="TVCShareRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Q9f-Bw-9h3">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareTextCell" textLabel="Jpk-wK-OOM" style="IBUITableViewCellStyleDefault" id="9z8-9E-JVQ">
<rect key="frame" x="0.0" y="55.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="9z8-9E-JVQ" id="TtO-r6-0fS">
<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="Description" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.90000000000000002" id="Jpk-wK-OOM">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareCheckboxCell" textLabel="OM7-b6-6E8" style="IBUITableViewCellStyleDefault" id="q9q-X3-TNl">
<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="q9q-X3-TNl" id="Hf9-5j-89Q">
<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="OM7-b6-6E8">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5am-ax-7O0">
<rect key="frame" x="257" y="6" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="didChangeNotesCheckbox:" destination="1e2-YP-lHV" eventType="valueChanged" id="xzg-3J-0IT"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="5am-ax-7O0" id="fw9-Nb-leO"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="shareOpenTextCell" textLabel="842-tZ-Dai" style="IBUITableViewCellStyleDefault" id="Fiz-tT-R1A">
<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="Fiz-tT-R1A" id="ZfU-g2-ohM">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Open Text" lineBreakMode="tailTruncation" numberOfLines="6" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="842-tZ-Dai">
<rect key="frame" x="16" y="0.0" width="269" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="5DP-MB-rOM" kind="push" id="sLA-GQ-Jxx"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareKeyValueCell" textLabel="skk-x9-pZl" detailTextLabel="Z4C-mX-qNQ" style="IBUITableViewCellStyleValue2" id="3xN-XZ-3WD">
<rect key="frame" x="0.0" y="186" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3xN-XZ-3WD" id="V5u-dB-PAq">
<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="right" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="skk-x9-pZl">
<rect key="frame" x="16" y="13" width="91" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Z4C-mX-qNQ">
<rect key="frame" x="113" y="13" width="39.5" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="shareLogCell" textLabel="c0B-OV-ujb" detailTextLabel="sAD-Ns-DV6" style="IBUITableViewCellStyleSubtitle" id="ilN-ct-Db4">
<rect key="frame" x="0.0" y="229.5" width="320" height="58.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ilN-ct-Db4" id="xMK-8l-diB">
<rect key="frame" x="0.0" y="0.0" width="280" height="58.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="c0B-OV-ujb">
<rect key="frame" x="16" y="8" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="4" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" id="sAD-Ns-DV6">
<rect key="frame" x="16" y="31.5" width="35" height="16"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<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="1e2-YP-lHV" id="nSk-Aa-sk1"/>
<outlet property="delegate" destination="1e2-YP-lHV" id="bQg-k9-Jvt"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Contribute" id="rd1-ra-OBF">
<barButtonItem key="rightBarButtonItem" title="Send" id="N7P-2k-lkO">
<connections>
<action selector="shareRecording:" destination="1e2-YP-lHV" id="QrV-1X-bQ3"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="sendButton" destination="N7P-2k-lkO" id="POi-w1-U0C"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cIS-61-X0s" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2100" y="0.0"/>
</scene>
<!--Edit Notes-->
<scene sceneID="1PP-Uc-VkW">
<objects>
<viewController hidesBottomBarWhenPushed="YES" id="5DP-MB-rOM" customClass="VCEditText" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F5N-5G-rm9">
<rect key="frame" x="0.0" y="0.0" width="320" height="325.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" contentInsetAdjustmentBehavior="never" text="Lorem ipsum dolor sit er elit lamet" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="pCU-n1-q6J">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="pCU-n1-q6J" firstAttribute="leading" secondItem="5JR-8n-qUg" secondAttribute="leading" id="7fM-0S-SUi"/>
<constraint firstItem="5JR-8n-qUg" firstAttribute="bottom" secondItem="pCU-n1-q6J" secondAttribute="bottom" id="AG3-zo-ek0"/>
<constraint firstItem="pCU-n1-q6J" firstAttribute="top" secondItem="5JR-8n-qUg" secondAttribute="top" id="IM4-ty-wxG"/>
<constraint firstItem="pCU-n1-q6J" firstAttribute="trailing" secondItem="5JR-8n-qUg" secondAttribute="trailing" id="dMB-pY-HEO"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5JR-8n-qUg"/>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
<navigationItem key="navigationItem" title="Edit Notes" id="Ex8-YV-Ebv"/>
<connections>
<outlet property="textBottom" destination="AG3-zo-ek0" id="EXG-Zb-UGb"/>
<outlet property="textView" destination="pCU-n1-q6J" id="K4D-t0-F9P"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="l5b-JD-MQb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2800" y="0.0"/>
</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="369.5"/>
<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="idg-Q7-qLu"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" enabled="NO" style="done" systemItem="save" id="rWg-hE-Ydl">
<connections>
<action selector="didTapSave" destination="VRk-wv-rhk" id="fPE-i2-I06"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
<connections>
<outletCollection property="gestureRecognizers" destination="B0n-l6-MKc" appends="YES" id="57w-bJ-Vjh"/>
</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="52" width="288" height="307.5"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Guy-Ra-fpS" userLabel="Title">
<rect key="frame" x="0.0" y="0.0" width="288" height="40"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AppCheck" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Et0-8d-CId">
<rect key="frame" x="0.0" y="2" width="80" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="University Bamberg" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l8O-Kw-uc8">
<rect key="frame" x="0.0" y="23.5" width="111" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchIcon.png" translatesAutoresizingMaskIntoConstraints="NO" id="rbW-pK-Kct">
<rect key="frame" x="248" y="0.0" width="40" height="40"/>
<constraints>
<constraint firstAttribute="width" secondItem="rbW-pK-Kct" secondAttribute="height" multiplier="1:1" id="dV9-kR-y39"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<gestureRecognizers/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="40" id="5ew-Cq-VKh"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Et0-8d-CId" secondAttribute="trailing" constant="4" id="EzM-br-BIF"/>
<constraint firstItem="Et0-8d-CId" firstAttribute="leading" secondItem="Guy-Ra-fpS" secondAttribute="leading" id="F1b-aQ-6rA"/>
<constraint firstAttribute="bottom" secondItem="l8O-Kw-uc8" secondAttribute="bottom" priority="999" constant="2" id="Tpw-nU-HHb"/>
<constraint firstItem="l8O-Kw-uc8" firstAttribute="top" secondItem="Et0-8d-CId" secondAttribute="bottom" priority="999" constant="1" id="Wpc-8H-6b8"/>
<constraint firstItem="l8O-Kw-uc8" firstAttribute="leading" secondItem="Guy-Ra-fpS" secondAttribute="leading" id="Xmq-Pl-TrJ"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="centerY" secondItem="Guy-Ra-fpS" secondAttribute="centerY" id="bYB-Jd-Meb"/>
<constraint firstAttribute="trailing" secondItem="rbW-pK-Kct" secondAttribute="trailing" id="bsC-L7-fZn"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="l8O-Kw-uc8" secondAttribute="trailing" constant="4" id="fJ8-TH-hwT"/>
<constraint firstItem="Et0-8d-CId" firstAttribute="top" secondItem="Guy-Ra-fpS" secondAttribute="top" priority="999" constant="2" id="nXu-FP-JVX"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="height" secondItem="Guy-Ra-fpS" secondAttribute="height" id="zLK-Gu-HcF"/>
</constraints>
<connections>
<outletCollection property="gestureRecognizers" destination="Jab-q2-U9X" appends="YES" id="V27-A7-AL5"/>
</connections>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-UG-dwT" userLabel="Notes">
<rect key="frame" x="0.0" y="48" width="288" height="160.5"/>
<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="136.5"/>
<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" style="UICTFontTextStyleSubhead"/>
<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="216.5" 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" 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"/>
<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>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LOr-e7-foG">
<rect key="frame" x="268" y="46.5" width="20" height="22"/>
<state key="normal" image="filter-clear"/>
<connections>
<action selector="didTapFilter" destination="VRk-wv-rhk" eventType="touchUpInside" id="5NH-qa-yMg"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="pql-H5-k6U" secondAttribute="trailing" id="44x-p3-qWK"/>
<constraint firstItem="LOr-e7-foG" firstAttribute="trailing" secondItem="pql-H5-k6U" secondAttribute="trailing" id="5sr-Cf-h0c"/>
<constraint firstItem="pql-H5-k6U" firstAttribute="top" secondItem="FR1-Nt-XuB" secondAttribute="bottom" id="As8-Px-t6G"/>
<constraint firstItem="pql-H5-k6U" firstAttribute="leading" secondItem="QiY-Mm-Dej" secondAttribute="leading" id="ItB-cO-reV"/>
<constraint firstItem="LOr-e7-foG" firstAttribute="centerY" secondItem="pql-H5-k6U" secondAttribute="centerY" id="OLu-MI-3sa"/>
<constraint firstAttribute="height" priority="250" constant="91" id="or7-9o-FZb"/>
<constraint firstAttribute="bottom" secondItem="pql-H5-k6U" secondAttribute="bottom" id="pbd-43-eN0"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="PUH-xO-ZbD"/>
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="xdn-EU-IMx" 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="8" id="Lec-83-aaD"/>
<constraint firstItem="fMa-Lq-tGz" firstAttribute="trailing" secondItem="xdn-EU-IMx" secondAttribute="trailing" constant="16" id="hhC-bL-G3S"/>
<constraint firstItem="fMa-Lq-tGz" firstAttribute="bottom" secondItem="xdn-EU-IMx" 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="appDeveloper" destination="l8O-Kw-uc8" id="dfg-s6-Biz"/>
<outlet property="appIcon" destination="rbW-pK-Kct" id="VlO-fG-y1a"/>
<outlet property="appTitle" destination="Et0-8d-CId" id="HgD-oI-0J8"/>
<outlet property="buttonCancel" destination="TGg-60-wZW" id="5Ej-7t-jaD"/>
<outlet property="buttonSave" destination="rWg-hE-Ydl" id="zfM-kx-erX"/>
<outlet property="chooseAppTap" destination="Jab-q2-U9X" id="Tzv-lm-sUm"/>
<outlet property="inputDetails" destination="pql-H5-k6U" id="NXm-8f-5E6"/>
<outlet property="inputNotes" destination="NXU-yU-eST" id="c2n-cG-aLq"/>
<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="Jab-q2-U9X">
<connections>
<segue destination="qNp-w1-7Md" kind="modal" id="22y-Dy-3xf"/>
</connections>
</tapGestureRecognizer>
<tapGestureRecognizer id="B0n-l6-MKc">
<connections>
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="eMC-Nn-xoE"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="700" y="700"/>
</scene>
<!--App Search-->
<scene sceneID="n6R-Wm-XxF">
<objects>
<tableViewController id="qNp-w1-7Md" customClass="TVCAppSearch" 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="WYZ-wA-6Rh">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<searchBar key="tableHeaderView" contentMode="redraw" preservesSuperviewLayoutMargins="YES" placeholder="Search AppStore" showsCancelButton="YES" id="9dl-ZI-85h">
<rect key="frame" x="0.0" y="0.0" width="320" height="56"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textInputTraits key="textInputTraits" returnKeyType="search" enablesReturnKeyAutomatically="YES"/>
<connections>
<outlet property="delegate" destination="qNp-w1-7Md" id="vcp-BS-7xF"/>
</connections>
</searchBar>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AppStoreSearchCell" textLabel="yIh-WB-0rK" detailTextLabel="PDe-1x-vle" style="IBUITableViewCellStyleSubtitle" id="Q8M-0Q-Mc7">
<rect key="frame" x="0.0" y="84" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q8M-0Q-Mc7" id="BPc-V2-I7v">
<rect key="frame" x="0.0" y="0.0" width="320" height="57.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="yIh-WB-0rK">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="PDe-1x-vle">
<rect key="frame" x="16" y="32.5" width="33" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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="qNp-w1-7Md" id="Jdv-03-iVJ"/>
<outlet property="delegate" destination="qNp-w1-7Md" id="3vD-b3-Ake"/>
</connections>
</tableView>
<connections>
<outlet property="searchBar" destination="9dl-ZI-85h" id="8Zr-E0-mzs"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="PhN-mC-C3W" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="700"/>
</scene>
</scenes>
<resources>
<image name="LaunchIcon.png" width="128" height="128"/>
<image name="detail-help" width="22" height="22"/>
<image name="filter-clear" width="20" height="20"/>
<image name="line-expand" width="20" height="20"/>
</resources>
</document>

View File

@@ -0,0 +1,506 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pdd-aM-sKl">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<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>
<!--Domains-->
<scene sceneID="MN1-aZ-cZt">
<objects>
<tableViewController id="pdd-aM-sKl" customClass="TVCDomains" 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="kj3-8X-TyT">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<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="57.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="293" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="0HB-5f-eB1">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MRe-Eq-gvc">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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="WcC-nb-Vf5" kind="push" id="EVQ-hO-JE9"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="pdd-aM-sKl" id="4fX-iP-7Oa"/>
<outlet property="delegate" destination="pdd-aM-sKl" id="3RN-az-SYU"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Domains" id="nY5-jL-QT9">
<leftBarButtonItems>
<barButtonItem image="filter-clear" id="FZm-Ld-jJE">
<connections>
<action selector="filterButtonTapped:" destination="pdd-aM-sKl" id="Xyy-LF-eCF"/>
</connections>
</barButtonItem>
<barButtonItem enabled="NO" title="7 days" id="wxA-bC-1pN"/>
</leftBarButtonItems>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
<connections>
<outlet property="filterButton" destination="FZm-Ld-jJE" id="g96-Q2-cYX"/>
<outlet property="filterButtonDetail" destination="wxA-bC-1pN" id="CgP-oz-aXa"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="jfx-iA-E0v" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
<!--Hosts-->
<scene sceneID="ZCV-Yx-jjW">
<objects>
<tableViewController storyboardIdentifier="requestsHosts" id="WcC-nb-Vf5" customClass="TVCHosts" 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="nRF-dc-dC2">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<containerView key="tableHeaderView" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kh4-PQ-hy6">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<connections>
<segue destination="1ba-SA-8sT" kind="embed" id="vf1-07-AS4"/>
</connections>
</containerView>
<prototypes>
<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="77" width="320" height="57.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="293" height="57.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">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ovQ-lJ-hWJ">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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="h7Z-Qr-pJ5" kind="push" id="TPa-Zn-eOs"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="WcC-nb-Vf5" id="szM-iI-Jgi"/>
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="0.0"/>
</scene>
<!--Occurrences-->
<scene sceneID="ws3-sK-l8m">
<objects>
<tableViewController storyboardIdentifier="requestsOccurrences" id="h7Z-Qr-pJ5" customClass="TVCHostDetails" 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="4ms-FO-Fge">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<containerView key="tableHeaderView" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SxM-2c-aJb">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<connections>
<segue destination="1ba-SA-8sT" kind="embed" id="ueN-6L-cP7"/>
</connections>
</containerView>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostDetailCell" textLabel="J2P-mU-Vad" detailTextLabel="eWb-mX-udN" style="IBUITableViewCellStyleValue1" id="ZCA-Dz-i92">
<rect key="frame" x="0.0" y="77" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ZCA-Dz-i92" id="nxe-48-jAQ">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="J2P-mU-Vad">
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="eWb-mX-udN">
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="rjy-Di-Cru" kind="push" id="SfC-iY-Ce0"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="h7Z-Qr-pJ5" id="fyW-Av-fWY"/>
<outlet property="delegate" destination="h7Z-Qr-pJ5" id="gBq-jA-u5V"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="0.0"/>
</scene>
<!--Occurrence Context-->
<scene sceneID="A1T-7G-agr">
<objects>
<tableViewController id="rjy-Di-Cru" customClass="TVCOccurrenceContext" 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="EfM-yv-85f">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="OccurrenceContextCell" textLabel="xgq-hW-e3R" detailTextLabel="No8-Bf-ptL" style="IBUITableViewCellStyleValue2" id="KQh-Ei-If8">
<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="KQh-Ei-If8" id="i32-u4-1Q8">
<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="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xgq-hW-e3R">
<rect key="frame" x="16" y="12" width="91" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="No8-Bf-ptL">
<rect key="frame" x="113" y="12" width="59" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="rjy-Di-Cru" id="6CT-Vd-Ixn"/>
<outlet property="delegate" destination="rjy-Di-Cru" id="JtY-um-ZTF"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Occurrence Context" id="2mj-It-uND">
<barButtonItem key="rightBarButtonItem" image="jump-to-target" id="TqX-qO-B3s">
<connections>
<action selector="jumpToTsZero" destination="rjy-Di-Cru" id="RS6-IO-hi4"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cYd-oX-akc" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2100" y="0.0"/>
</scene>
<!--Date Filter-->
<scene sceneID="GqC-c0-bWe">
<objects>
<viewController storyboardIdentifier="domainFilter" modalTransitionStyle="crossDissolve" id="r7v-PM-PrR" customClass="VCDateFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="QBv-5g-BTH">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jAM-LN-evh">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<items>
<navigationItem title="Placeholder" id="s5o-aw-nIo">
<barButtonItem key="leftBarButtonItem" title="Item" image="filter-clear" id="oMW-R3-3Eh"/>
</navigationItem>
</items>
</navigationBar>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pEc-vv-7Ts">
<rect key="frame" x="8" y="64" width="233.5" height="391.5"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="UNT-qn-2cg">
<rect key="frame" x="8" y="8" width="217.5" height="32"/>
<segments>
<segment title="Most recent"/>
<segment title="Date Range"/>
</segments>
<connections>
<action selector="didChangeFilterBy:" destination="r7v-PM-PrR" eventType="valueChanged" id="kM6-QE-ZGV"/>
</connections>
</segmentedControl>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="gEf-Ra-RyA">
<rect key="frame" x="10" y="47" width="213.5" height="334.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show entries no older than" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UBq-oH-pKp">
<rect key="frame" x="0.0" y="0.0" width="213.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ucF-MH-iRP">
<rect key="frame" x="0.0" y="35.5" width="213.5" height="50"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="qhe-6d-hGB">
<rect key="frame" x="-2" y="0.0" width="155.5" height="51"/>
<connections>
<action selector="durationSliderChanged:" destination="r7v-PM-PrR" eventType="valueChanged" id="nQB-w9-dY1"/>
</connections>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="7 days" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ika-su-PZQ">
<rect key="frame" x="159.5" y="16" width="54" height="18"/>
<constraints>
<constraint firstAttribute="width" secondItem="ika-su-PZQ" secondAttribute="height" multiplier="3" id="o9m-cy-kkn"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="qhe-6d-hGB" firstAttribute="leading" secondItem="ucF-MH-iRP" secondAttribute="leading" id="EGU-ln-lJF"/>
<constraint firstItem="ika-su-PZQ" firstAttribute="trailing" secondItem="ucF-MH-iRP" secondAttribute="trailing" id="KLs-Ft-wSo"/>
<constraint firstItem="qhe-6d-hGB" firstAttribute="bottom" secondItem="ucF-MH-iRP" secondAttribute="bottom" id="PTR-Et-Klv"/>
<constraint firstItem="ika-su-PZQ" firstAttribute="leading" secondItem="qhe-6d-hGB" secondAttribute="trailing" constant="8" symbolic="YES" id="aHy-IX-X4B"/>
<constraint firstItem="ika-su-PZQ" firstAttribute="centerY" secondItem="qhe-6d-hGB" secondAttribute="centerY" id="cHs-5z-CHK"/>
<constraint firstItem="qhe-6d-hGB" firstAttribute="top" secondItem="ucF-MH-iRP" secondAttribute="top" id="eJC-d4-zg0"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show entries within range" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rtf-o1-gk6">
<rect key="frame" x="0.0" y="100.5" width="213.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9As-hA-MKt">
<rect key="frame" x="0.0" y="136" width="213.5" height="74"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="From:" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wAd-o2-PHY">
<rect key="frame" x="0.0" y="6.5" width="44" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FVD-kB-91w">
<rect key="frame" x="52" y="0.0" width="161.5" height="33"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<state key="normal" title="1970-01-01 01:00"/>
<connections>
<action selector="didTapRangeButton:" destination="r7v-PM-PrR" eventType="touchUpInside" id="g05-Sc-0P2"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="To:" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fzL-94-c0l">
<rect key="frame" x="0.0" y="47.5" width="44" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IG3-Wc-UI4">
<rect key="frame" x="52" y="41" width="161.5" height="33"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<state key="normal" title="1970-01-01 01:00"/>
<connections>
<action selector="didTapRangeButton:" destination="r7v-PM-PrR" eventType="touchUpInside" id="63v-sy-Bpo"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="FVD-kB-91w" firstAttribute="top" secondItem="9As-hA-MKt" secondAttribute="top" id="J2e-NO-NUD"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="top" secondItem="FVD-kB-91w" secondAttribute="bottom" constant="8" symbolic="YES" id="Jyg-b6-5rH"/>
<constraint firstItem="fzL-94-c0l" firstAttribute="centerY" secondItem="IG3-Wc-UI4" secondAttribute="centerY" id="LAG-jg-nBi"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="trailing" secondItem="fzL-94-c0l" secondAttribute="trailing" id="Lav-7y-PUJ"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="trailing" secondItem="9As-hA-MKt" secondAttribute="trailing" id="XqI-1p-FNv"/>
<constraint firstItem="FVD-kB-91w" firstAttribute="leading" secondItem="wAd-o2-PHY" secondAttribute="trailing" constant="8" symbolic="YES" id="YlE-w7-CZ5"/>
<constraint firstItem="FVD-kB-91w" firstAttribute="trailing" secondItem="9As-hA-MKt" secondAttribute="trailing" id="a6D-1D-HvF"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="leading" secondItem="fzL-94-c0l" secondAttribute="leading" id="bM0-gJ-IW5"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="centerY" secondItem="FVD-kB-91w" secondAttribute="centerY" id="g7F-LP-PQQ"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="bottom" secondItem="9As-hA-MKt" secondAttribute="bottom" priority="750" id="jlK-69-8hl"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="leading" secondItem="fzL-94-c0l" secondAttribute="trailing" constant="8" symbolic="YES" id="pcE-Gv-oj7"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="leading" secondItem="9As-hA-MKt" secondAttribute="leading" id="zgR-pJ-vFs"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Order by" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9Fe-5F-TVt">
<rect key="frame" x="0.0" y="225" width="213.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWy-un-IHC">
<rect key="frame" x="0.0" y="260.5" width="213.5" height="74"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="UKE-MR-kRJ">
<rect key="frame" x="-2" y="0.0" width="217.5" height="36"/>
<segments>
<segment title="Date"/>
<segment title="Name"/>
<segment title="Count"/>
</segments>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="eG2-a4-zm5">
<rect key="frame" x="-2" y="43" width="217.5" height="32"/>
<segments>
<segment title="Ascending"/>
<segment title="Descending"/>
</segments>
</segmentedControl>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="eG2-a4-zm5" firstAttribute="top" secondItem="UKE-MR-kRJ" secondAttribute="bottom" constant="8" symbolic="YES" id="6oC-bZ-XdM"/>
<constraint firstItem="eG2-a4-zm5" firstAttribute="leading" secondItem="cWy-un-IHC" secondAttribute="leading" constant="-2" id="7R0-qB-J0u"/>
<constraint firstAttribute="bottom" secondItem="eG2-a4-zm5" secondAttribute="bottom" id="JbN-vA-Rd5"/>
<constraint firstItem="UKE-MR-kRJ" firstAttribute="top" secondItem="cWy-un-IHC" secondAttribute="top" id="L21-Kf-g2d"/>
<constraint firstAttribute="trailing" secondItem="eG2-a4-zm5" secondAttribute="trailing" constant="-2" id="cbD-H9-e1Q"/>
<constraint firstItem="UKE-MR-kRJ" firstAttribute="leading" secondItem="cWy-un-IHC" secondAttribute="leading" constant="-2" id="lKB-g4-asw"/>
<constraint firstAttribute="trailing" secondItem="UKE-MR-kRJ" secondAttribute="trailing" constant="-2" id="xIa-X2-0Lp"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="UNT-qn-2cg" firstAttribute="top" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="8" id="Awu-uv-9wF"/>
<constraint firstItem="UNT-qn-2cg" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="8" id="Icx-YR-5bc"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="top" secondItem="UNT-qn-2cg" secondAttribute="bottom" constant="8" symbolic="YES" id="QPi-aa-6ff"/>
<constraint firstItem="UNT-qn-2cg" firstAttribute="trailing" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="-8" id="Sof-6L-T2D"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="bottom" constant="-10" id="TMx-5J-z2P"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="10" id="U6l-7M-bm4"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="trailing" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="-10" id="YKE-TR-fTB"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="12"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sAi-8j-0n1" customClass="PopupTriangle" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="14" y="46" width="28" height="22"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="MaD-aD-U8h"/>
<constraint firstAttribute="width" constant="28" id="ntP-rP-UMh"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="color">
<color key="value" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="sAi-8j-0n1" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="4" id="DCq-Ps-sQo"/>
<constraint firstItem="pEc-vv-7Ts" firstAttribute="top" secondItem="jAM-LN-evh" secondAttribute="bottom" constant="20" id="EdA-nv-DEa"/>
<constraint firstItem="pEc-vv-7Ts" firstAttribute="leading" secondItem="u0F-hK-vVD" secondAttribute="leading" constant="8" id="Iow-GV-Lxy"/>
<constraint firstItem="jAM-LN-evh" firstAttribute="trailing" secondItem="u0F-hK-vVD" secondAttribute="trailing" id="Lju-K6-G89"/>
<constraint firstItem="jAM-LN-evh" firstAttribute="top" secondItem="u0F-hK-vVD" secondAttribute="top" id="MqW-YU-POp"/>
<constraint firstItem="u0F-hK-vVD" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="8" id="V9T-2Y-oNy"/>
<constraint firstItem="sAi-8j-0n1" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="6" id="cXH-3c-s6t"/>
<constraint firstItem="jAM-LN-evh" firstAttribute="leading" secondItem="u0F-hK-vVD" secondAttribute="leading" id="ula-eW-vAq"/>
</constraints>
<viewLayoutGuide key="safeArea" id="u0F-hK-vVD"/>
<connections>
<outletCollection property="gestureRecognizers" destination="oRi-17-cVM" appends="YES" id="2pk-5e-eJD"/>
</connections>
</view>
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
<connections>
<outlet property="buttonRangeEnd" destination="IG3-Wc-UI4" id="wAd-ca-bVQ"/>
<outlet property="buttonRangeStart" destination="FVD-kB-91w" id="HbX-Vl-uBE"/>
<outlet property="durationLabel" destination="ika-su-PZQ" id="1Br-vu-xir"/>
<outlet property="durationSlider" destination="qhe-6d-hGB" id="wph-zX-WIz"/>
<outlet property="durationTitle" destination="UBq-oH-pKp" id="BEd-Lo-a2v"/>
<outlet property="durationView" destination="ucF-MH-iRP" id="TCI-Pp-drf"/>
<outlet property="filterBy" destination="UNT-qn-2cg" id="M1J-n8-LHq"/>
<outlet property="orderbyAsc" destination="eG2-a4-zm5" id="II1-hc-pyZ"/>
<outlet property="orderbyType" destination="UKE-MR-kRJ" id="fK7-dW-MLd"/>
<outlet property="rangeTitle" destination="rtf-o1-gk6" id="2DY-xP-VOg"/>
<outlet property="rangeView" destination="9As-hA-MKt" id="0Mq-Gi-nF6"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="xTS-RW-xLN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="oRi-17-cVM">
<connections>
<outlet property="delegate" destination="r7v-PM-PrR" id="JME-7W-w51"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="0.0" y="-700"/>
</scene>
<!--Analysis Bar-->
<scene sceneID="1qq-WD-Lqq">
<objects>
<viewController id="1ba-SA-8sT" customClass="VCAnalysisBar" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qp6-er-N6U">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tabBar contentMode="scaleToFill" translucent="NO" id="1Jy-zg-CXR">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<items>
<tabBarItem title="Co-Occurrence" image="intersection" id="KXh-kQ-rAF"/>
</items>
<connections>
<outlet property="delegate" destination="1ba-SA-8sT" id="bRS-kh-dOv"/>
</connections>
</tabBar>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="dtz-KG-P4C"/>
</view>
<connections>
<outlet property="tabBar" destination="1Jy-zg-CXR" id="VTV-xq-Aou"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XnK-B9-RSJ" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="-700"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="vf1-07-AS4"/>
</inferredMetricsTieBreakers>
<resources>
<image name="filter-clear" width="20" height="20"/>
<image name="intersection" width="25" height="25"/>
<image name="jump-to-target" width="20" height="20"/>
</resources>
</document>

View File

@@ -0,0 +1,837 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qdB-ZO-LHY">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--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" 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="VPN Proxy Settings" id="w58-6X-Jea">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="3y8-eK-09n" style="IBUITableViewCellStyleDefault" id="ghM-ze-fvp">
<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="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="VPN Proxy Enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="3y8-eK-09n">
<rect key="frame" x="16" y="0.0" width="288" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ZAz-WT-FDb">
<rect key="frame" x="257" y="6" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleVPNProxy:" destination="qdB-ZO-LHY" eventType="valueChanged" id="DNS-71-2ga"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="ZAz-WT-FDb" id="SX3-lk-I3M"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<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="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="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">
<rect key="frame" x="16" y="13" width="91" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bHb-Tw-nPR">
<rect key="frame" x="113" y="13" width="73" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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="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="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="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">
<rect key="frame" x="16" y="13" width="91" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="CGG-47-cdc">
<rect key="frame" x="113" y="13" width="73" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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="q3B-Yi-1bx" kind="push" identifier="segueFilterBlocked" id="cOY-j0-75m"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Notification Settings" id="gNL-sO-BEp">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" textLabel="pN1-lL-bGz" detailTextLabel="ldE-NT-c2c" style="IBUITableViewCellStyleValue1" id="jZA-aP-aHG">
<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" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="jZA-aP-aHG" id="OYo-TE-SLp">
<rect key="frame" x="0.0" y="0.0" width="293" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Reminders" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="pN1-lL-bGz">
<rect key="frame" x="16" y="12" width="81.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Disabled" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ldE-NT-c2c">
<rect key="frame" x="218" y="12" width="67" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="JYM-cs-i4H" kind="push" identifier="" id="uOT-Eo-Fm8"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" textLabel="o7c-vQ-haI" detailTextLabel="VeV-go-DXR" style="IBUITableViewCellStyleValue1" id="OTC-Kt-LFT">
<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" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="OTC-Kt-LFT" id="RLb-Oi-WBg">
<rect key="frame" x="0.0" y="0.0" width="293" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Connection Alerts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="o7c-vQ-haI">
<rect key="frame" x="16" y="12" width="137.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Disabled" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VeV-go-DXR">
<rect key="frame" x="218" y="12" width="67" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="D2a-Po-vDU" kind="push" id="6NC-bN-nVR"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Privacy Settings" id="wLR-T2-Qxm">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="8gD-At-D8n" detailTextLabel="Yy4-Ip-Wdv" style="IBUITableViewCellStyleValue1" id="Qyy-0U-yhd">
<rect key="frame" x="0.0" y="443.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="293" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Auto-Delete Logs" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="8gD-At-D8n">
<rect key="frame" x="16" y="12" width="134" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Never" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Yy4-Ip-Wdv">
<rect key="frame" x="239.5" y="12" width="45.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Reset Settings" id="tBs-BI-JqN">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Uii-Jp-53c">
<rect key="frame" x="0.0" y="543.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Uii-Jp-53c" id="4Fp-Ox-yrk">
<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="6B5-l4-Hgz">
<rect key="frame" x="74.5" y="7" width="171" height="30"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<state key="normal" title="Reset Introduction Alerts"/>
<connections>
<action selector="resetTutorialAlerts:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="hw8-as-4PZ"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="6B5-l4-Hgz" firstAttribute="centerY" secondItem="4Fp-Ox-yrk" secondAttribute="centerY" id="h2Y-P2-Feo"/>
<constraint firstItem="6B5-l4-Hgz" firstAttribute="centerX" secondItem="4Fp-Ox-yrk" secondAttribute="centerX" id="jpA-gA-3jY"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Xgc-6Z-IlH">
<rect key="frame" x="0.0" y="587.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Xgc-6Z-IlH" id="efR-vn-6MX">
<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="sE3-Vh-0lM">
<rect key="frame" x="111.5" y="7" width="97" height="30"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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="heU-m1-oJq"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="sE3-Vh-0lM" firstAttribute="centerX" secondItem="efR-vn-6MX" secondAttribute="centerX" id="TvC-jA-Wp5"/>
<constraint firstItem="sE3-Vh-0lM" firstAttribute="centerY" secondItem="efR-vn-6MX" secondAttribute="centerY" id="WoM-cy-cAY"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Advanced Settings" id="Vlg-nm-VB3">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="VnR-9B-1zl">
<rect key="frame" x="0.0" y="687.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VnR-9B-1zl" id="ZTz-vZ-l5p">
<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="twS-Ne-dU0">
<rect key="frame" x="125" y="7" width="70" height="30"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<state key="normal" title="Export DB"/>
<connections>
<action selector="exportDB" destination="qdB-ZO-LHY" eventType="touchUpInside" id="FYN-Zz-UK4"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="twS-Ne-dU0" firstAttribute="centerY" secondItem="ZTz-vZ-l5p" secondAttribute="centerY" id="LgK-8q-r6K"/>
<constraint firstItem="twS-Ne-dU0" firstAttribute="centerX" secondItem="ZTz-vZ-l5p" secondAttribute="centerX" id="ltC-Ba-Bxr"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="qdB-ZO-LHY" id="RH3-xR-dpC"/>
<outlet property="delegate" destination="qdB-ZO-LHY" id="eYf-Xd-2Jq"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Settings" id="9Ce-p2-kGX"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="cellDomainsBlocked" destination="3pw-7c-M6R" id="AHT-FE-z0s"/>
<outlet property="cellDomainsIgnored" destination="fZR-we-Y0k" id="Huy-N3-gz7"/>
<outlet property="cellNotificationConnectionAlert" destination="OTC-Kt-LFT" id="XiG-CC-4lC"/>
<outlet property="cellNotificationReminder" destination="jZA-aP-aHG" id="sjo-2s-rqW"/>
<outlet property="cellPrivacyAutoDelete" destination="Qyy-0U-yhd" id="PzN-iv-kFl"/>
<outlet property="vpnToggle" destination="ZAz-WT-FDb" id="lGX-J8-WrU"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="VNK-Z0-T0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-200" y="0.0"/>
</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">
<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"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="DomainFilterCell" textLabel="MrS-rb-RLB" style="IBUITableViewCellStyleDefault" id="EO2-ww-xuz">
<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="EO2-ww-xuz" id="AtR-ce-uYs">
<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" minimumScaleFactor="0.80000000000000004" id="MrS-rb-RLB">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="q3B-Yi-1bx" id="eWw-VO-n1c"/>
<outlet property="delegate" destination="q3B-Yi-1bx" id="02X-f0-d1a"/>
</connections>
</tableView>
<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="1500" y="0.0"/>
</scene>
<!--Reminders-->
<scene sceneID="fWF-ss-cNz">
<objects>
<tableViewController id="JYM-cs-i4H" customClass="TVCReminderAlerts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Dop-3B-Uvh">
<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="Restart Reminder" id="UOi-fT-8Vh">
<string key="footerTitle">If VPN stops accidentally, show a notification 5 minutes later. It will remind you to re-enable the VPN after system reboot.
if Notification is enabled, show a notification banner once, stating the VPN has stopped.
If App Badge is enabled, display the letter "1" on the homescreen app icon, as long as the VPN is not running.</string>
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="dl8-4J-a0L" style="IBUITableViewCellStyleDefault" id="Z2r-Kz-TCt">
<rect key="frame" x="0.0" y="55.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="Z2r-Kz-TCt" id="rEy-qO-PEN">
<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="Warn If VPN Stops" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="dl8-4J-a0L">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="zaV-mh-eqb">
<rect key="frame" x="252" y="6" width="54" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleAllowRestartReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="F4e-k2-bni"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="zaV-mh-eqb" id="irZ-hk-KoR"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="XC6-mj-vkg" style="IBUITableViewCellStyleDefault" id="5sU-vh-JDf">
<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="5sU-vh-JDf" id="MDI-fb-989">
<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="… with Notification" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XC6-mj-vkg">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="HaE-En-NH3">
<rect key="frame" x="252" y="6" width="54" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleAllowRestartNotify:" destination="JYM-cs-i4H" eventType="valueChanged" id="12C-h5-mrR"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="HaE-En-NH3" id="dld-32-3vc"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="wHg-Wo-szR" style="IBUITableViewCellStyleDefault" id="01e-KG-qDH">
<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="01e-KG-qDH" id="XWV-FF-VxR">
<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="… with App Badge" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wHg-Wo-szR">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="N2Q-cU-pkd">
<rect key="frame" x="252" y="6" width="54" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleAllowRestartBadge:" destination="JYM-cs-i4H" eventType="valueChanged" id="76l-6y-fOu"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="N2Q-cU-pkd" id="6LN-Zw-4Nf"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="UBT-zI-VNd" detailTextLabel="tj7-1l-bts" style="IBUITableViewCellStyleValue1" id="pAS-8r-oS5">
<rect key="frame" x="0.0" y="186" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pAS-8r-oS5" id="iMr-Gb-dhX">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Sound" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="UBT-zI-VNd">
<rect key="frame" x="16" y="12" width="49.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="tj7-1l-bts">
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="I37-dZ-c9Q" kind="push" identifier="segueSoundRestartReminder" id="nBh-15-nBq"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Recording Reminder" footerTitle="Very sporadic reminder. Triggered if your last recording is older than two weeks." id="9Wn-bc-wKX">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="u55-7Z-Q0Q" style="IBUITableViewCellStyleDefault" id="87B-MT-J9s">
<rect key="frame" x="0.0" y="449" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="87B-MT-J9s" id="79D-rZ-spg">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Recording Reminder" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="u55-7Z-Q0Q">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mTm-Rm-1RQ">
<rect key="frame" x="255" y="6" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleAllowRecordingReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="unC-Ur-jPM"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="mTm-Rm-1RQ" id="MRe-3h-xfa"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="VYC-xF-awf" detailTextLabel="Ywb-pT-l1W" style="IBUITableViewCellStyleValue1" id="UkL-k7-bB7">
<rect key="frame" x="0.0" y="492.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="UkL-k7-bB7" id="fZv-NH-YA3">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Sound" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VYC-xF-awf">
<rect key="frame" x="16" y="12" width="49.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="Ywb-pT-l1W">
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="I37-dZ-c9Q" kind="push" identifier="segueSoundRecordingReminder" id="xKB-i8-J9c"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="JYM-cs-i4H" id="1ji-9q-6qB"/>
<outlet property="delegate" destination="JYM-cs-i4H" id="YU7-R1-VjB"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Reminders" id="Z9N-kQ-xhI"/>
<connections>
<outlet property="recordingAllow" destination="mTm-Rm-1RQ" id="tqz-Pk-pSi"/>
<outlet property="recordingSound" destination="UkL-k7-bB7" id="Y9s-fL-cQU"/>
<outlet property="restartAllow" destination="zaV-mh-eqb" id="zcQ-1e-i4H"/>
<outlet property="restartAllowBadge" destination="N2Q-cU-pkd" id="RPU-f6-Dv3"/>
<outlet property="restartAllowNotify" destination="HaE-En-NH3" id="1BN-5h-zNR"/>
<outlet property="restartSound" destination="pAS-8r-oS5" id="9Dy-QR-WXq"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="W7H-LK-3wW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-200" y="768"/>
</scene>
<!--Sound-->
<scene sceneID="3a5-wn-tdm">
<objects>
<tableViewController id="I37-dZ-c9Q" customClass="TVCChooseAlertTone" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="w2R-BE-lM4">
<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"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="SettingsAlertToneCell" textLabel="O50-Db-5TI" style="IBUITableViewCellStyleDefault" id="38V-eP-HSv">
<rect key="frame" x="0.0" y="55.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="38V-eP-HSv" id="hoG-Cy-sHr">
<rect key="frame" x="0.0" y="0.0" width="280" 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="O50-Db-5TI">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<sections/>
<connections>
<outlet property="dataSource" destination="I37-dZ-c9Q" id="aji-Ci-VQs"/>
<outlet property="delegate" destination="I37-dZ-c9Q" id="YUg-WL-kh8"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Sound" id="5hI-rp-d1Z"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="NXc-3P-uoW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1500" y="768"/>
</scene>
<!--Connection Alerts-->
<scene sceneID="JyV-QU-Dw8">
<objects>
<tableViewController id="D2a-Po-vDU" customClass="TVCConnectionAlerts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="u0e-LW-1Qh">
<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="Connection Alerts" footerTitle="Get a notification whenever a specific DNS request occurs." id="4OJ-qA-l8L">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="9g7-rO-sIS" style="IBUITableViewCellStyleDefault" id="8hz-pm-rV5">
<rect key="frame" x="0.0" y="55.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="8hz-pm-rV5" id="fz3-2p-ave">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Enable Alerts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="9g7-rO-sIS">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="who-8G-voz">
<rect key="frame" x="256" y="6" width="48" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleShowNotifications:" destination="D2a-Po-vDU" eventType="valueChanged" id="Thg-6R-7wM"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="who-8G-voz" id="6YE-dG-ThI"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="pck-pT-tnX" detailTextLabel="l8v-5i-Zue" style="IBUITableViewCellStyleValue1" id="laE-pg-nAE">
<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="laE-pg-nAE" id="157-KR-1R5">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Sound" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="pck-pT-tnX">
<rect key="frame" x="16" y="12" width="49.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="l8v-5i-Zue">
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="I37-dZ-c9Q" kind="push" id="tUF-Kv-koO"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Operation Mode" footerTitle="Select whether you'd like to manually include or manually exclude specific domains from notifications." id="9fV-UJ-d1S">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="Lug-Bp-oz0" style="IBUITableViewCellStyleDefault" id="ZMb-xn-r8o">
<rect key="frame" x="0.0" y="234" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ZMb-xn-r8o" id="eT6-OU-8eU">
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Notify only selected lists" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Lug-Bp-oz0">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="FRE-W4-dw2" style="IBUITableViewCellStyleDefault" id="47P-B8-Sul">
<rect key="frame" x="0.0" y="277.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="47P-B8-Sul" id="RbD-bN-NCj">
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Notify all, except selected" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FRE-W4-dw2">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Include" id="Lus-cA-eCF">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="be7-GU-inU" style="IBUITableViewCellStyleDefault" id="2bN-EB-rDk">
<rect key="frame" x="0.0" y="421" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2bN-EB-rDk" id="UxC-Sm-W4n">
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Blocked Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="be7-GU-inU">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="fYg-Mq-C4Q" style="IBUITableViewCellStyleDefault" id="UTd-2r-8c7">
<rect key="frame" x="0.0" y="464.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="UTd-2r-8c7" id="Z6Y-qL-4bw">
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Domains of List A" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="fYg-Mq-C4Q">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="f4F-j9-uiy" style="IBUITableViewCellStyleDefault" id="dk0-Vg-0Zl">
<rect key="frame" x="0.0" y="508" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="dk0-Vg-0Zl" id="69j-ye-ziM">
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Domains of List B" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="f4F-j9-uiy">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="RiJ-Eq-LiA" style="IBUITableViewCellStyleDefault" id="VTi-I6-dXQ">
<rect key="frame" x="0.0" y="551.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="VTi-I6-dXQ" id="PbB-ri-ibd">
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Not in Any List" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="RiJ-Eq-LiA">
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Custom Lists" footerTitle="" id="5bN-ic-93C">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="1fx-L3-esi" detailTextLabel="SAD-ad-RUa" style="IBUITableViewCellStyleValue2" id="fzJ-h6-8Ll">
<rect key="frame" x="0.0" y="658.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fzJ-h6-8Ll" id="u6I-My-k4J">
<rect key="frame" x="0.0" y="0.0" width="293" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="List A" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="1fx-L3-esi">
<rect key="frame" x="16" y="13" width="91" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="SAD-ad-RUa">
<rect key="frame" x="113" y="13" width="73" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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="q3B-Yi-1bx" kind="push" identifier="segueFilterListCustomA" id="2ak-aX-wVl"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="bP5-xR-UWP" detailTextLabel="3b0-Aj-9So" style="IBUITableViewCellStyleValue2" id="uTh-Xw-vp2">
<rect key="frame" x="0.0" y="702.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uTh-Xw-vp2" id="h1L-kH-yQX">
<rect key="frame" x="0.0" y="0.0" width="293" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="List B" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bP5-xR-UWP">
<rect key="frame" x="16" y="13" width="91" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="3b0-Aj-9So">
<rect key="frame" x="113" y="13" width="73" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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="q3B-Yi-1bx" kind="push" identifier="segueFilterListCustomB" id="6wc-d1-VYY"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="D2a-Po-vDU" id="4t8-lb-7wO"/>
<outlet property="delegate" destination="D2a-Po-vDU" id="I1e-X5-6xm"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Connection Alerts" id="5Re-pU-mt2"/>
<connections>
<outlet property="cellSound" destination="laE-pg-nAE" id="Qd5-mf-wox"/>
<outlet property="listsCustomA" destination="fzJ-h6-8Ll" id="77h-42-70y"/>
<outlet property="listsCustomB" destination="uTh-Xw-vp2" id="6OI-pU-Vff"/>
<outlet property="showNotifications" destination="who-8G-voz" id="cUz-Bg-ftS"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="1z3-Sx-YAL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="650" y="384"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="tUF-Kv-koO"/>
<segue reference="6wc-d1-VYY"/>
</inferredMetricsTieBreakers>
</document>

164
main/GlassVPN.swift Normal file
View File

@@ -0,0 +1,164 @@
import NetworkExtension
let GlassVPN = GlassVPNManager()
enum VPNState : Int { case on = 1, inbetween, off }
final class GlassVPNManager {
static let bundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
private var managerVPN: NETunnelProviderManager?
private(set) var state: VPNState = .off
fileprivate init() {
#if IOS_SIMULATOR
postProcessedVPNState(.on)
SimulatorVPN.start()
#else
NETunnelProviderManager.loadAllFromPreferences { managers, error in
self.managerVPN = managers?.first {
($0.protocolConfiguration as? NETunnelProviderProtocol)?
.providerBundleIdentifier == GlassVPNManager.bundleIdentifier
}
guard let mgr = self.managerVPN else {
self.postRawVPNState(.invalid)
return
}
mgr.loadFromPreferences { _ in
self.postRawVPNState(mgr.connection.status)
}
}
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
#endif
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
}
func setEnabled(_ newState: Bool) {
#if IOS_SIMULATOR
postProcessedVPNState(newState ? .on : .off)
newState ? SimulatorVPN.start() : SimulatorVPN.stop()
#else
guard let mgr = self.managerVPN else {
self.createNewVPN { manager in
self.managerVPN = manager
self.setEnabled(newState)
}
return
}
let state = mgr.isEnabled && (mgr.connection.status == .connected)
if state != newState {
self.updateVPN({ mgr.isEnabled = true }) {
newState ? try? mgr.connection.startVPNTunnel() : mgr.connection.stopVPNTunnel()
}
}
#endif
}
/// Notify VPN extension about changes
/// - Returns: `true` on success, `false` if VPN is off or message could not be converted to `.utf8`
@discardableResult func send(_ message: VPNAppMessage) -> Bool {
#if IOS_SIMULATOR
if state == .on, let data = message.raw {
SimulatorVPN.sendMsg(data)
return true
}
#else
if let session = self.managerVPN?.connection as? NETunnelProviderSession,
session.status == .connected, let data = message.raw {
do {
try session.sendProviderMessage(data, responseHandler: nil)
return true
} catch {}
}
#endif
return false
}
// MARK: - Notify callback
@objc private func vpnStatusChanged(_ notification: Notification) {
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid)
}
@objc private func didChangeDomainFilter(_ notification: Notification) {
send(.filterUpdate(domain: notification.object as? String))
}
// MARK: - Manage configuration
private func createNewVPN(_ success: @escaping (_ manager: NETunnelProviderManager) -> Void) {
let mgr = NETunnelProviderManager()
mgr.localizedDescription = "AppCheck Monitor"
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = GlassVPNManager.bundleIdentifier
proto.serverAddress = "127.0.0.1"
mgr.protocolConfiguration = proto
mgr.isEnabled = true
mgr.saveToPreferences { error in
guard error == nil else {
self.postProcessedVPNState(.off)
//ErrorAlert(error!).presentIn(self.window?.rootViewController)
return
}
success(mgr)
}
}
private func updateVPN(_ body: @escaping () -> Void, _ onSuccess: @escaping () -> Void) {
self.managerVPN?.loadFromPreferences { error in
guard error == nil else { return }
body()
self.managerVPN?.saveToPreferences { error in
guard error == nil else { return }
onSuccess()
}
}
}
// MARK: - Post Notifications
private func postRawVPNState(_ origState: NEVPNStatus) {
let state: VPNState
switch origState {
case .connected: state = .on
case .connecting, .disconnecting, .reasserting: state = .inbetween
case .invalid, .disconnected: fallthrough
@unknown default: state = .off
}
postProcessedVPNState(state)
}
private func postProcessedVPNState(_ state: VPNState) {
self.state = state
NotifyVPNStateChanged.post()
}
}
// ---------------------------------------------------------------
// |
// | MARK: - VPN message
// |
// ---------------------------------------------------------------
struct VPNAppMessage {
let raw: Data?
init(_ string: String) { raw = string.data(using: .utf8) }
static func filterUpdate(domain: String? = nil) -> Self {
.init("filter-update:\(domain ?? "")")
}
static func autoDelete(after interval: Int) -> Self {
.init("auto-delete:\(interval)")
}
/// Only used for connection alert notifications
static func notificationSettingsChanged() -> Self {
.init("notify-prefs-change:1")
}
/// Triggered whenever user taps on the start/stop recording button
static func isRecording(_ state: CurrentRecordingState) -> Self {
.init("recording-now:\(state.rawValue)")
}
}

146
main/GlassVPNHook.swift Normal file
View File

@@ -0,0 +1,146 @@
import Foundation
class GlassVPNHook {
private let queue = DispatchQueue.init(label: "PSIGlassDNSQueue", qos: .userInteractive, target: .main)
private var filterDomains: [String]!
private var filterOptions: [(block: Bool, ignore: Bool, customA: Bool, customB: Bool)]!
private var autoDeleteTimer: Timer? = nil
private var cachedNotify: CachedConnectionAlert!
private var currentlyRecording: Bool = false
init() { reset() }
/// Reload from stored settings and rebuilt binary search tree
private func reset() {
reloadDomainFilter()
setAutoDelete(PrefsShared.AutoDeleteLogsDays)
cachedNotify = CachedConnectionAlert()
currentlyRecording = PrefsShared.CurrentlyRecording != .Off
}
/// Invalidate auto-delete timer and release stored properties. You should nullify this instance afterwards.
func cleanUp() {
filterDomains = nil
filterOptions = nil
autoDeleteTimer?.fire() // one last time before we quit
autoDeleteTimer?.invalidate()
cachedNotify = nil
currentlyRecording = false
}
/// Call this method from `PacketTunnelProvider.handleAppMessage(_:completionHandler:)`
func handleAppMessage(_ messageData: Data) {
let message = String(data: messageData, encoding: .utf8)
if let msg = message, let i = msg.firstIndex(of: ":") {
let action = msg.prefix(upTo: i)
let value = msg.suffix(from: msg.index(after: i))
switch action {
case "filter-update":
reloadDomainFilter() // TODO: reload only selected domain?
return
case "auto-delete":
setAutoDelete(Int(value) ?? PrefsShared.AutoDeleteLogsDays)
return
case "notify-prefs-change":
cachedNotify = CachedConnectionAlert()
return
case "recording-now":
let newState = CurrentRecordingState(rawValue: Int(value) ?? 0)
currentlyRecording = newState != .Off
return
default: break
}
}
NSLog("[VPN.WARN] This should never happen! Received unknown handleAppMessage: \(message ?? messageData.base64EncodedString())")
reset() // just in case we fallback to do everything
}
// MARK: - Process DNS Request
/// Log domain request and post notification (if enabled).
/// - Returns: `true` if the request shoud be blocked.
func processDNSRequest(_ domain: String) -> Bool {
let i = filterIndex(for: domain)
let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i]
if ignore, !currentlyRecording {
return block
}
let blockActive = block && !currentlyRecording
queue.async {
do { try AppDB?.logWrite(domain, blocked: blockActive) }
catch { NSLog("[VPN.WARN] Couldn't write: \(error)") }
}
// TODO: disable notifications during recording?
cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB)
// TODO: wait for notify response to block or allow connection
return blockActive
}
/// Build binary tree for reverse DNS lookup
private func reloadDomainFilter() {
let tmp = AppDB?.loadFilters()?.map({
(String($0.reversed()), $1)
}).sorted(by: { $0.0 < $1.0 }) ?? []
let t1 = tmp.map { $0.0 }
let t2 = tmp.map { ($1.contains(.blocked),
$1.contains(.ignored),
$1.contains(.customA),
$1.contains(.customB)) }
filterDomains = t1
filterOptions = t2
}
/// Lookup for reverse DNS binary tree
private func filterIndex(for domain: String) -> Int {
let reverseDomain = String(domain.reversed())
var lo = 0, hi = filterDomains.count - 1
while lo <= hi {
let mid = (lo + hi)/2
if filterDomains[mid] < reverseDomain {
lo = mid + 1
} else if reverseDomain < filterDomains[mid] {
hi = mid - 1
} else {
return mid
}
}
if lo > 0, reverseDomain.hasPrefix(filterDomains[lo - 1] + ".") {
return lo - 1
}
return -1
}
// MARK: - Auto-delete Timer
/// Prepare auto-delete timer with interval between 1 hr - 1 day.
/// - Parameter days: Max age to keep when deleting
private func setAutoDelete(_ days: Int) {
autoDeleteTimer?.invalidate()
guard days > 0 else { return }
// Repeat interval uses days as hours. min 1 hr, max 24 hrs.
let interval = TimeInterval(min(24, days) * 60 * 60)
autoDeleteTimer = Timer.scheduledTimer(timeInterval: interval,
target: self, selector: #selector(autoDeleteNow),
userInfo: days, repeats: true)
autoDeleteTimer!.fire()
}
/// Callback fired when old data should be deleted.
@objc private func autoDeleteNow(_ sender: Timer) {
NSLog("[VPN.INFO] Auto-delete old logs")
queue.async {
do {
try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int)
} catch {
NSLog("[VPN.WARN] Couldn't delete logs, will retry in 5 minutes. \(error)")
if sender.isValid {
sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
import Foundation
import UserNotifications
struct CachedConnectionAlert {
let enabled: Bool
let invertedMode: Bool
let listBlocked, listCustomA, listCustomB, listElse: Bool
let tone: AnyObject?
init() {
enabled = PrefsShared.ConnectionAlerts.Enabled
guard #available(iOS 10.0, *), enabled else {
invertedMode = false
listBlocked = false
listCustomA = false
listCustomB = false
listElse = false
tone = nil
return
}
invertedMode = PrefsShared.ConnectionAlerts.ExcludeMode
listBlocked = PrefsShared.ConnectionAlerts.Lists.Blocked
listCustomA = PrefsShared.ConnectionAlerts.Lists.CustomA
listCustomB = PrefsShared.ConnectionAlerts.Lists.CustomB
listElse = PrefsShared.ConnectionAlerts.Lists.Else
tone = UNNotificationSound.from(string: PrefsShared.ConnectionAlerts.Sound)
}
/// If notifications are enabled and allowed, schedule new notification. Otherwise NOOP.
/// - Parameters:
/// - domain: Domain will be used as unique identifier for noticiation center and in notification message.
/// - blck: Indicator whether `domain` is part of `blocked` list
/// - custA: Indicator whether `domain` is part of custom list `A`
/// - custB: Indicator whether `domain` is part of custom list `B`
func postOrIgnore(_ domain: String, blck: Bool, custA: Bool, custB: Bool) {
if #available(iOS 10.0, *), enabled {
let onAnyList = listBlocked && blck || listCustomA && custA || listCustomB && custB || listElse
if invertedMode ? !onAnyList : onAnyList {
PushNotification.scheduleConnectionAlert(domain, sound: tone as! UNNotificationSound?)
}
}
}
}

View File

@@ -0,0 +1,153 @@
import UserNotifications
struct PushNotification {
enum Identifier: String {
case YouShallRecordMoreReminder
case CantStopMeNowReminder
case RestInPeaceTombstone
case AllConnectionAlertNotifications
}
static func allowed(_ closure: @escaping (NotificationRequestState) -> Void) {
guard #available(iOS 10, *) else { return }
UNUserNotificationCenter.current().getNotificationSettings { settings in
let state = NotificationRequestState(settings.authorizationStatus)
DispatchQueue.main.async {
closure(state)
}
}
}
/// Available in iOS 12+
static func requestProvisionalOrDoNothing(_ closure: @escaping (Bool) -> Void) {
guard #available(iOS 12, *) else { return closure(false) }
let opt: UNAuthorizationOptions = [.alert, .sound, .badge, .provisional, .providesAppNotificationSettings]
UNUserNotificationCenter.current().requestAuthorization(options: opt) { granted, _ in
DispatchQueue.main.async {
closure(granted)
}
}
}
static func requestAuthorization(_ closure: @escaping (Bool) -> Void) {
guard #available(iOS 10, *) else { return }
var opt: UNAuthorizationOptions = [.alert, .sound, .badge]
if #available(iOS 12, *) {
opt.formUnion(.providesAppNotificationSettings)
}
UNUserNotificationCenter.current().requestAuthorization(options: opt) { granted, _ in
DispatchQueue.main.async {
closure(granted)
}
}
}
static func hasPending(_ ident: Identifier, _ closure: @escaping (Bool) -> Void) {
guard #available(iOS 10, *) else { return }
UNUserNotificationCenter.current().getPendingNotificationRequests {
let hasIt = $0.contains { $0.identifier == ident.rawValue }
DispatchQueue.main.async {
closure(hasIt)
}
}
}
static func cancel(_ ident: Identifier, keepDelivered: Bool = false) {
guard #available(iOS 10, *) else { return }
let center = UNUserNotificationCenter.current()
guard ident != .AllConnectionAlertNotifications else {
// remove all connection alert notifications while
// keeping general purpose reminder notifications
center.getDeliveredNotifications {
var list = $0.map { $0.request.identifier }
list.removeAll { !$0.contains(".") } // each domain (or IP) has a dot
center.removeDeliveredNotifications(withIdentifiers: list)
// no need to do the same for pending since con-alerts are always immediate
}
return
}
center.removePendingNotificationRequests(withIdentifiers: [ident.rawValue])
if !keepDelivered {
center.removeDeliveredNotifications(withIdentifiers: [ident.rawValue])
}
}
@available(iOS 10.0, *)
static func schedule(_ ident: Identifier, content: UNNotificationContent, trigger: UNNotificationTrigger? = nil, waitUntilDone: Bool = false) {
schedule(ident.rawValue, content: content, trigger: trigger, waitUntilDone: waitUntilDone)
}
@available(iOS 10.0, *)
static func schedule(_ ident: String, content: UNNotificationContent, trigger: UNNotificationTrigger? = nil, waitUntilDone: Bool = false) {
let req = UNNotificationRequest(identifier: ident, content: content, trigger: trigger)
waitUntilDone ? req.pushAndWait() : req.push()
}
}
// MARK: - Reminder Alerts
extension PushNotification {
/// Auto-check preferences whether `withText` is set, then schedule notification to 5 min in the future.
static func scheduleRestartReminderBanner() {
guard #available(iOS 10, *), PrefsShared.RestartReminder.WithText else { return }
schedule(.CantStopMeNowReminder,
content: .make("AppCheck disabled",
body: "AppCheck can't monitor network traffic because VPN has stopped.",
sound: .from(string: PrefsShared.RestartReminder.Sound)),
trigger: .make(Date(timeIntervalSinceNow: 5 * 60)),
waitUntilDone: true)
}
/// Auto-check preferences whether `withBadge` is set, then post badge immediatelly.
/// - Parameter on: If `true`, set `1` on app icon. If `false`, remove badge on app icon.
static func scheduleRestartReminderBadge(on: Bool) {
guard #available(iOS 10, *), PrefsShared.RestartReminder.WithBadge else { return }
schedule(.RestInPeaceTombstone, content: .makeBadge(on ? 1 : 0), waitUntilDone: true)
}
}
// MARK: - Connection Alerts
extension PushNotification {
static private let queue = ThrottledBatchQueue<String>(0.5, using: .init(label: "PSINotificationQueue", qos: .default, target: .global()))
/// Post new notification with given domain name. If notification already exists, increase occurrence count.
/// - Parameter domain: Used in the description and as notification identifier.
@available(iOS 10.0, *)
static func scheduleConnectionAlert(_ domain: String, sound: UNNotificationSound?) {
queue.addDelayed(domain) { batch in
let groupSum = batch.reduce(into: [:]) { $0[$1] = ($0[$1] ?? 0) + 1 }
scheduleConnectionAlertMulti(groupSum, sound: sound)
}
}
/// Internal method to post a batch of counted domains.
@available(iOS 10.0, *)
static private func scheduleConnectionAlertMulti(_ group: [String: Int], sound: UNNotificationSound?) {
UNUserNotificationCenter.current().getDeliveredNotifications { delivered in
for (dom, count) in group {
let num: Int
if let prev = delivered.first(where: { $0.request.identifier == dom })?.request.content {
if let p = prev.body.split(separator: "×").first, let i = Int(p) {
num = count + i
} else {
num = count + 1
}
} else {
num = count
}
schedule(dom, content: .make("DNS connection", body: num > 1 ? "\(num)× \(dom)" : dom, sound: sound))
}
}
}
}

View File

@@ -0,0 +1,28 @@
import UserNotifications
extension PushNotification {
static func scheduleRecordingReminder(force: Bool) {
if force {
scheduleRecordingReminder()
} else {
hasPending(.YouShallRecordMoreReminder) {
if !$0 { scheduleRecordingReminder() }
}
}
}
private static func scheduleRecordingReminder() {
guard #available(iOS 10, *) else { return }
let now = Timestamp.now()
var next = RecordingsDB.lastTimestamp() ?? (now - 1)
while next < now {
next += .days(14)
}
schedule(.YouShallRecordMoreReminder,
content: .make("Start new recording",
body: "It's been a while since your last recording …",
sound: .from(string: Prefs.RecordingReminder.Sound)),
trigger: .make(Date(next)))
}
}

View File

@@ -0,0 +1,83 @@
import UserNotifications
enum NotificationRequestState {
case NotDetermined, Denied, Authorized, Provisional
@available(iOS 10.0, *)
init(_ from: UNAuthorizationStatus) {
switch from {
case .denied: self = .Denied
case .authorized: self = .Authorized
case .provisional: self = .Provisional
case .notDetermined: fallthrough
@unknown default: self = .NotDetermined
}
}
}
@available(iOS 10.0, *)
extension UNNotificationRequest {
func push() {
UNUserNotificationCenter.current().add(self) { error in
if let e = error {
NSLog("[ERROR] Can't add push notification: \(e)")
}
}
}
func pushAndWait() {
let semaphore = DispatchSemaphore(value: 0)
UNUserNotificationCenter.current().add(self) { error in
if let e = error {
NSLog("[ERROR] Can't add push notification: \(e)")
}
semaphore.signal()
}
_ = semaphore.wait(wallTimeout: .distantFuture)
}
}
@available(iOS 10.0, *)
extension UNNotificationContent {
/// - Parameter sound: Use `#default` or `nil` to play the default tone. Use `#mute` to play no tone at all. Else use an `UNNotificationSoundName`.
static func make(_ title: String, body: String, sound: UNNotificationSound? = .default) -> UNNotificationContent {
let x = UNMutableNotificationContent()
// use NSString.localizedUserNotificationString(forKey:arguments:)
x.title = title
x.body = body
x.sound = sound
return x
}
/// - Parameter value: `0` will remove the badge
static func makeBadge(_ value: Int) -> UNNotificationContent {
let x = UNMutableNotificationContent()
x.badge = value as NSNumber?
return x
}
}
@available(iOS 10.0, *)
extension UNNotificationTrigger {
/// Calls `(dateMatching: components, repeats: repeats)`
static func make(_ components: DateComponents, repeats: Bool) -> UNCalendarNotificationTrigger {
UNCalendarNotificationTrigger(dateMatching: components, repeats: repeats)
}
/// Calls `(dateMatching: components(second-year), repeats: false)`
static func make(_ date: Date) -> UNCalendarNotificationTrigger {
let components = Calendar.current.dateComponents([.year,.month,.day,.hour,.minute,.second], from: date)
return UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
}
/// Calls `(timeInterval: time, repeats: repeats)`
static func make(_ time: TimeInterval, repeats: Bool) -> UNTimeIntervalNotificationTrigger {
UNTimeIntervalNotificationTrigger(timeInterval: time, repeats: repeats)
}
}
@available(iOS 10.0, *)
extension UNNotificationSound {
static func from(string: String) -> UNNotificationSound? {
switch string {
case "#mute": return nil
case "#default": return .default
case let name: return .init(named: UNNotificationSoundName(name + ".caf"))
}
}
}

View File

@@ -0,0 +1,52 @@
import Foundation
import UIKit
extension URL {
static func appStoreSearch(query: String) -> URL {
// https://itunes.apple.com/lookup?bundleId=...
URL.make("https://itunes.apple.com/search", params: [
"media" : "software",
"limit" : "25",
"country" : NSLocale.current.regionCode ?? "DE",
"version" : "2",
"term" : query,
])!
}
}
struct AppStoreSearch {
struct Result {
let bundleId, name: String
let developer, imageURL: String?
}
static func search(_ term: String, _ closure: @escaping ([Result]?, Error?) -> Void) {
URLSession.shared.dataTask(with: .init(url: .appStoreSearch(query: term))) { data, response, error in
guard let data = data, error == nil,
let response = response as? HTTPURLResponse,
(200 ..< 300) ~= response.statusCode else {
closure(nil, error ?? URLError(.badServerResponse))
return
}
closure(jsonSearchToList(data), nil)
}.resume()
}
private static func jsonSearchToList(_ data: Data) -> [Result]? {
guard let json = (try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)) as? [String: Any],
let resAll = json["results"] as? [Any] else {
return nil
}
return resAll.compactMap {
guard let res = $0 as? [String: Any],
let bndl = res["bundleId"] as? String,
let name = res["trackName"] as? String // trackCensoredName
else {
return nil
}
let seller = res["sellerName"] as? String // artistName
let image = res["artworkUrl60"] as? String // artworkUrl100
return Result(bundleId: bndl, name: name, developer: seller, imageURL: image)
}
}
}

View File

@@ -0,0 +1,104 @@
import UIKit
extension CGContext {
func lineFromTo(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) {
self.move(to: CGPoint(x: x1, y: y1))
self.addLine(to: CGPoint(x: x2, y: y2))
}
}
struct BundleIcon {
static let unknown : UIImage? = {
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
let lineWidth: CGFloat = 0.5
let corner: CGFloat = 6.75
let c = corner / CGFloat.pi + lineWidth/2
let sz: CGFloat = rect.height
let m = sz / 2
let r1 = 0.2 * sz, r2 = sqrt(2 * r1 * r1)
// diagonal
context.lineFromTo(x1: c, y1: c, x2: sz-c, y2: sz-c)
context.lineFromTo(x1: c, y1: sz-c, x2: sz-c, y2: c)
// horizontal
context.lineFromTo(x1: 0, y1: m, x2: sz, y2: m)
context.lineFromTo(x1: 0, y1: m + r1, x2: sz, y2: m + r1)
context.lineFromTo(x1: 0, y1: m - r1, x2: sz, y2: m - r1)
// vertical
context.lineFromTo(x1: m, y1: 0, x2: m, y2: sz)
context.lineFromTo(x1: m + r1, y1: 0, x2: m + r1, y2: sz)
context.lineFromTo(x1: m - r1, y1: 0, x2: m - r1, y2: sz)
// circles
context.addEllipse(in: CGRect(x: m - r1, y: m - r1, width: 2*r1, height: 2*r1))
context.addEllipse(in: CGRect(x: m - r2, y: m - r2, width: 2*r2, height: 2*r2))
let r3 = CGRect(x: c, y: c, width: sz - 2*c, height: sz - 2*c)
context.addEllipse(in: r3)
context.addRect(r3)
UIColor.clear.setFill()
UIColor.gray.setStroke()
let rounded = UIBezierPath(roundedRect: rect.insetBy(dx: lineWidth/2, dy: lineWidth/2), cornerRadius: corner)
rounded.lineWidth = lineWidth
rounded.stroke()
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}()
private static let apple : UIImage? = {
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
// #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1).setFill()
// UIBezierPath(roundedRect: rect, cornerRadius: 0).fill()
// print("drawing")
let fs = 36 as CGFloat
let hFont = UIFont.systemFont(ofSize: fs)
var attrib = [
NSAttributedString.Key.font: hFont,
NSAttributedString.Key.foregroundColor: UIColor.gray
]
let str = "" as NSString
let actualHeight = str.size(withAttributes: attrib).height
attrib[NSAttributedString.Key.font] = hFont.withSize(fs * fs / actualHeight)
let strW = str.size(withAttributes: attrib).width
str.draw(at: CGPoint(x: (rect.size.width - strW) / 2.0, y: -3), withAttributes: attrib)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}()
private static let cacheDir = URL.documentDir().appendingPathComponent("app-store-search-cache", isDirectory:true)
private static func local(_ bundleId: String) -> URL {
cacheDir.appendingPathComponent("\(bundleId).img")
}
static func initCache() {
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true, attributes: nil)
}
static func image(_ bundleId: String?, ifNotStored: (() -> Void)? = nil) -> UIImage? {
guard let appId = bundleId else {
return unknown
}
guard let data = try? Data(contentsOf: local(appId)),
let img = UIImage(data: data, scale: 2.0) else {
ifNotStored?()
return appId.hasPrefix("com.apple.") ? apple : unknown
}
return img
}
static func download(_ bundleId: String, url: URL, whenDone: @escaping () -> Void) -> URLSessionDownloadTask {
return url.download(to: local(bundleId), onSuccess: whenDone)
}
}

View File

@@ -0,0 +1,190 @@
import UIKit
protocol TVCAppSearchDelegate {
func appSearch(didSelect bundleId: String, appName: String?, developer: String?)
}
class TVCAppSearch: UITableViewController, UISearchBarDelegate {
private var dataSource: [AppStoreSearch.Result] = []
private var dataSourceLocal: [AppBundleInfo] = []
private var isLoading: Bool = false
private var searchActive: Bool = false
var delegate: TVCAppSearchDelegate?
private var searchNo = 0
private var searchError: Bool = false
private var downloadQueue: [URLSessionDownloadTask] = []
@IBOutlet private var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
BundleIcon.initCache()
dataSourceLocal = AppDB?.appBundleList() ?? []
}
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(closeThis))]
}
@objc private func closeThis() {
searchBar.endEditing(true)
dismiss(animated: true)
}
private func showManualEntryAlert() {
let alert = AskAlert(title: "App Name",
text: "Be as descriptive as possible. Preferably use app bundle id if available. Alternatively use app name or a link to a public repository.",
buttonText: "Set") {
self.delegate?.appSearch(didSelect: "_manually", appName: $0.textFields?.first?.text, developer: nil)
self.closeThis()
}
alert.addTextField { $0.placeholder = "com.apple.notes" }
alert.presentIn(self)
}
// MARK: - Table View Data Source
override func numberOfSections(in _: UITableView) -> Int {
dataSourceLocal.count > 0 ? 2 : 1
}
override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return max(1, dataSource.count) + (searchActive ? 1 : 0)
case 1: return dataSourceLocal.count
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "AppStore"
case 1: return "Found in other recordings"
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppStoreSearchCell")!
let bundleId: String
let altLoadUrl: String?
switch indexPath.section {
case 0:
guard dataSource.count > 0, indexPath.row < dataSource.count else {
if indexPath.row == 0 {
if searchError {
cell.textLabel?.text = "Error loading results"
} else if isLoading {
cell.textLabel?.text = "Loading …"
} else {
cell.textLabel?.text = "No results"
}
cell.isUserInteractionEnabled = false
} else {
cell.textLabel?.text = "Create manually …"
}
cell.detailTextLabel?.text = nil
cell.imageView?.image = nil
return cell
}
let src = dataSource[indexPath.row]
bundleId = src.bundleId
altLoadUrl = src.imageURL
cell.textLabel?.text = src.name
cell.detailTextLabel?.text = src.developer
case 1:
let src = dataSourceLocal[indexPath.row]
bundleId = src.bundleId
altLoadUrl = nil
cell.textLabel?.text = src.name
cell.detailTextLabel?.text = src.author
default:
preconditionFailure()
}
let sno = searchNo
cell.imageView?.image = BundleIcon.image(bundleId) {
guard let u = altLoadUrl, let url = URL(string: u) else { return }
self.downloadQueue.append(BundleIcon.download(bundleId, url: url) {
DispatchQueue.main.async {
// make sure its the same request
guard sno == self.searchNo else { return }
tableView.reloadRows(at: [indexPath], with: .automatic)
}
})
}
cell.isUserInteractionEnabled = true
cell.imageView?.layer.cornerRadius = 6.75
cell.imageView?.layer.masksToBounds = true
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 0:
guard indexPath.row < dataSource.count else {
showManualEntryAlert()
return
}
let src = dataSource[indexPath.row]
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.developer)
case 1:
let src = dataSourceLocal[indexPath.row]
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.author)
default: preconditionFailure()
}
closeThis()
}
// MARK: - Search Bar Delegate
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
isLoading = true
tableView.reloadData()
for x in downloadQueue { x.cancel() }
downloadQueue = []
if searchText.count > 0 {
perform(#selector(performSearch), with: nil, afterDelay: 0.4)
} else {
performSearch()
}
}
/// Internal callback function for delayed text evaluation.
/// This way we can avoid unnecessary searches while user is typing.
@objc private func performSearch() {
func setSource(_ newSource: [AppStoreSearch.Result], _ err: Bool) {
searchNo += 1
searchError = err
dataSource = searchActive ? newSource : []
tableView.reloadData()
}
isLoading = false
let term = searchBar.text?.lowercased() ?? ""
searchActive = term.count > 0
guard searchActive else {
setSource([], false)
return
}
AppStoreSearch.search(term) { source, error in
DispatchQueue.main.async {
setSource(source ?? [], error != nil)
}
}
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.endEditing(true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
closeThis()
}
}

View File

@@ -69,7 +69,14 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove {
let x = dataSource[indexPath.row] let x = dataSource[indexPath.row]
cell.textLabel?.text = x.title ?? x.fallbackTitle cell.textLabel?.text = x.title ?? x.fallbackTitle
cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil
cell.detailTextLabel?.text = "at \(DateFormat.seconds(x.start)), duration: \(x.durationString ?? "?")" cell.detailTextLabel?.text = "\(x.isShared ? "" : "")at \(DateFormat.minutes(x.start)), duration: \(TimeFormat.from(x.duration))"
cell.imageView?.image = x.isLongTerm ? nil : BundleIcon.image(x.appId)
cell.imageView?.layer.cornerRadius = 6.75
cell.imageView?.layer.masksToBounds = true
if #available(iOS 11, *) {} else {
cell.textLabel?.numberOfLines = 1
cell.detailTextLabel?.numberOfLines = 1
}
return cell return cell
} }

View File

@@ -2,23 +2,80 @@ import UIKit
class TVCRecordingDetails: UITableViewController, EditActionsRemove { class TVCRecordingDetails: UITableViewController, EditActionsRemove {
var record: Recording! var record: Recording!
private var dataSource: [RecordLog]! var noResults: Bool = false
private lazy var isLongRecording: Bool = record.isLongTerm
private var showRaw: Bool = false
/// Sorted by `ts` in ascending order (oldest first)
private lazy var dataSourceRaw: [DomainTsPair] = {
let list = RecordingsDB.details(record)
noResults = list.count == 0
return list
}()
/// Sorted by `count` (descending), then alphabetically
private lazy var dataSourceSum: [(domain: String, count: Int)] = {
var result: [String:Int] = [:]
for x in dataSourceRaw {
result[x.domain] = (result[x.domain] ?? 0) + 1 // group and count
}
return result.map{$0}.sorted {
$0.count > $1.count || $0.count == $1.count && $0.domain < $1.domain
}
}()
override func viewDidLoad() { override func viewDidLoad() {
title = record.title ?? record.fallbackTitle title = record.title ?? record.fallbackTitle
dataSource = RecordingsDB.details(record) NotifyRecordingChanged.observe(call: #selector(recordingDidChange(_:)), on: self)
}
@objc private func recordingDidChange(_ notification: Notification) {
let (rec, deleted) = notification.object as! (Recording, Bool)
if rec.id == record.id, !deleted {
record = rec // almost exclusively when 'shared' is set true
}
}
@IBAction private func toggleDisplayStyle(_ sender: UIBarButtonItem) {
showRaw = !showRaw
sender.image = UIImage(named: showRaw ? "line-collapse" : "line-expand")
tableView.reloadData()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let tgt = segue.destination as? TVCShareRecording {
tgt.record = self.record
}
} }
// MARK: - Table View Data Source // MARK: - Table View Data Source
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
max(1, showRaw ? dataSourceRaw.count : dataSourceSum.count)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PreviousRecordDetailCell")! let cell: UITableViewCell
let x = dataSource[indexPath.row] if noResults {
cell.textLabel?.text = x.domain cell = tableView.dequeueReusableCell(withIdentifier: "RecordNoResultsCell")!
cell.detailTextLabel?.text = "\(x.count)" cell.textLabel?.text = " empty recording "
} else if showRaw {
let x = dataSourceRaw[indexPath.row]
if isLongRecording {
cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailLongCell")!
cell.textLabel?.text = x.domain
cell.detailTextLabel?.text = DateFormat.seconds(x.ts)
} else {
cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailShortCell")!
cell.textLabel?.text = "+ " + TimeFormat.from(x.ts - record.start)
cell.detailTextLabel?.text = x.domain
}
} else {
let x = dataSourceSum[indexPath.row]
cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailCountedCell")!
cell.textLabel?.text = x.domain
cell.detailTextLabel?.text = "\(x.count)×"
}
return cell return cell
} }
@@ -26,18 +83,85 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
// MARK: - Editing // MARK: - Editing
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
getRowActionsIOS9(indexPath, tableView) noResults ? nil : getRowActionsIOS9(indexPath, tableView)
} }
@available(iOS 11.0, *) @available(iOS 11.0, *)
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
getRowActionsIOS11(indexPath) noResults ? nil : getRowActionsIOS11(indexPath)
} }
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool { func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
if RecordingsDB.deleteDetails(record, domain: dataSource[index.row].domain) { if showRaw {
dataSource.remove(at: index.row) let x = dataSourceRaw[index.row]
tableView.deleteRows(at: [index], with: .automatic) if RecordingsDB.deleteSingle(record, domain: x.domain, ts: x.ts) {
if let i = dataSourceSum.firstIndex(where: { $0.domain == x.domain }) {
dataSourceSum[i].count -= 1
if dataSourceSum[i].count == 0 {
dataSourceSum.remove(at: i)
}
}
dataSourceRaw.remove(at: index.row)
tableView.deleteRows(at: [index], with: .automatic)
}
} else {
let dom = dataSourceSum[index.row].domain
if RecordingsDB.deleteDetails(record, domain: dom) {
dataSourceRaw.removeAll { $0.domain == dom }
dataSourceSum.remove(at: index.row)
tableView.deleteRows(at: [index], with: .automatic)
}
} }
noResults = dataSourceRaw.count == 0
return true return true
} }
// MARK: - Tap to Copy
private var cellMenu = TableCellTapMenu()
private var copyDomain: String? = nil
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if noResults { return nil }
let buttons = [
UIMenuItem(title: "All requests", action: #selector(openInLogs)),
UIMenuItem(title: "Co-Occurrence", action: #selector(openCoOccurrence))
]
if cellMenu.start(tableView, indexPath, items: buttons) {
if showRaw {
copyDomain = cellMenu.getSelected(dataSourceRaw)?.domain
} else {
copyDomain = cellMenu.getSelected(dataSourceSum)?.domain
}
self.becomeFirstResponder()
}
return nil
}
override var canBecomeFirstResponder: Bool { true }
override func copy(_ sender: Any?) {
if let dom = copyDomain {
UIPasteboard.general.string = dom
}
cellMenu.reset()
copyDomain = nil
}
@objc private func openInLogs() {
if let dom = copyDomain, let req = (tabBarController as? TBCMain)?.openTab(0) as? TVCDomains {
VCDateFilter.disableFilter()
req.pushOpen(domain: dom)
}
cellMenu.reset()
copyDomain = nil
}
@objc private func openCoOccurrence() {
if let dom = copyDomain {
present(VCCoOccurrence.make(dom), animated: true)
}
cellMenu.reset()
copyDomain = nil
}
} }

View File

@@ -0,0 +1,270 @@
import UIKit
class TVCShareRecording : UITableViewController, UITextViewDelegate, VCEditTextDelegate {
@IBOutlet private var sendButton: UIBarButtonItem!
// vars
var record: Recording!
private var shareNotes: Bool = false // opt-in
private lazy var hasNotes: Bool = (self.record.notes != nil)
private lazy var editedNotes: String = self.record.notes ?? ""
private lazy var weekInYear: String = {
let comp = Calendar.current.dateComponents(
[.weekOfYear, .yearForWeekOfYear], from: Date(self.record.start))
return "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)"
}()
// Data source
private lazy var dataSource: [String : [Timestamp]] = RecordingsDB.detailCluster(self.record)
private lazy var dataSourceKeyValue: [(key: String, value: String)] = [
("Date", self.weekInYear),
("Rec-Length", "\(self.record.duration) sec"),
("App-Bundle", self.record.appId ?? " "),
("App-Name", self.record.title ?? " "),
("Notes", " ") // see delegate below
]
private lazy var dataSourceLogs: [(domain: String, occurrences: String, enabled: Bool)] = self.dataSource.map {
($0.key, $0.value.map{"\($0)"}.joined(separator: ", "), true)
}.sorted(by: { $0.domain < $1.domain })
override func viewDidLoad() {
super.viewDidLoad()
if record.isShared {
sendButton.tintColor = .gray
}
}
private func reloadNotes() {
tableView.reloadRows(at: [
IndexPath(row: 0, section: 1), // edit field
IndexPath(row: 4, section: 2) // display field
], with: .automatic)
}
// MARK: - User Interaction
@IBAction private func didChangeNotesCheckbox(_ sender: UISwitch) {
shareNotes = sender.isOn
reloadNotes()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dest = segue.destination as? VCEditText {
dest.text = editedNotes
dest.delegate = self
}
}
func editText(didFinish text: String) {
editedNotes = text
reloadNotes()
}
@IBAction private func shareRecording(_ sender: UIBarButtonItem) {
guard !record.isShared else {
showAlertAlreadyShared()
return
}
navigationItem.rightBarButtonItem = {
let v = UIView()
let activity = UIActivityIndicatorView()
v.addSubview(activity)
activity.anchor([.centerX, .centerY], to: v)
activity.startAnimating()
v.widthAnchor =&= 2 * activity.widthAnchor
return UIBarButtonItem(customView: v)
}()
postToServer() { [weak self] in
self?.navigationItem.rightBarButtonItem = self?.sendButton
}
}
// MARK: - Table Data Source
override func numberOfSections(in _: UITableView) -> Int { 4 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return 1 // description
case 1: return hasNotes ? 2 : 0 // notes + checkbox
case 2: return dataSourceKeyValue.count
case 3: return dataSourceLogs.count
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "Review before sending"
case 1: return hasNotes ? "Notes" : nil
case 2: return "Send to server"
case 3: return "Logs"
default: return nil
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
switch section {
case 0: return "You can tap on a domain cell to exclude it from the upload."
case 2: return "Below you see the domain names, followed by a list of relative time offsets (in seconds)."
default: return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
switch indexPath.section {
case 0:
cell = tableView.dequeueReusableCell(withIdentifier: "shareTextCell")!
cell.textLabel?.text = """
You are about to upload the following information to our servers.
The data is anonymized in regards to device identifiers and time of recording. However, it is not anonymous to the domains requested during the recording.
"""
case 1:
switch indexPath.row {
case 0:
cell = tableView.dequeueReusableCell(withIdentifier: "shareOpenTextCell")!
cell.textLabel?.text = editedNotes
cell.textLabel?.textColor = shareNotes ? nil : .gray
case 1:
cell = tableView.dequeueReusableCell(withIdentifier: "shareCheckboxCell")!
cell.textLabel?.text = "Upload your notes?"
let accessory = cell.accessoryView as! UISwitch
accessory.isOn = shareNotes
default: preconditionFailure()
}
case 2:
cell = tableView.dequeueReusableCell(withIdentifier: "shareKeyValueCell")!
let src = dataSourceKeyValue[indexPath.row]
cell.textLabel?.text = src.key
let flag = shareNotes && indexPath.row == 4
cell.detailTextLabel?.text = flag ? editedNotes : src.value
case 3:
cell = tableView.dequeueReusableCell(withIdentifier: "shareLogCell")!
let src = dataSourceLogs[indexPath.row]
let sent = src.enabled
cell.textLabel?.text = src.domain
cell.detailTextLabel?.text = sent ? src.occurrences : "don't upload"
cell.accessoryType = sent ? .checkmark : .none
cell.textLabel?.isEnabled = sent
cell.detailTextLabel?.isEnabled = sent
default:
preconditionFailure()
}
if #available(iOS 11, *) {} else {
cell.detailTextLabel?.numberOfLines = 1
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard indexPath.section == 3 else { return }
dataSourceLogs[indexPath.row].enabled = !dataSourceLogs[indexPath.row].enabled
tableView.deselectRow(at: indexPath, animated: true)
tableView.reloadRows(at: [indexPath], with: .automatic)
}
// MARK: - Upload
private func postToServer(_ onceLoaded: @escaping () -> Void) {
// prepare json
let allowed = dataSourceLogs.filter{ $0.enabled }.map{ $0.domain }
let json = try? JSONSerialization.data(withJSONObject: [
"v" : 1,
"date" : weekInYear,
"duration" : record.duration,
"app-bundle" : record.appId ?? "",
"app-name" : record.title ?? "",
"notes" : shareNotes ? editedNotes : "",
"logs" : dataSource.filter{ allowed.contains($0.key) }
])
// prepare post request
let url = URL(string: "https://appchk.de/api/v1/contribute/")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = json
var rec = record! // store temporarily so self can be released
// send to server
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async { [weak self] in
onceLoaded()
guard error == nil, let data = data,
let response = response as? HTTPURLResponse else {
self?.banner(.fail, "\(error?.localizedDescription ?? "Unkown error occurred")")
return
}
guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any],
let v = json["v"] as? Int, v > 0 else {
QLog.Warning("Couldn't contribute: Not JSON or no version key")
self?.banner(.fail, "Server couldn't parse request.\nTry again later.")
return
}
let status = json["status"] as? String ?? "unkown reason"
guard status == "ok", (200 ... 299) ~= response.statusCode else {
QLog.Warning("Couldn't contribute: \(status)")
self?.banner(.fail, "Error: \(status)")
return
}
// update db, mark record as shared
rec.uploadkey = json["key"] as? String ?? "_"
self?.record = rec // in case view is still open
RecordingsDB.update(rec) // rec cause self may not be available
self?.sendButton.tintColor = .gray
// notify user about results
if v == 1, let urlStr = json["url"] as? String {
let nextUpdateIn = json["when"] as? Int
self?.showAlertAvailableSoon(urlStr, when: nextUpdateIn)
}
self?.banner(.ok, "Thank you for your contribution.")
}
}.resume()
}
// MARK: - Alerts & Banner
private func banner(_ style: NotificationBanner.Style, _ msg: String) {
NotificationBanner(msg, style: style).present(in: self)
}
private func showAlertAvailableSoon(_ urlStr: String, when: Int?) {
var msg = "Your contribution is being processed and will be available "
if let when = when {
if when < 61 {
msg += "in approx. \(when) sec. "
} else {
let fmt = TimeFormat.from(Timestamp(when))
msg += "in \(fmt) min. "
}
} else {
msg += "shortly. "
}
msg += "Open results webpage now?"
AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in
if let url = URL(string: urlStr) {
UIApplication.shared.openURL(url)
}
}.presentIn(self)
}
private func showAlertAlreadyShared() {
let alert = Alert(title: nil, text: "You already shared this recording.")
if let bid = record.appId, bid.isValidBundleId() {
alert.addAction(UIAlertAction.init(title: "Open results", style: .default, handler: { _ in
URL(string: "https://appchk.de/redirect.html?id=\(bid)")?.open()
}))
}
alert.presentIn(self)
}
}

View File

@@ -1,57 +1,92 @@
import UIKit import UIKit
class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate { class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate, TVCAppSearchDelegate {
var record: Recording! var record: Recording!
var deleteOnCancel: Bool = false var deleteOnCancel: Bool = false
var appId: String?
@IBOutlet private var buttonCancel: UIBarButtonItem! @IBOutlet private var buttonCancel: UIBarButtonItem!
@IBOutlet private var buttonSave: UIBarButtonItem! @IBOutlet private var buttonSave: UIBarButtonItem!
@IBOutlet private var inputTitle: UITextField! @IBOutlet private var appTitle: UILabel!
@IBOutlet private var appDeveloper: UILabel!
@IBOutlet private var appIcon: UIImageView!
@IBOutlet private var inputNotes: UITextView! @IBOutlet private var inputNotes: UITextView!
@IBOutlet private var inputDetails: UITextView! @IBOutlet private var inputDetails: UITextView!
@IBOutlet private var noteBottom: NSLayoutConstraint! @IBOutlet private var noteBottom: NSLayoutConstraint!
@IBOutlet private var chooseAppTap: UITapGestureRecognizer!
override func viewDidLoad() { override func viewDidLoad() {
inputTitle.placeholder = record.fallbackTitle if deleteOnCancel { // aka newly created
inputTitle.text = record.title RecordingsDB.persist(record)
inputNotes.text = record.notes if Prefs.RecordingReminder.Enabled {
inputDetails.text = """ PushNotification.scheduleRecordingReminder(force: true)
Start: \(DateFormat.seconds(record.start)) }
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!)) // mark as destructive
Duration: \(record.durationString ?? "?")
"""
validateSaveButton()
if deleteOnCancel { // mark as destructive
buttonCancel.tintColor = .systemRed buttonCancel.tintColor = .systemRed
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
isModalInPresentation = true isModalInPresentation = true
} }
} }
if record.isLongTerm {
appId = nil
appIcon.image = nil
appTitle.text = record.fallbackTitle
appDeveloper.text = nil
chooseAppTap.isEnabled = false
} else {
appId = record.appId
appIcon.image = BundleIcon.image(record.appId)
appIcon.layer.cornerRadius = 6.75
appIcon.layer.masksToBounds = true
if record.appId == nil {
appTitle.text = "Tap here to choose app"
appDeveloper.text = record.title
} else {
appTitle.text = record.title ?? record.fallbackTitle
appDeveloper.text = record.subtitle
}
}
inputNotes.text = record.notes
inputDetails.text = """
Start: \(DateFormat.seconds(record.start))
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!))
Duration: \(TimeFormat.from(record.duration))
"""
validateSaveButton()
UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self) UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self)
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self) UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// MARK: Save & Cancel Buttons if let tvc = segue.destination as? TVCAppSearch {
tvc.delegate = self
@IBAction func didTapSave(_ sender: UIBarButtonItem) {
let newlyCreated = deleteOnCancel
if newlyCreated {
// 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) {
RecordingsDB.update(self.record)
if newlyCreated {
RecordingsDB.persist(self.record)
}
} }
} }
@IBAction func didTapCancel(_ sender: UIBarButtonItem) { // MARK: Save & Cancel Buttons
@IBAction func didTapSave() {
// if remains true, `viewDidDisappear` will delete the record
deleteOnCancel = false
QLog.Debug("updating record #\(record.id)")
if let id = appId, id != "" {
record.appId = id
record.title = (appTitle.text == "") ? nil : appTitle.text
record.subtitle = (appDeveloper.text == "") ? nil : appDeveloper.text
} else {
record.appId = nil
record.title = nil
record.subtitle = nil
}
record.notes = (inputNotes.text == "") ? nil : inputNotes.text
dismiss(animated: true) {
RecordingsDB.update(self.record)
}
}
@IBAction func didTapCancel() {
QLog.Debug("discard edit of record #\(record.id)") QLog.Debug("discard edit of record #\(record.id)")
dismiss(animated: true) dismiss(animated: true)
} }
@@ -64,6 +99,16 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
} }
} }
@IBAction func didTapFilter() {
if buttonSave.isEnabled {
NotificationBanner("Filter set", style: .ok).present(in: self, hideAfter: 1)
} else {
(presentingViewController as? TBCMain)?.openTab(0)
didTapCancel()
}
VCDateFilter.setFilter(range: record.start, to: record.stop)
}
// MARK: Handle Keyboard & Notes Frame // MARK: Handle Keyboard & Notes Frame
@@ -118,11 +163,18 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
func textViewDidChange(_ _: UITextView) { validateSaveButton() } func textViewDidChange(_ _: UITextView) { validateSaveButton() }
private func validateSaveButton() { private func validateSaveButton() {
let changed = (inputTitle.text != record.title ?? "" || inputNotes.text != record.notes ?? "") let changed = (appId != record.appId
|| (appTitle.text != record.title && appTitle.text != "Tap here to choose app" && appTitle.text != record.fallbackTitle)
|| appDeveloper.text != record.subtitle
|| inputNotes.text != record.notes ?? "")
buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings
} }
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func appSearch(didSelect bundleId: String, appName: String?, developer: String?) {
textField == inputTitle ? inputNotes.becomeFirstResponder() : true appId = bundleId
appTitle.text = appName
appDeveloper.text = developer
appIcon.image = BundleIcon.image(bundleId)
validateSaveButton()
} }
} }

View File

@@ -0,0 +1,39 @@
import UIKit
protocol VCEditTextDelegate {
func editText(didFinish text: String)
}
class VCEditText: UIViewController, UITextViewDelegate {
var text: String!
var delegate: VCEditTextDelegate!
@IBOutlet private var textView: UITextView!
@IBOutlet private var textBottom: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
textView.text = text
textView.becomeFirstResponder()
UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self)
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
delegate.editText(didFinish: textView.text)
}
// MARK: - Adapt to Keyboard
@objc func keyboardWillShow(_ notification: NSNotification) {
textBottom.constant = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0
}
@objc func keyboardWillHide(_ notification: NSNotification) {
textBottom.constant = 0
}
}

View File

@@ -3,138 +3,142 @@ import UIKit
class VCRecordings: UIViewController, UINavigationControllerDelegate { class VCRecordings: UIViewController, UINavigationControllerDelegate {
private var currentRecording: Recording? private var currentRecording: Recording?
private var recordingTimer: Timer? private var recordingTimer: Timer?
private var state: CurrentRecordingState = .Off
@IBOutlet private var headerView: UIView!
@IBOutlet private var buttonView: UIView!
@IBOutlet private var runningView: UIView!
@IBOutlet private var timeLabel: UILabel! @IBOutlet private var timeLabel: UILabel!
@IBOutlet private var startButton: UIButton! @IBOutlet private var stopButton: UIButton!
@IBOutlet private var startNewRecView: UIView!
private var prevRecController: UINavigationController!
override func viewDidLoad() { override func viewDidLoad() {
prevRecController = (children.first as! UINavigationController)
prevRecController.delegate = self
timeLabel.font = timeLabel.font.monoSpace() timeLabel.font = timeLabel.font.monoSpace()
// hide timer if not running if let ongoing = RecordingsDB.getCurrent() {
updateUI(setRecording: false, animated: false) currentRecording = ongoing
currentRecording = RecordingsDB.getCurrent() // Currently this class is the only one that changes the state,
// if that ever changes, make sure to update local state as well
if !Pref.DidShowTutorial.Recordings { state = PrefsShared.CurrentlyRecording
self.perform(#selector(showTutorial), with: nil, afterDelay: 0.5) startTimer(animate: false, longterm: state == .Background)
} else { // hide timer if not running
updateUI(setRecording: false, animated: false)
}
if !Prefs.DidShowTutorial.Recordings {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let x = TutorialSheet()
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-1"))
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-2"))
x.buttonTitleDone = "Got it"
x.present {
Prefs.DidShowTutorial.Recordings = true
}
}
} }
} }
override func viewDidAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
if currentRecording != nil { startTimer(animate: false) } super.viewWillAppear(animated)
recordingTimer?.fireDate = .distantPast
navigationController?.setNavigationBarHidden(true, animated: animated)
// set hidden in will appear causes UITableViewAlertForLayoutOutsideViewHierarchy
// but otherwise navBar is visible during transition
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
stopTimer(animate: false) super.viewWillDisappear(animated)
recordingTimer?.fireDate = .distantFuture
navigationController?.setNavigationBarHidden(false, animated: animated)
} }
func navigationController(_ nav: UINavigationController, willShow vc: UIViewController, animated: Bool) { @IBAction private func showInfo(_ sender: UIButton?) {
hideNewRecording(isRootVC: (vc == nav.viewControllers.first), didShow: false) let x = TutorialSheet()
} x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-howto"))
x.buttonTitleDone = "Close"
func navigationController(_ nav: UINavigationController, didShow vc: UIViewController, animated: Bool) { x.present() {
// TODO: use interactive animation handler to dynamically animate "new recording" view Prefs.DidShowTutorial.RecordingHowTo = true
hideNewRecording(isRootVC: (vc == nav.viewControllers.first), didShow: true)
}
private func hideNewRecording(isRootVC: Bool, didShow: Bool) {
if isRootVC == didShow {
UIView.animate(withDuration: 0.3) {
self.startNewRecView.isHidden = !isRootVC // hide "new recording" if details open
}
} }
} }
// MARK: Start New Recording // MARK: Start New Recording
@IBAction private func startRecordingButtonTapped(_ sender: UIButton) { @IBAction private func startRecording(_ sender: UISegmentedControl) {
if recordingTimer == nil { guard GlassVPN.state == .on else {
currentRecording = RecordingsDB.startNew() AskAlert(title: "VPN stopped",
startTimer(animate: true) text: "You need to start the VPN proxy before you can start a recording.",
} else { buttonText: "Start") { _ in
stopTimer(animate: true) GlassVPN.setEnabled(true)
RecordingsDB.stop(&currentRecording!) }.presentIn(self)
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 return
} }
recordingTimer = Timer.repeating(0.086, call: #selector(timerCallback(_:)), on: self, userInfo: Date(r.start)) guard Prefs.DidShowTutorial.RecordingHowTo else {
updateUI(setRecording: true, animated: animate) showInfo(nil) // show at least once. Later, user can click the help icon.
return
}
currentRecording = RecordingsDB.startNew()
QLog.Debug("start recording #\(currentRecording!.id)")
let longterm = sender.selectedSegmentIndex == 1
startTimer(animate: true, longterm: longterm)
notifyVPN(setRecording: longterm ? .Background : .App)
} }
@objc private func timerCallback(_ sender: Timer) { @IBAction private func stopRecording(_ sender: UIButton) {
timeLabel.text = TimeFormat.since(sender.userInfo as! Date, millis: true) let validRecording = (state == .Background) == currentRecording!.isLongTerm
notifyVPN(setRecording: .Off) // will change state = .Off
stopTimer()
QLog.Debug("stop recording #\(currentRecording!.id)")
RecordingsDB.stop(&currentRecording!)
if validRecording {
let editVC = (children.first as! TVCPreviousRecords)
editVC.insertAndEditRecording(currentRecording!)
} else {
QLog.Debug("Discard illegal recording #\(currentRecording!.id)")
RecordingsDB.delete(currentRecording!)
}
currentRecording = nil // otherwise it will restart
} }
private func stopTimer(animate: Bool) { private func notifyVPN(setRecording state: CurrentRecordingState) {
recordingTimer?.invalidate() PrefsShared.CurrentlyRecording = state
recordingTimer = nil self.state = state
updateUI(setRecording: false, animated: animate) GlassVPN.send(.isRecording(state))
} }
private func updateUI(setRecording: Bool, animated: Bool) { private func updateUI(setRecording: Bool, animated: Bool) {
let title = setRecording ? "Stop Recording" : "Start New Recording" stopButton.tag = 99 // tag used in timerCallback()
let color = setRecording ? UIColor.systemRed : nil stopButton.setTitle("", for: .normal) // prevent flashing while animating in and out
let yT = setRecording ? 0 : -timeLabel.frame.height let block = {
let yB = (setRecording ? 1 : 0.5) * (startButton.superview!.frame.height - startButton.frame.height) self.headerView.isHidden = setRecording
if !animated { // else title will flash self.buttonView.isHidden = setRecording
startButton.titleLabel?.text = title self.runningView.isHidden = !setRecording
}
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)
} }
animated ? UIView.animate(withDuration: 0.3, animations: block) : block()
} }
private func startTimer(animate: Bool, longterm: Bool) {
guard let r = currentRecording, r.stop == nil else {
return
}
updateUI(setRecording: true, animated: animate)
let freq = longterm ? 1 : 0.086
let obj = (longterm, Date(r.start))
recordingTimer = Timer.repeating(freq, call: #selector(timerCallback(_:)), on: self, userInfo: obj)
recordingTimer!.fire() // update label immediately
}
// MARK: Tutorial View Controller private func stopTimer() {
recordingTimer?.invalidate()
recordingTimer = nil
updateUI(setRecording: false, animated: true)
}
@objc private func showTutorial() { @objc private func timerCallback(_ sender: Timer) {
let x = TutorialSheet() let (slow, start) = sender.userInfo as! (Bool, Date)
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() timeLabel.text = TimeFormat.since(start, millis: !slow, hours: slow)
.h1("What are Recordings?\n") let valid = slow == currentRecording!.isLongTerm
.normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " + let validInt = (valid ? 1 : 0)
"Recordings are usually 3  5 minutes long and cover a single application. " + if stopButton.tag != validInt {
"You can utilize recordings for App analysis or to get a ground truth for background traffic." + stopButton.tag = validInt
"\n\n" + stopButton.setTitle(valid ? "Stop" : slow ? "Cancel" : "Discard", for: .normal)
"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 {
Pref.DidShowTutorial.Recordings = true
} }
} }
} }

View File

@@ -0,0 +1,57 @@
import UIKit
protocol AnalysisBarDelegate {
func analysisBarWillOpenCoOccurrence() -> (domain: String, isFQDN: Bool)
}
class VCAnalysisBar: UIViewController, UITabBarDelegate {
@IBOutlet private var tabBar: UITabBar!
override func viewDidLoad() {
if #available(iOS 10.0, *) {
tabBar.unselectedItemTintColor = .sysLink
}
super.viewDidLoad()
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
let enabled = (parent as? AnalysisBarDelegate) != nil
for item in tabBar.items! { item.isEnabled = enabled }
}
// MARK: - Tab Bar Appearance
override func viewWillAppear(_: Bool) {
resizeTableViewHeader()
}
override func traitCollectionDidChange(_: UITraitCollection?) {
resizeTableViewHeader()
}
func resizeTableViewHeader() {
guard let tableView = (parent as? UITableViewController)?.tableView,
let head = tableView.tableHeaderView else { return }
// Recalculate and apply new height. Otherwise tabBar won't compress
tabBar.sizeToFit()
head.frame.size.height = tabBar.frame.height
tableView.tableHeaderView = head
}
// MARK: - Tab Bar Delegate
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
tabBar.selectedItem = nil
openCoOccurrence()
}
private func openCoOccurrence() {
guard let delegate = parent as? AnalysisBarDelegate else {
return
}
let x = delegate.analysisBarWillOpenCoOccurrence()
present(VCCoOccurrence.make(x.domain, isFQDN: x.isFQDN), animated: true)
}
}

View File

@@ -1,7 +1,8 @@
import UIKit import UIKit
class VCCoOccurrence: UIViewController, UITableViewDataSource { class VCCoOccurrence: UIViewController, UITableViewDataSource {
var fqdn: String! var domainName: String!
var isFQDN: Bool!
private var dataSource: [ContextAnalysisResult] = [] private var dataSource: [ContextAnalysisResult] = []
@IBOutlet private var tableView: UITableView! @IBOutlet private var tableView: UITableView!
@@ -13,9 +14,17 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
private var logTimeDelta: CGFloat = 1 private var logTimeDelta: CGFloat = 1
private var logMaxCount: CGFloat = 1 private var logMaxCount: CGFloat = 1
static func make(_ domain: String, isFQDN: Bool = true) -> Self {
let story = UIStoryboard(name: "CoOccurrence", bundle: nil)
let vc = story.instantiateInitialViewController() as! Self
vc.domainName = domain
vc.isFQDN = isFQDN
return vc
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
selectedTime = Pref.ContextAnalyis.CoOccurrenceTime ?? 5 // calls `didSet` and `logTimeDelta` selectedTime = Prefs.ContextAnalyis.CoOccurrenceTime // calls `didSet` and `logTimeDelta`
timeSegment.removeAllSegments() // clear IB values timeSegment.removeAllSegments() // clear IB values
for (i, time) in availableTimes.enumerated() { for (i, time) in availableTimes.enumerated() {
timeSegment.insertSegment(withTitle: TimeFormat(.abbreviated).from(seconds: time), at: i, animated: false) timeSegment.insertSegment(withTitle: TimeFormat(.abbreviated).from(seconds: time), at: i, animated: false)
@@ -30,14 +39,15 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
dataSource = [("Loading …", 0, 0, 0)] dataSource = [("Loading …", 0, 0, 0)]
logMaxCount = 1 logMaxCount = 1
tableView.reloadData() tableView.reloadData()
let domain = fqdn! let domain = domainName!
let flag = isFQDN!
let time = Timestamp(selectedTime) let time = Timestamp(selectedTime)
DispatchQueue.global().async { [weak self] in DispatchQueue.global().async { [weak self] in
let temp: [ContextAnalysisResult] let temp: [ContextAnalysisResult]
let total: Int32 let total: Int32
if let db = AppDB, if let db = AppDB,
let times = db.dnsLogsUniqTs(domain), times.count > 0, let times = db.dnsLogsUniqTs(domain, isFQDN: flag), times.count > 0,
let result = db.contextAnalysis(coOccurrence: times, plusMinus: time, exclude: domain), let result = db.contextAnalysis(coOccurrence: times, plusMinus: time, exclude: domain, isFQDN: flag),
result.count > 0 result.count > 0
{ {
temp = result temp = result
@@ -58,7 +68,7 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
@IBAction func didChangeTime(_ sender: UISegmentedControl) { @IBAction func didChangeTime(_ sender: UISegmentedControl) {
selectedTime = availableTimes[sender.selectedSegmentIndex] selectedTime = availableTimes[sender.selectedSegmentIndex]
Pref.ContextAnalyis.CoOccurrenceTime = selectedTime Prefs.ContextAnalyis.CoOccurrenceTime = selectedTime
reloadDataSource() reloadDataSource()
} }
@@ -130,40 +140,9 @@ extension VCCoOccurrence {
}() }()
let x = TutorialSheet() let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-cooccurrence", replacements: [
.h3("Co-Occurrence") "<IMG>" : .init(image: sampleCell, centered: true)
.normal(" allows you to find requests that happen often at the same time as the selected domain. " + ]))
"Hence it will give you a hint what Apps might be involved in the activity." +
"\n\nHow do you interpret these results? Lets look at an example:\n\n")
.centered(.image(sampleCell))
.normal("\n\nThe domain ").bold("example.org").normal(" had ").bold("14").normal(" requests with an ").italic("average time divergence").normal(" of ").bold("0.71 seconds").normal(". " +
"That is, these 14 domain calls happend, on average, less then a second before or after the original request of the selected domain." +
"\n\nClose temporal proximity and high occurrence counts are both indicators for domain correlation. " +
"Results are sorted by a ranking index (").bold("9.").normal(") which strikes a balance between the two. " +
"Preferring entries with higher counts as well as low time divergence.")
.italic("\n\nTip: ").normal("As a visual guide you can look for the colored bar beside each value. " +
"The larger the bar, the greater the correlation.")
))
x.present(in: self) x.present(in: self)
} }
} }
fileprivate extension UIView {
func asImage(insets: UIEdgeInsets = .zero) -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds.inset(by: insets))
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(bounds.inset(by: insets).size)
let ctx = UIGraphicsGetCurrentContext()!
ctx.translateBy(x: -insets.left, y: -insets.top)
layer.render(in:ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}

View File

@@ -25,11 +25,20 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS
} }
} }
func pushOpen(domain: String) {
let A: TVCHosts = storyboard!.load("requestsHosts")
let B: TVCHostDetails = storyboard!.load("requestsOccurrences")
A.parentDomain = domain.extractDomain()
B.fullDomain = domain
navigationController?.pushViewController(A, animated: false)
navigationController?.pushViewController(B, animated: false)
}
// MARK: - Filter // MARK: - Filter
@IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) { @IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) {
let vc = self.storyboard!.instantiateViewController(withIdentifier: "domainFilter") let vc = storyboard!.load("domainFilter")
vc.modalPresentationStyle = .custom vc.modalPresentationStyle = .custom
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
vc.isModalInPresentation = true vc.isModalInPresentation = true
@@ -38,12 +47,12 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS
} }
@objc private func didChangeDateFilter() { @objc private func didChangeDateFilter() {
switch Pref.DateFilter.Kind { switch Prefs.DateFilter.Kind {
case .ABRange: // read start/end time case .ABRange: // read start/end time
self.filterButtonDetail.title = "AB" self.filterButtonDetail.title = "AB"
self.filterButton.image = UIImage(named: "filter-filled") self.filterButton.image = UIImage(named: "filter-filled")
case .LastXMin: // most recent case .LastXMin: // most recent
let lastXMin = Pref.DateFilter.LastXMin let lastXMin = Prefs.DateFilter.LastXMin
if lastXMin == 0 { fallthrough } if lastXMin == 0 { fallthrough }
self.filterButtonDetail.title = TimeFormat(.abbreviated).from(minutes: lastXMin) self.filterButtonDetail.title = TimeFormat(.abbreviated).from(minutes: lastXMin)
self.filterButton.image = UIImage(named: "filter-filled") self.filterButton.image = UIImage(named: "filter-filled")

View File

@@ -1,9 +1,7 @@
import UIKit import UIKit
class TVCHostDetails: UITableViewController, SyncUpdateDelegate, UITabBarDelegate { class TVCHostDetails: UITableViewController, SyncUpdateDelegate, AnalysisBarDelegate {
@IBOutlet private var actionsBar: UITabBar!
public var fullDomain: String! public var fullDomain: String!
private var dataSource: [GroupedTsOccurrence] = [] private var dataSource: [GroupedTsOccurrence] = []
// TODO: respect date reverse sort order // TODO: respect date reverse sort order
@@ -14,49 +12,35 @@ class TVCHostDetails: UITableViewController, SyncUpdateDelegate, UITabBarDelegat
sync.addObserver(self) // calls `syncUpdate(reset:)` sync.addObserver(self) // calls `syncUpdate(reset:)`
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
sync.allowPullToRefresh(onTVC: self, forObserver: self) sync.allowPullToRefresh(onTVC: self, forObserver: self)
actionsBar.unselectedItemTintColor = .systemBlue
} }
UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self) }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let index = tableView.indexPathForSelectedRow?.row {
let tvc = segue.destination as? TVCOccurrenceContext
tvc?.domain = fullDomain
tvc?.ts = dataSource[index].ts
}
}
func analysisBarWillOpenCoOccurrence() -> (domain: String, isFQDN: Bool) {
(fullDomain, true)
} }
// MARK: - Table View Data Source // MARK: - Table View Data Source
override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")! let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")!
let src = dataSource[indexPath.row] let src = dataSource[indexPath.row]
cell.textLabel?.text = DateFormat.seconds(src.ts) cell.textLabel?.text = DateFormat.seconds(src.ts)
cell.detailTextLabel?.text = (src.total > 1) ? "\(src.total)x" : nil cell.detailTextLabel?.text = (src.total > 1) ? "\(src.total)×" : nil
cell.imageView?.image = (src.blocked > 0 ? UIImage(named: "shield-x") : nil) cell.imageView?.image = (src.blocked > 0 ? UIImage(named: "shield-x") : nil)
return cell return cell
} }
} }
// #########################
// #
// # MARK: - Tab Bar
// #
// #########################
extension TVCHostDetails {
@objc private func didChangeOrientation(_ sender: Notification) {
tableView.sizeHeaderToFit() // otherwise TabBar won't compress
}
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
tabBar.selectedItem = nil
performSegue(withIdentifier: "segueAnalysisCoOccurrence", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueAnalysisCoOccurrence" {
(segue.destination as? VCCoOccurrence)?.fqdn = fullDomain
}
}
}
// ################################ // ################################
// # // #
// # MARK: - Partial Update // # MARK: - Partial Update

View File

@@ -1,6 +1,6 @@
import UIKit import UIKit
class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate { class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate, AnalysisBarDelegate {
lazy var source = GroupedDomainDataSource(withParent: parentDomain) lazy var source = GroupedDomainDataSource(withParent: parentDomain)
@@ -21,6 +21,10 @@ class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate {
} }
} }
func analysisBarWillOpenCoOccurrence() -> (domain: String, isFQDN: Bool) {
(parentDomain, false)
}
// MARK: - Table View Data Source // MARK: - Table View Data Source

View File

@@ -0,0 +1,89 @@
import UIKit
class TVCOccurrenceContext: UITableViewController {
var ts: Timestamp!
var domain: String!
private let dT: Timestamp = 300 // +/- 5 minutes
private lazy var dataSource: [DomainTsPair] = {
let logs = AppDB?.dnsLogs(between: ts - dT, and: ts + dT) ?? []
return [("[…]", ts - dT)] + logs.reversed() + [("[…]", ts + dT)]
}()
override func viewDidLoad() {
navigationItem.title = "± 5 Min Context"
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
jumpToTsZero()
}
@IBAction private func jumpToTsZero() {
if let i = dataSource.firstIndex(where: { isChoosenOne($0) }) {
tableView.scrollToRow(at: IndexPath(row: i), at: .middle, animated: true)
}
}
private func isChoosenOne(_ obj: DomainTsPair) -> Bool {
obj.domain == domain && obj.ts == ts
}
private func firstOrLast(_ row: Int) -> Bool {
row == 0 || row == dataSource.count - 1
}
// MARK: - Table View Data Source
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "OccurrenceContextCell")!
let src = dataSource[indexPath.row]
cell.detailTextLabel?.text = src.domain
if firstOrLast(indexPath.row) {
cell.detailTextLabel?.textColor = .sysLabel2 // same as textLabel
} else if isChoosenOne(src) {
cell.detailTextLabel?.textColor = .sysLink
} else {
cell.detailTextLabel?.textColor = .sysLabel
}
if src.ts > ts {
cell.textLabel?.text = "+ " + TimeFormat.from(src.ts - ts)
} else if src.ts < ts {
cell.textLabel?.text = " " + TimeFormat.from(ts - src.ts)
} else {
cell.textLabel?.text = "0"
}
//cell.textLabel?.text = String(format: "%+d s", src.ts - ts)
return cell
}
// MARK: - Tap to Copy
private var cellMenu = TableCellTapMenu()
private var copyDomain: String? = nil
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if cellMenu.start(tableView, indexPath) {
copyDomain = cellMenu.getSelected(dataSource)?.domain
self.becomeFirstResponder()
}
return nil
}
override var canBecomeFirstResponder: Bool { true }
override func copy(_ sender: Any?) {
if let dom = copyDomain {
UIPasteboard.general.string = dom
}
cellMenu.reset()
copyDomain = nil
}
}

View File

@@ -18,8 +18,8 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet private var rangeView: UIView! @IBOutlet private var rangeView: UIView!
@IBOutlet private var buttonRangeStart: UIButton! @IBOutlet private var buttonRangeStart: UIButton!
@IBOutlet private var buttonRangeEnd: UIButton! @IBOutlet private var buttonRangeEnd: UIButton!
private lazy var tsRangeA: Timestamp = Pref.DateFilter.RangeA ?? AppDB?.dnsLogsMinDate() ?? .now() private lazy var tsRangeA: Timestamp = Prefs.DateFilter.RangeA ?? AppDB?.dnsLogsMinDate() ?? .now()
private lazy var tsRangeB: Timestamp = Pref.DateFilter.RangeB ?? .now() private lazy var tsRangeB: Timestamp = Prefs.DateFilter.RangeB ?? .now()
// order by // order by
@IBOutlet private var orderbyType: UISegmentedControl! @IBOutlet private var orderbyType: UISegmentedControl!
@@ -29,18 +29,18 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
filterBy.selectedSegmentIndex = (Pref.DateFilter.Kind == .ABRange ? 1 : 0) filterBy.selectedSegmentIndex = (Prefs.DateFilter.Kind == .ABRange ? 1 : 0)
didChangeFilterBy(filterBy) didChangeFilterBy(filterBy)
durationSlider.tag = -1 // otherwise wont update because `tag == 0` durationSlider.tag = -1 // otherwise wont update because `tag == 0`
durationSlider.value = Float(durationTimes.firstIndex(of: Pref.DateFilter.LastXMin) ?? 0) / 9 durationSlider.value = Float(durationTimes.firstIndex(of: Prefs.DateFilter.LastXMin) ?? 0) / 9
durationSliderChanged(durationSlider) durationSliderChanged(durationSlider)
buttonRangeStart.setTitle(DateFormat.minutes(tsRangeA), for: .normal) buttonRangeStart.setTitle(DateFormat.minutes(tsRangeA), for: .normal)
buttonRangeEnd.setTitle(DateFormat.minutes(tsRangeB), for: .normal) buttonRangeEnd.setTitle(DateFormat.minutes(tsRangeB), for: .normal)
orderbyType.selectedSegmentIndex = Pref.DateFilter.OrderBy.rawValue orderbyType.selectedSegmentIndex = Prefs.DateFilter.OrderBy.rawValue
orderbyAsc.selectedSegmentIndex = (Pref.DateFilter.OrderAsc ? 0 : 1) orderbyAsc.selectedSegmentIndex = (Prefs.DateFilter.OrderAsc ? 0 : 1)
} }
@IBAction private func didChangeFilterBy(_ sender: UISegmentedControl) { @IBAction private func didChangeFilterBy(_ sender: UISegmentedControl) {
@@ -63,10 +63,9 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
@IBAction private func didTapRangeButton(_ sender: UIButton) { @IBAction private func didTapRangeButton(_ sender: UIButton) {
let flag = (sender == buttonRangeStart) let flag = (sender == buttonRangeStart)
DatePickerAlert(presentIn: self, configure: { let oldDate = flag ? Date(self.tsRangeA) : Date(self.tsRangeB)
$0.setDate(Date(flag ? self.tsRangeA : self.tsRangeB), animated: false) DatePickerAlert(initial: oldDate).present(in: self) {
}, onSuccess: { var ts = $1.timestamp
var ts = $0.timestamp
ts -= ts % 60 // remove seconds ts -= ts % 60 // remove seconds
// if one of these is greater than the other, adjust the latter too. // if one of these is greater than the other, adjust the latter too.
if flag || self.tsRangeA > ts { if flag || self.tsRangeA > ts {
@@ -77,7 +76,7 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
self.tsRangeB = ts + 59 // upper end of minute self.tsRangeB = ts + 59 // upper end of minute
self.buttonRangeEnd.setTitle(DateFormat.minutes(ts + 59), for: .normal) self.buttonRangeEnd.setTitle(DateFormat.minutes(ts + 59), for: .normal)
} }
}) }
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
@@ -104,17 +103,33 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
case 2: orderType = .Count case 2: orderType = .Count
default: preconditionFailure() default: preconditionFailure()
} }
let a = Pref.DateFilter.OrderBy <-? orderType let a = Prefs.DateFilter.OrderBy <-? orderType
let b = Pref.DateFilter.OrderAsc <-? (orderbyAsc.selectedSegmentIndex == 0) let b = Prefs.DateFilter.OrderAsc <-? (orderbyAsc.selectedSegmentIndex == 0)
if a || b { if a || b {
NotifySortOrderChanged.post() NotifySortOrderChanged.post()
} }
let c = Pref.DateFilter.Kind <-? filterType let c = Prefs.DateFilter.Kind <-? filterType
let d = Pref.DateFilter.LastXMin <-? newXMin let d = Prefs.DateFilter.LastXMin <-? newXMin
let e = Pref.DateFilter.RangeA <-? (filterType == .ABRange ? tsRangeA : nil) let e = Prefs.DateFilter.RangeA <-? (filterType == .ABRange ? tsRangeA : nil)
let f = Pref.DateFilter.RangeB <-? (filterType == .ABRange ? tsRangeB : nil) let f = Prefs.DateFilter.RangeB <-? (filterType == .ABRange ? tsRangeB : nil)
if c || d || e || f { if c || d || e || f {
NotifyDateFilterChanged.post() NotifyDateFilterChanged.post()
} }
} }
static func disableFilter() {
if Prefs.DateFilter.Kind <-? .Off {
Prefs.DateFilter.LastXMin = 0
Prefs.DateFilter.RangeA = nil
Prefs.DateFilter.RangeB = nil
NotifyDateFilterChanged.post()
}
}
static func setFilter(range from: Timestamp?, to: Timestamp?) {
Prefs.DateFilter.Kind = .ABRange
Prefs.DateFilter.RangeA = from
Prefs.DateFilter.RangeB = to
NotifyDateFilterChanged.post()
}
} }

View File

@@ -0,0 +1,97 @@
import UIKit
import AudioToolbox
protocol NotificationSoundChangedDelegate {
/// Use `#mute` to disable sounds and `#default` to use default notification sound.
func notificationSoundCurrent() -> String
/// Called every time the user changes selection
func notificationSoundChanged(filename: String, title: String)
}
class TVCChooseAlertTone: UITableViewController {
var delegate: NotificationSoundChangedDelegate!
private lazy var selected: String = delegate.notificationSoundCurrent()
private func playTone(_ name: String) {
switch name {
case "#mute": return // No Sound
case "#default": AudioServicesPlayAlertSound(1315) // Default sound
default:
guard let url = Bundle.main.url(forResource: name, withExtension: "caf") else {
preconditionFailure("Something went wrong. Sound file \(name).caf does not exist.")
}
var soundId: SystemSoundID = 0
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
AudioServicesAddSystemSoundCompletion(soundId, nil, nil, { id, _ -> Void in
AudioServicesDisposeSystemSoundID(id)
}, nil)
AudioServicesPlayAlertSound(soundId)
}
}
// MARK: Table View Delegate
override func numberOfSections(in _: UITableView) -> Int {
AvailableSounds.count
}
override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
AvailableSounds[section].count
}
override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
section == 1 ? "AppCheck" : nil
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAlertToneCell")!
let src = AvailableSounds[indexPath.section][indexPath.row]
cell.textLabel?.text = src.title
cell.accessoryType = (src.file == selected) ? .checkmark : .none
return cell
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let src = AvailableSounds[indexPath.section][indexPath.row]
selected = src.file
tableView.reloadData() // re-apply checkmarks
playTone(selected)
delegate.notificationSoundChanged(filename: src.file, title: src.title)
return nil
}
}
// MARK: - Sounds Data Source
// afconvert input.aiff output.caf -d ima4 -f caff -v
fileprivate let AvailableSounds: [[(title: String, file: String)]] = [
[ // System sounds
("None", "#mute"),
("Default", "#default")
], [ // AppCheck sounds
("Clock", "clock"),
("Drum 1", "drum1"),
("Drum 2", "drum2"),
("Plop 1", "plop1"),
("Plop 2", "plop2"),
("Snap 1", "snap1"),
("Snap 2", "snap2"),
("Typewriter 1", "typewriter1"),
("Typewriter 2", "typewriter2"),
("Wood 1", "wood1"),
("Wood 2", "wood2")
]
]
func AlertSoundTitle(for filename: String) -> String {
for section in AvailableSounds {
for row in section {
if row.file == filename {
return row.title
}
}
}
return ""
}

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