Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b270f30f3c | ||
|
|
03177cee0b | ||
|
|
9ee094dc20 | ||
|
|
b1d49c6765 | ||
|
|
b774e2152c | ||
|
|
e398ac8bcd | ||
|
|
01523b250f | ||
|
|
a2b0f311d5 | ||
|
|
88a52fb92c | ||
|
|
723f1665a7 | ||
|
|
4f92d3d58d | ||
|
|
05d06a4f31 | ||
|
|
f9ab545e0f | ||
|
|
b10d4c8b36 | ||
|
|
5a3ca024f8 | ||
|
|
92216c0c03 | ||
|
|
9ece3474c6 | ||
|
|
6dcc2086e6 | ||
|
|
08483711e2 | ||
|
|
0e100006d3 | ||
|
|
710c617862 | ||
|
|
3ed25c92cd | ||
|
|
f7644e6048 | ||
|
|
80afa6aff1 | ||
|
|
43de81929f | ||
|
|
e315e71d07 | ||
|
|
416eb34799 | ||
|
|
b7b13f51b2 | ||
|
|
2312187670 | ||
|
|
c7d0dc7c5f | ||
|
|
895cabee80 |
@@ -7,26 +7,64 @@
|
|||||||
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 */; };
|
||||||
@@ -39,10 +77,11 @@
|
|||||||
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 */; };
|
||||||
|
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; };
|
||||||
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 */; };
|
||||||
@@ -128,6 +167,8 @@
|
|||||||
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 */; };
|
||||||
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 +176,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 +213,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 +233,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,7 +259,7 @@
|
|||||||
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>"; };
|
||||||
@@ -201,10 +270,11 @@
|
|||||||
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>"; };
|
||||||
|
549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; 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>"; };
|
||||||
@@ -294,16 +364,21 @@
|
|||||||
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>"; };
|
||||||
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 +407,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 +418,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>";
|
||||||
@@ -357,6 +436,17 @@
|
|||||||
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,8 +473,11 @@
|
|||||||
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 */,
|
||||||
@@ -392,6 +485,7 @@
|
|||||||
54B345B12422E029004C53CC /* unused */,
|
54B345B12422E029004C53CC /* unused */,
|
||||||
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
|
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
|
||||||
541AC5DB2399498A00A769D7 /* Main.storyboard */,
|
541AC5DB2399498A00A769D7 /* Main.storyboard */,
|
||||||
|
543078C124B60F3B00278F2D /* Settings.storyboard */,
|
||||||
541AC5DE2399498B00A769D7 /* Assets.xcassets */,
|
541AC5DE2399498B00A769D7 /* Assets.xcassets */,
|
||||||
541AC5E32399498B00A769D7 /* Info.plist */,
|
541AC5E32399498B00A769D7 /* Info.plist */,
|
||||||
54953E7023E473F10054345C /* Settings.bundle */,
|
54953E7023E473F10054345C /* Settings.bundle */,
|
||||||
@@ -399,23 +493,43 @@
|
|||||||
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 = (
|
||||||
|
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,12 +547,16 @@
|
|||||||
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 */,
|
||||||
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 */,
|
||||||
);
|
);
|
||||||
path = "Common Classes";
|
path = "Common Classes";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -458,10 +576,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 */,
|
||||||
@@ -708,7 +828,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 +931,23 @@
|
|||||||
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 */,
|
||||||
|
543078B024B5E12500278F2D /* plop2.caf in Resources */,
|
||||||
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */,
|
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard 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 */,
|
||||||
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */,
|
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */,
|
||||||
54B345B0242264F8004C53CC /* third-level.txt in Resources */,
|
54B345B0242264F8004C53CC /* third-level.txt 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 */,
|
||||||
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */,
|
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -824,6 +956,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 +977,54 @@
|
|||||||
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 */,
|
||||||
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 */,
|
||||||
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 /* AppInfoType.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 */,
|
||||||
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */,
|
541FC9872497D81C00962623 /* TheGreatDestroyer.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 +1032,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 +1053,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 +1077,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 +1088,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 +1131,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 +1163,14 @@
|
|||||||
name = LaunchScreen.storyboard;
|
name = LaunchScreen.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
543078C124B60F3B00278F2D /* Settings.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
543078C224B60F3B00278F2D /* Base */,
|
||||||
|
);
|
||||||
|
name = Settings.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@@ -1127,7 +1301,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 = 26;
|
||||||
INFOPLIST_FILE = main/Info.plist;
|
INFOPLIST_FILE = main/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -1146,7 +1320,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 = 26;
|
||||||
INFOPLIST_FILE = main/Info.plist;
|
INFOPLIST_FILE = main/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -1165,7 +1339,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 = 26;
|
||||||
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 +1357,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 = 26;
|
||||||
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";
|
||||||
|
|||||||
@@ -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
|
private func didInitProxy() {
|
||||||
guard error == nil else {
|
if PrefsShared.RestartReminder.Enabled {
|
||||||
DDLogError("setTunnelNetworkSettings error: \(String(describing: error))")
|
PushNotification.scheduleRestartReminderBadge(on: false)
|
||||||
completionHandler(error)
|
PushNotification.cancel(.CantStopMeNowReminder)
|
||||||
return
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
14
README.md
@@ -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
|
||||||
|
- Context Analysis
|
||||||
|
- What other domains occur often at the same time?
|
||||||
|
- What happened immediately before or after the action?
|
||||||
|
- Export results for custom analysis
|
||||||
|
|
||||||
**… and soon:**
|
**… and soon:**
|
||||||
|
|
||||||
- Alert Monitor & reminder
|
- Alert Monitor & reminder
|
||||||
- Occurrence Context Analysis
|
|
||||||
- Participate in privacy research
|
- Participate in privacy research
|
||||||
|
|
||||||
|
|
||||||
<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™*
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 158 KiB |
@@ -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,21 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
main/Assets.xcassets/.DS_Store
vendored
@@ -19,5 +19,8 @@
|
|||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,8 @@
|
|||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,8 @@
|
|||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
main/Assets.xcassets/jump-to-target.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
main/Assets.xcassets/jump-to-target.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 230 B |
BIN
main/Assets.xcassets/jump-to-target.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 409 B |
BIN
main/Assets.xcassets/jump-to-target.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 544 B |
26
main/Assets.xcassets/line-collapse.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
main/Assets.xcassets/line-collapse.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 150 B |
BIN
main/Assets.xcassets/line-collapse.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 216 B |
BIN
main/Assets.xcassets/line-collapse.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 283 B |
26
main/Assets.xcassets/line-expand.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
main/Assets.xcassets/line-expand.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
main/Assets.xcassets/line-expand.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
main/Assets.xcassets/line-expand.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 282 B |
@@ -234,7 +234,7 @@
|
|||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
</view>
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" white="0.0" alpha="0.2" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="0.0" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<gestureRecognizers/>
|
<gestureRecognizers/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="sAi-8j-0n1" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="4" id="DCq-Ps-sQo"/>
|
<constraint firstItem="sAi-8j-0n1" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="4" id="DCq-Ps-sQo"/>
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="0HB-5f-eB1">
|
<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"/>
|
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
@@ -339,14 +339,21 @@
|
|||||||
<!--Hosts-->
|
<!--Hosts-->
|
||||||
<scene sceneID="ZCV-Yx-jjW">
|
<scene sceneID="ZCV-Yx-jjW">
|
||||||
<objects>
|
<objects>
|
||||||
<tableViewController id="WcC-nb-Vf5" customClass="TVCHosts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
<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">
|
<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="568"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
<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>
|
<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">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
|
||||||
<rect key="frame" x="0.0" y="28" width="320" height="57.5"/>
|
<rect key="frame" x="0.0" y="77" width="320" height="57.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
|
||||||
@@ -387,31 +394,27 @@
|
|||||||
<!--Occurrences-->
|
<!--Occurrences-->
|
||||||
<scene sceneID="ws3-sK-l8m">
|
<scene sceneID="ws3-sK-l8m">
|
||||||
<objects>
|
<objects>
|
||||||
<tableViewController id="h7Z-Qr-pJ5" customClass="TVCHostDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
<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" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="4ms-FO-Fge">
|
<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="568"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
<tabBar key="tableHeaderView" contentMode="scaleToFill" fixedFrame="YES" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1Jy-zg-CXR">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="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>
|
<connections>
|
||||||
<outlet property="delegate" destination="h7Z-Qr-pJ5" id="qNN-nI-Kub"/>
|
<segue destination="1ba-SA-8sT" kind="embed" id="ueN-6L-cP7"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tabBar>
|
</containerView>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="HostDetailCell" textLabel="J2P-mU-Vad" detailTextLabel="eWb-mX-udN" style="IBUITableViewCellStyleValue1" id="ZCA-Dz-i92">
|
<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"/>
|
<rect key="frame" x="0.0" y="77" width="320" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<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">
|
<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="320" height="43.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="J2P-mU-Vad">
|
<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"/>
|
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
@@ -419,7 +422,7 @@
|
|||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="eWb-mX-udN">
|
<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="260" y="12" width="44" height="20.5"/>
|
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<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"/>
|
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
@@ -427,6 +430,9 @@
|
|||||||
</label>
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</tableViewCellContentView>
|
</tableViewCellContentView>
|
||||||
|
<connections>
|
||||||
|
<segue destination="rjy-Di-Cru" kind="push" id="SfC-iY-Ce0"/>
|
||||||
|
</connections>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</prototypes>
|
</prototypes>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -435,25 +441,52 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</tableView>
|
</tableView>
|
||||||
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
|
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
|
||||||
<connections>
|
|
||||||
<outlet property="actionsBar" destination="1Jy-zg-CXR" id="7x3-Vy-i9C"/>
|
|
||||||
<segue destination="W5Q-oz-bFb" kind="modal" identifier="segueAnalysisCoOccurrence" id="ukY-Dy-AIA"/>
|
|
||||||
</connections>
|
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="2100" y="-1250"/>
|
<point key="canvasLocation" x="2100" y="-1250"/>
|
||||||
</scene>
|
</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="1400" y="-1950"/>
|
||||||
|
</scene>
|
||||||
<!--Co Occurrence-->
|
<!--Co Occurrence-->
|
||||||
<scene sceneID="Gbm-AP-b72">
|
<scene sceneID="Gbm-AP-b72">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController id="W5Q-oz-bFb" customClass="VCCoOccurrence" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="IBCoOccurrence" id="W5Q-oz-bFb" customClass="VCCoOccurrence" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" contentMode="scaleToFill" id="f34-NO-d8f">
|
<view key="view" contentMode="scaleToFill" id="f34-NO-d8f">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="548"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rvt-nC-2Zr">
|
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rvt-nC-2Zr">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="56"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||||
<items>
|
<items>
|
||||||
<navigationItem title="Co-Occurrence" id="csY-x8-Rpe">
|
<navigationItem title="Co-Occurrence" id="csY-x8-Rpe">
|
||||||
<barButtonItem key="leftBarButtonItem" systemItem="done" id="eg9-p3-Xas">
|
<barButtonItem key="leftBarButtonItem" systemItem="done" id="eg9-p3-Xas">
|
||||||
@@ -463,7 +496,7 @@
|
|||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
<barButtonItem key="rightBarButtonItem" id="bTi-7F-CFS">
|
<barButtonItem key="rightBarButtonItem" id="bTi-7F-CFS">
|
||||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="infoLight" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="kqK-SL-CxZ">
|
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="infoLight" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="kqK-SL-CxZ">
|
||||||
<rect key="frame" x="279" y="16" width="25" height="24"/>
|
<rect key="frame" x="279" y="10" width="25" height="24"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="showInfoScreen" destination="W5Q-oz-bFb" eventType="touchUpInside" id="TuI-R9-PNr"/>
|
<action selector="showInfoScreen" destination="W5Q-oz-bFb" eventType="touchUpInside" id="TuI-R9-PNr"/>
|
||||||
@@ -474,7 +507,7 @@
|
|||||||
</items>
|
</items>
|
||||||
</navigationBar>
|
</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">
|
<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="56" width="320" height="492"/>
|
<rect key="frame" x="0.0" y="44" width="320" height="524"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="32"/>
|
||||||
@@ -505,7 +538,7 @@
|
|||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zbU-wC-qJG">
|
<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"/>
|
<rect key="frame" x="15" y="11" width="290" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
@@ -638,6 +671,57 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yYY-5U-gct" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="yYY-5U-gct" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
|
<point key="canvasLocation" x="2100" y="-1950"/>
|
||||||
|
</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="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="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="2800" y="-1250"/>
|
<point key="canvasLocation" x="2800" y="-1250"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Recordings-->
|
<!--Recordings-->
|
||||||
@@ -746,7 +830,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="280" height="57.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="280" height="57.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hr0-Xt-5gV">
|
<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"/>
|
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
@@ -946,14 +1030,14 @@ Duration: 60:00</string>
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="PreviousRecordDetailCell" textLabel="rN0-kA-Eln" detailTextLabel="xRp-XG-oKf" style="IBUITableViewCellStyleValue1" id="ceT-cF-lLF">
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" 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"/>
|
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<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">
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rN0-kA-Eln">
|
<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"/>
|
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
@@ -970,13 +1054,67 @@ Duration: 60:00</string>
|
|||||||
</subviews>
|
</subviews>
|
||||||
</tableViewCellContentView>
|
</tableViewCellContentView>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" 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="default" 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>
|
||||||
</prototypes>
|
</prototypes>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
|
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
|
||||||
<outlet property="delegate" destination="50g-BI-Q6S" id="LBY-sp-dg0"/>
|
<outlet property="delegate" destination="50g-BI-Q6S" id="LBY-sp-dg0"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableView>
|
</tableView>
|
||||||
<navigationItem key="navigationItem" title="Logs" id="AXT-fV-keV"/>
|
<navigationItem key="navigationItem" title="Logs" id="AXT-fV-keV">
|
||||||
|
<barButtonItem key="rightBarButtonItem" image="line-expand" id="xLc-O7-KVB">
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleDisplayStyle:" destination="50g-BI-Q6S" id="3wo-9O-7gV"/>
|
||||||
|
</connections>
|
||||||
|
</barButtonItem>
|
||||||
|
</navigationItem>
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
@@ -992,7 +1130,7 @@ Duration: 60:00</string>
|
|||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</navigationBar>
|
</navigationBar>
|
||||||
<connections>
|
<connections>
|
||||||
<segue destination="qdB-ZO-LHY" kind="relationship" relationship="rootViewController" id="qJW-Jc-O4D"/>
|
<segue destination="cV2-If-0fV" kind="relationship" relationship="rootViewController" id="vQq-KE-MOO"/>
|
||||||
</connections>
|
</connections>
|
||||||
</navigationController>
|
</navigationController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
@@ -1000,250 +1138,25 @@ Duration: 60:00</string>
|
|||||||
<point key="canvasLocation" x="0.0" y="150"/>
|
<point key="canvasLocation" x="0.0" y="150"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<scene sceneID="gEe-ny-NaU">
|
<scene sceneID="l0f-fL-3tG">
|
||||||
<objects>
|
<objects>
|
||||||
<tableViewController id="qdB-ZO-LHY" customClass="TVCSettings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
<viewControllerPlaceholder storyboardName="Settings" id="cV2-If-0fV" 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">
|
<navigationItem key="navigationItem" id="wlO-Ea-6he"/>
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
</viewControllerPlaceholder>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="i3W-Ff-rJL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
<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" 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>
|
|
||||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmY-ot-lJW">
|
|
||||||
<rect key="frame" x="255" y="6.5" width="51" height="31"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleVPNProxy:" destination="qdB-ZO-LHY" eventType="valueChanged" id="y95-2Z-Uep"/>
|
|
||||||
</connections>
|
|
||||||
</switch>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="VPN Proxy enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qha-4I-go0">
|
|
||||||
<rect key="frame" x="16" y="12" width="147" height="20"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="kmY-ot-lJW" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Qha-4I-go0" secondAttribute="trailing" constant="8" id="Lnx-hC-xOx"/>
|
|
||||||
<constraint firstItem="kmY-ot-lJW" firstAttribute="trailing" secondItem="d2v-vz-QIB" secondAttribute="trailingMargin" id="Ylz-D4-hz4"/>
|
|
||||||
<constraint firstItem="Qha-4I-go0" firstAttribute="centerY" secondItem="d2v-vz-QIB" secondAttribute="centerY" id="dKE-By-qEu"/>
|
|
||||||
<constraint firstItem="kmY-ot-lJW" firstAttribute="centerY" secondItem="Qha-4I-go0" secondAttribute="centerY" id="dgh-tx-Y8a"/>
|
|
||||||
<constraint firstItem="Qha-4I-go0" firstAttribute="leading" secondItem="d2v-vz-QIB" secondAttribute="leadingMargin" id="rHx-0D-DPX"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
</tableViewCell>
|
|
||||||
</cells>
|
|
||||||
</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="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="299.5" width="320" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="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="343.5" width="320" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="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="adR-Yk-zsB"/>
|
|
||||||
</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" id="wLR-T2-Qxm">
|
|
||||||
<cells>
|
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
|
|
||||||
<rect key="frame" x="0.0" y="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="320" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Ko-sD-7x0">
|
|
||||||
<rect key="frame" x="125" y="7" width="70" height="30"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
|
||||||
<state key="normal" title="Export DB"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerX" secondItem="Mfs-fu-W5k" secondAttribute="centerX" id="LzG-xg-XTg"/>
|
|
||||||
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerY" secondItem="Mfs-fu-W5k" secondAttribute="centerY" id="SXw-dC-2kl"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
</tableViewCell>
|
|
||||||
</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"/>
|
|
||||||
<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="vpnToggle" destination="kmY-ot-lJW" id="yeS-DE-FfR"/>
|
|
||||||
</connections>
|
|
||||||
</tableViewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="VNK-Z0-T0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="700" y="150"/>
|
<point key="canvasLocation" x="700" y="150"/>
|
||||||
</scene>
|
</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" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
|
||||||
<prototypes>
|
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" 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" adjustsFontSizeToFit="NO" 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="1400" y="150"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
</scenes>
|
||||||
<inferredMetricsTieBreakers>
|
<inferredMetricsTieBreakers>
|
||||||
<segue reference="EzT-Xq-wka"/>
|
<segue reference="ueN-6L-cP7"/>
|
||||||
</inferredMetricsTieBreakers>
|
</inferredMetricsTieBreakers>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="filter-clear" width="20" height="20"/>
|
<image name="filter-clear" width="20" height="20"/>
|
||||||
<image name="intersection" width="25" height="25"/>
|
<image name="intersection" width="25" height="25"/>
|
||||||
<image name="journal" width="25" height="25"/>
|
<image name="journal" width="25" height="25"/>
|
||||||
|
<image name="jump-to-target" width="20" height="20"/>
|
||||||
|
<image name="line-expand" width="20" height="20"/>
|
||||||
<image name="settings" width="25" height="25"/>
|
<image name="settings" width="25" height="25"/>
|
||||||
<image name="tag" width="25" height="25"/>
|
<image name="tag" width="25" height="25"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
837
main/Base.lproj/Settings.storyboard
Normal 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" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<prototypes>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" 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>
|
||||||
237
main/Common Classes/CustomAlert.swift
Normal 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]`
|
||||||
|
let 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)) }
|
||||||
|
|||||||
122
main/Common Classes/Prefs.swift
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
main/Common Classes/PrefsShared.swift
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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: - 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, *) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
182
main/Common Classes/SlideInAnimation.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
main/Common Classes/ThrottledBatchQueue.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ extension SQLiteDatabase {
|
|||||||
do {
|
do {
|
||||||
try run(sql: "SELECT 1 FROM req LIMIT 1;") // fails if req doesnt exist
|
try run(sql: "SELECT 1 FROM req LIMIT 1;") // fails if req doesnt exist
|
||||||
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,domain,domainof(domain),nullif(logOpt,0) FROM req;
|
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,domain,domainof(domain),nullif(logOpt,0) FROM req;
|
||||||
DROP TABLE req;
|
DROP TABLE req;
|
||||||
COMMIT;
|
|
||||||
""")
|
""")
|
||||||
} catch { /* no need to migrate */ }
|
} catch { /* no need to migrate */ }
|
||||||
}
|
}
|
||||||
@@ -54,6 +52,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 +107,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 {
|
||||||
|
|
||||||
@@ -114,13 +117,12 @@ extension SQLiteDatabase {
|
|||||||
/// - Returns: `nil` in case no entries were transmitted.
|
/// - Returns: `nil` in case no entries were transmitted.
|
||||||
@discardableResult func dnsLogsPersist() -> SQLiteRowRange? {
|
@discardableResult func dnsLogsPersist() -> SQLiteRowRange? {
|
||||||
guard lastRowId(.cache) > 0 else { return nil }
|
guard lastRowId(.cache) > 0 else { return nil }
|
||||||
|
transaction("ALTER TABLE cache RENAME TO tmp_cache; \(CreateTable.cache)")
|
||||||
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 tmp_cache;
|
||||||
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache;
|
DROP TABLE tmp_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 +152,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 +166,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 +202,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 +219,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 +241,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 +255,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 +280,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,13 +362,13 @@ 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))
|
notes: col_text(stmt, 5))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `WHERE stop IS NULL`
|
/// `WHERE stop IS NULL`
|
||||||
@@ -365,6 +379,14 @@ extension SQLiteDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 * FROM rec WHERE stop IS NOT NULL;") {
|
||||||
@@ -396,8 +418,6 @@ extension CreateTable {
|
|||||||
"""}
|
"""}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias RecordLog = (domain: String, count: Int32)
|
|
||||||
|
|
||||||
extension SQLiteDatabase {
|
extension SQLiteDatabase {
|
||||||
|
|
||||||
// MARK: write
|
// MARK: write
|
||||||
@@ -426,13 +446,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)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,21 @@ 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 }
|
||||||
|
return try self.run(sql: "DELETE FROM cache WHERE ts < strftime('%s', 'now', ?);",
|
||||||
|
bind: [BindText("-\(days) days")]) {
|
||||||
|
try ifStep($0, SQLITE_DONE)
|
||||||
|
return numberOfChanges > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +74,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 +85,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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,15 +91,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;"); }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -193,7 +198,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,5 @@ extension FilterOptions {
|
|||||||
extension Recording {
|
extension Recording {
|
||||||
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } }
|
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } }
|
||||||
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } }
|
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } }
|
||||||
var durationString: String? { get { stop == nil ? nil : TimeFormat.from(duration!) } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,18 @@ 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")
|
||||||
|
guard let success = try? AppDB?.dnsLogsDeleteOlderThan(days: days), success else {
|
||||||
|
return // nothing changed
|
||||||
|
}
|
||||||
|
sync.needsReloadDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,8 @@ 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) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification.
|
/// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification.
|
||||||
@@ -43,5 +46,11 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,29 @@ 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 domain = "\(arc4random() % 5).count.test.com"
|
||||||
|
let kill = hook.processDNSRequest(domain)
|
||||||
|
if kill { QLog.Info("Blocked: \(domain)") }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sendMsg(_ messageData: Data) {
|
||||||
|
hook.handleAppMessage(messageData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -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!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ func AskAlert(title: String?, text: String?, buttonText: String = "Continue", bu
|
|||||||
return alert
|
return alert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show alert hinting the user to go to system settings and re-enable notifications.
|
||||||
|
func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
|
||||||
|
Alert(title: "Notifications Disabled",
|
||||||
|
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications."
|
||||||
|
).presentIn(viewController)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Alert with multiple options
|
// MARK: Alert with multiple options
|
||||||
|
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
25
main/Extensions/Color.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
33
main/Extensions/Equatable.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ extension NSMutableAttributedString {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
main/Extensions/Logging.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,12 +74,18 @@ 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) -> String {
|
||||||
let t = Int(duration)
|
let t = Int(duration)
|
||||||
if millis {
|
if millis {
|
||||||
@@ -83,7 +95,7 @@ struct TimeFormat {
|
|||||||
return String(format: "%02d:%02d", t / 60, t % 60)
|
return String(format: "%02d:%02d", t / 60, t % 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Duration string with format `HH:mm` or `HH:mm.sss` since reference date
|
/// Duration string with format `mm:ss` or `mm:ss.SSS` since reference date
|
||||||
static func since(_ date: Date, millis: Bool = false) -> String {
|
static func since(_ date: Date, millis: Bool = false) -> String {
|
||||||
from(Date().timeIntervalSince(date), millis: millis)
|
from(Date().timeIntervalSince(date), millis: millis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ 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 exportDir() -> URL { FileManager.default.exportDir() }
|
||||||
static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
|
static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
|
||||||
|
|||||||
44
main/Extensions/View.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
160
main/GlassVPN.swift
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
138
main/GlassVPNHook.swift
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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!
|
||||||
|
|
||||||
|
init() { reset() }
|
||||||
|
|
||||||
|
/// Reload from stored settings and rebuilt binary search tree
|
||||||
|
private func reset() {
|
||||||
|
reloadDomainFilter()
|
||||||
|
setAutoDelete(PrefsShared.AutoDeleteLogsDays)
|
||||||
|
cachedNotify = CachedConnectionAlert()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
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)
|
||||||
|
// TODO: disable ignore & block during recordings
|
||||||
|
let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i]
|
||||||
|
if ignore {
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
queue.async {
|
||||||
|
do { try AppDB?.logWrite(domain, blocked: block) }
|
||||||
|
catch { NSLog("[VPN.WARN] Couldn't write: \(error)") }
|
||||||
|
}
|
||||||
|
cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB)
|
||||||
|
// TODO: wait for notify response to block or allow connection
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
main/Push Notifications/CachedConnectionAlert.swift
Normal 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?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
main/Push Notifications/PushNotification.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
main/Push Notifications/PushNotificationAppOnly.swift
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
83
main/Push Notifications/UNNotification.swift
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ 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 = "at \(DateFormat.seconds(x.start)), duration: \(TimeFormat.from(x.duration ?? 0))"
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,58 @@ import UIKit
|
|||||||
|
|
||||||
class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||||
var record: Recording!
|
var record: Recording!
|
||||||
private var dataSource: [RecordLog]!
|
private lazy var isLongRecording: Bool = (record.duration ?? 0) > Timestamp.hours(1)
|
||||||
|
|
||||||
|
private var showRaw: Bool = false
|
||||||
|
/// Sorted by `ts` in ascending order (oldest first)
|
||||||
|
private lazy var dataSourceRaw: [DomainTsPair] = RecordingsDB.details(record)
|
||||||
|
/// 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)
|
}
|
||||||
|
|
||||||
|
@IBAction private func toggleDisplayStyle(_ sender: UIBarButtonItem) {
|
||||||
|
showRaw = !showRaw
|
||||||
|
sender.image = UIImage(named: showRaw ? "line-collapse" : "line-expand")
|
||||||
|
tableView.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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 {
|
||||||
|
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 showRaw {
|
||||||
cell.textLabel?.text = x.domain
|
let x = dataSourceRaw[indexPath.row]
|
||||||
cell.detailTextLabel?.text = "\(x.count)"
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +69,25 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
|
|||||||
inputDetails.text = """
|
inputDetails.text = """
|
||||||
Start: \(DateFormat.seconds(record.start))
|
Start: \(DateFormat.seconds(record.start))
|
||||||
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!))
|
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!))
|
||||||
Duration: \(record.durationString ?? "?")
|
Duration: \(TimeFormat.from(record.duration ?? 0))
|
||||||
"""
|
"""
|
||||||
validateSaveButton()
|
validateSaveButton()
|
||||||
if deleteOnCancel { // mark as destructive
|
if deleteOnCancel { // mark as destructive
|
||||||
@@ -47,6 +47,9 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
|
|||||||
RecordingsDB.update(self.record)
|
RecordingsDB.update(self.record)
|
||||||
if newlyCreated {
|
if newlyCreated {
|
||||||
RecordingsDB.persist(self.record)
|
RecordingsDB.persist(self.record)
|
||||||
|
if Prefs.RecordingReminder.Enabled {
|
||||||
|
PushNotification.scheduleRecordingReminder(force: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
|
|||||||
updateUI(setRecording: false, animated: false)
|
updateUI(setRecording: false, animated: false)
|
||||||
currentRecording = RecordingsDB.getCurrent()
|
currentRecording = RecordingsDB.getCurrent()
|
||||||
|
|
||||||
if !Pref.DidShowTutorial.Recordings {
|
if !Prefs.DidShowTutorial.Recordings {
|
||||||
self.perform(#selector(showTutorial), with: nil, afterDelay: 0.5)
|
self.perform(#selector(showTutorial), with: nil, afterDelay: 0.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
|
|||||||
))
|
))
|
||||||
x.buttonTitleDone = "Got it"
|
x.buttonTitleDone = "Got it"
|
||||||
x.present {
|
x.present {
|
||||||
Pref.DidShowTutorial.Recordings = true
|
Prefs.DidShowTutorial.Recordings = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
main/Requests/Analysis/VCAnalysisBar.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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,
|
||||||
|
let vc: VCCoOccurrence = storyboard?.load("IBCoOccurrence") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let info = delegate.analysisBarWillOpenCoOccurrence()
|
||||||
|
vc.domainName = info.domain
|
||||||
|
vc.isFQDN = info.isFQDN
|
||||||
|
present(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!
|
||||||
@@ -15,7 +16,7 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
|
|||||||
|
|
||||||
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 +31,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 +60,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,22 +150,3 @@ extension VCCoOccurrence {
|
|||||||
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!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 = "A – B"
|
self.filterButtonDetail.title = "A – B"
|
||||||
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")
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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] = []
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
97
main/Requests/TVCOccurrenceContext.swift
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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 rowToCopy: Int = Int.max
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||||
|
if firstOrLast(indexPath.row) { return nil }
|
||||||
|
if rowToCopy == indexPath.row {
|
||||||
|
UIMenuController.shared.setMenuVisible(false, animated: true)
|
||||||
|
rowToCopy = Int.max
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rowToCopy = indexPath.row
|
||||||
|
self.becomeFirstResponder()
|
||||||
|
let cell = tableView.cellForRow(at: indexPath)!
|
||||||
|
UIMenuController.shared.setTargetRect(cell.bounds, in: cell)
|
||||||
|
UIMenuController.shared.setMenuVisible(true, animated: true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override var canBecomeFirstResponder: Bool { true }
|
||||||
|
|
||||||
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||||
|
action == #selector(UIResponderStandardEditActions.copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func copy(_ sender: Any?) {
|
||||||
|
guard rowToCopy < dataSource.count else { return }
|
||||||
|
UIPasteboard.general.string = dataSource[rowToCopy].domain
|
||||||
|
rowToCopy = Int.max
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,15 +103,15 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
97
main/Settings/TVCChooseAlertTone.swift
Normal 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 ""
|
||||||
|
}
|
||||||
135
main/Settings/TVCConnectionAlerts.swift
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TVCConnectionAlerts: UITableViewController {
|
||||||
|
|
||||||
|
@IBOutlet var showNotifications: UISwitch!
|
||||||
|
@IBOutlet var cellSound: UITableViewCell!
|
||||||
|
|
||||||
|
@IBOutlet var listsCustomA: UITableViewCell!
|
||||||
|
@IBOutlet var listsCustomB: UITableViewCell!
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
cascadeEnableConnAlert(PrefsShared.ConnectionAlerts.Enabled)
|
||||||
|
cellSound.detailTextLabel?.text = AlertSoundTitle(for: PrefsShared.ConnectionAlerts.Sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
let (_, _, custA, custB) = DomainFilter.counts()
|
||||||
|
listsCustomA.detailTextLabel?.text = "\(custA) Domains"
|
||||||
|
listsCustomB.detailTextLabel?.text = "\(custB) Domains"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cascadeEnableConnAlert(_ flag: Bool) {
|
||||||
|
showNotifications.isOn = flag
|
||||||
|
// en/disable related controls
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getListSelected(_ index: Int) -> Bool {
|
||||||
|
switch index {
|
||||||
|
case 0: return PrefsShared.ConnectionAlerts.Lists.Blocked
|
||||||
|
case 1: return PrefsShared.ConnectionAlerts.Lists.CustomA
|
||||||
|
case 2: return PrefsShared.ConnectionAlerts.Lists.CustomB
|
||||||
|
case 3: return PrefsShared.ConnectionAlerts.Lists.Else
|
||||||
|
default: preconditionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setListSelected(_ index: Int, _ value: Bool) {
|
||||||
|
switch index {
|
||||||
|
case 0: PrefsShared.ConnectionAlerts.Lists.Blocked = value
|
||||||
|
case 1: PrefsShared.ConnectionAlerts.Lists.CustomA = value
|
||||||
|
case 2: PrefsShared.ConnectionAlerts.Lists.CustomB = value
|
||||||
|
case 3: PrefsShared.ConnectionAlerts.Lists.Else = value
|
||||||
|
default: preconditionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Toggles
|
||||||
|
|
||||||
|
@IBAction private func toggleShowNotifications(_ sender: UISwitch) {
|
||||||
|
PrefsShared.ConnectionAlerts.Enabled = sender.isOn
|
||||||
|
cascadeEnableConnAlert(sender.isOn)
|
||||||
|
GlassVPN.send(.notificationSettingsChanged())
|
||||||
|
if sender.isOn {
|
||||||
|
PushNotification.requestAuthorization { granted in
|
||||||
|
if !granted {
|
||||||
|
NotificationsDisabledAlert(presentIn: self)
|
||||||
|
self.cascadeEnableConnAlert(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PushNotification.cancel(.AllConnectionAlertNotifications)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Table View Delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||||
|
let checked: Bool
|
||||||
|
switch indexPath.section {
|
||||||
|
case 1: // mode selection
|
||||||
|
checked = (indexPath.row == (PrefsShared.ConnectionAlerts.ExcludeMode ? 1 : 0))
|
||||||
|
case 2: // include & exclude lists
|
||||||
|
checked = getListSelected(indexPath.row)
|
||||||
|
default: return cell // process only checkmarked cells
|
||||||
|
}
|
||||||
|
cell.accessoryType = checked ? .checkmark : .none
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
switch indexPath.section {
|
||||||
|
case 1: // mode selection
|
||||||
|
PrefsShared.ConnectionAlerts.ExcludeMode = indexPath.row == 1
|
||||||
|
tableView.reloadSections(.init(integer: 2), with: .none)
|
||||||
|
case 2: // include & exclude lists
|
||||||
|
let prev = tableView.cellForRow(at: indexPath)?.accessoryType == .checkmark
|
||||||
|
setListSelected(indexPath.row, !prev)
|
||||||
|
default: return // process only checkmarked cells
|
||||||
|
}
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
tableView.reloadSections(.init(integer: indexPath.section), with: .none)
|
||||||
|
GlassVPN.send(.notificationSettingsChanged())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||||
|
if section == 2 {
|
||||||
|
return PrefsShared.ConnectionAlerts.ExcludeMode ? "Exclude All" : "Include All"
|
||||||
|
}
|
||||||
|
return super.tableView(tableView, titleForHeaderInSection: section)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
if let dest = segue.destination as? TVCFilter {
|
||||||
|
switch segue.identifier {
|
||||||
|
case "segueFilterListCustomA":
|
||||||
|
dest.navigationItem.title = "Custom List A"
|
||||||
|
dest.currentFilter = .customA
|
||||||
|
case "segueFilterListCustomB":
|
||||||
|
dest.navigationItem.title = "Custom List B"
|
||||||
|
dest.currentFilter = .customB
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if let tvc = segue.destination as? TVCChooseAlertTone {
|
||||||
|
tvc.delegate = self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sound Selection
|
||||||
|
|
||||||
|
extension TVCConnectionAlerts: NotificationSoundChangedDelegate {
|
||||||
|
func notificationSoundCurrent() -> String {
|
||||||
|
PrefsShared.ConnectionAlerts.Sound
|
||||||
|
}
|
||||||
|
|
||||||
|
func notificationSoundChanged(filename: String, title: String) {
|
||||||
|
cellSound.detailTextLabel?.text = title
|
||||||
|
PrefsShared.ConnectionAlerts.Sound = filename
|
||||||
|
GlassVPN.send(.notificationSettingsChanged())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,14 +25,10 @@ class TVCFilter: UITableViewController, EditActionsRemove {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction private func addNewFilter() {
|
@IBAction private func addNewFilter() {
|
||||||
let desc: String
|
let alert = AskAlert(title: "Create new filter",
|
||||||
switch currentFilter {
|
text: "Enter the domain name you wish to add.",
|
||||||
case .blocked: desc = "Enter the domain name you wish to block."
|
buttonText: "Add") {
|
||||||
case .ignored: desc = "Enter the domain name you wish to ignore."
|
guard let dom = $0.textFields?.first?.text?.lowercased() else {
|
||||||
default: return
|
|
||||||
}
|
|
||||||
let alert = AskAlert(title: "Create new filter", text: desc, buttonText: "Add") {
|
|
||||||
guard let dom = $0.textFields?.first?.text else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard dom.contains("."), !dom.isKnownSLD() else {
|
guard dom.contains("."), !dom.isKnownSLD() else {
|
||||||
|
|||||||
154
main/Settings/TVCReminderAlerts.swift
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TVCReminderAlerts: UITableViewController {
|
||||||
|
|
||||||
|
@IBOutlet var restartAllow: UISwitch!
|
||||||
|
@IBOutlet var restartAllowNotify: UISwitch!
|
||||||
|
@IBOutlet var restartAllowBadge: UISwitch!
|
||||||
|
@IBOutlet var restartSound: UITableViewCell!
|
||||||
|
|
||||||
|
@IBOutlet var recordingAllow: UISwitch!
|
||||||
|
@IBOutlet var recordingSound: UITableViewCell!
|
||||||
|
|
||||||
|
private enum ReminderCellType { case Restart, Recording }
|
||||||
|
private var selectedSound: ReminderCellType = .Restart
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
restartAllowNotify.isOn = PrefsShared.RestartReminder.WithText
|
||||||
|
restartAllowBadge.isOn = PrefsShared.RestartReminder.WithBadge
|
||||||
|
restartSound.detailTextLabel?.text = AlertSoundTitle(for: PrefsShared.RestartReminder.Sound)
|
||||||
|
recordingSound.detailTextLabel?.text = AlertSoundTitle(for: Prefs.RecordingReminder.Sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
readNotificationState { (allowStart, allowRecord, isProvisional) in
|
||||||
|
self.cascadeEnableRestart(allowStart && !isProvisional)
|
||||||
|
self.recordingAllow.isOn = (allowRecord && !isProvisional)
|
||||||
|
self.setIndicateProvisional(isProvisional)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readNotificationState(_ closure: @escaping (Bool, Bool, Bool) -> Void) {
|
||||||
|
let en1 = PrefsShared.RestartReminder.Enabled
|
||||||
|
let en2 = Prefs.RecordingReminder.Enabled
|
||||||
|
closure(en1, en2, false)
|
||||||
|
guard en1 || en2 else { return }
|
||||||
|
PushNotification.allowed { state in
|
||||||
|
switch state {
|
||||||
|
case .NotDetermined, .Denied: closure(false, false, false)
|
||||||
|
case .Authorized, .Provisional: closure(en1, en2, state == .Provisional)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cascadeEnableRestart(_ flag: Bool) {
|
||||||
|
restartAllow.isOn = flag
|
||||||
|
restartAllowNotify.isEnabled = flag
|
||||||
|
restartAllowBadge.isEnabled = flag
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setIndicateProvisional(_ flag: Bool) {
|
||||||
|
if flag {
|
||||||
|
restartAllow.thumbTintColor = .systemGreen
|
||||||
|
recordingAllow.thumbTintColor = .systemGreen
|
||||||
|
} else {
|
||||||
|
// thumb tint is only set in provisional mode
|
||||||
|
if restartAllow.thumbTintColor <-? nil { restartAllow.isOn = true }
|
||||||
|
if recordingAllow.thumbTintColor <-? nil { recordingAllow.isOn = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateBadge() {
|
||||||
|
let flag = (restartAllow.isOn && restartAllowBadge.isOn && GlassVPN.state != .on)
|
||||||
|
UIApplication.shared.applicationIconBadgeNumber = flag ? 1 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Toggles
|
||||||
|
|
||||||
|
extension TVCReminderAlerts {
|
||||||
|
@IBAction private func toggleAllowRestartReminder(_ sender: UISwitch) {
|
||||||
|
PrefsShared.RestartReminder.Enabled = sender.isOn
|
||||||
|
cascadeEnableRestart(sender.isOn)
|
||||||
|
updateBadge()
|
||||||
|
if sender.isOn {
|
||||||
|
askAuthorization {}
|
||||||
|
} else {
|
||||||
|
PushNotification.cancel(.CantStopMeNowReminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction private func toggleAllowRestartNotify(_ sender: UISwitch) {
|
||||||
|
PrefsShared.RestartReminder.WithText = sender.isOn
|
||||||
|
if !sender.isOn {
|
||||||
|
PushNotification.cancel(.CantStopMeNowReminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction private func toggleAllowRestartBadge(_ sender: UISwitch) {
|
||||||
|
PrefsShared.RestartReminder.WithBadge = sender.isOn
|
||||||
|
updateBadge()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction private func toggleAllowRecordingReminder(_ sender: UISwitch) {
|
||||||
|
Prefs.RecordingReminder.Enabled = sender.isOn
|
||||||
|
if sender.isOn {
|
||||||
|
askAuthorization { PushNotification.scheduleRecordingReminder(force: false) }
|
||||||
|
} else {
|
||||||
|
PushNotification.cancel(.YouShallRecordMoreReminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func askAuthorization(_ closure: @escaping () -> Void) {
|
||||||
|
setIndicateProvisional(false)
|
||||||
|
PushNotification.requestAuthorization { granted in
|
||||||
|
if granted {
|
||||||
|
closure()
|
||||||
|
} else {
|
||||||
|
NotificationsDisabledAlert(presentIn: self)
|
||||||
|
self.cascadeEnableRestart(false)
|
||||||
|
self.recordingAllow.isOn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Sound Selection
|
||||||
|
|
||||||
|
extension TVCReminderAlerts: NotificationSoundChangedDelegate {
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
if let tvc = segue.destination as? TVCChooseAlertTone {
|
||||||
|
switch segue.identifier {
|
||||||
|
case "segueSoundRestartReminder": selectedSound = .Restart
|
||||||
|
case "segueSoundRecordingReminder": selectedSound = .Recording
|
||||||
|
default: preconditionFailure()
|
||||||
|
}
|
||||||
|
tvc.delegate = self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notificationSoundCurrent() -> String {
|
||||||
|
switch selectedSound {
|
||||||
|
case .Restart: return PrefsShared.RestartReminder.Sound
|
||||||
|
case .Recording: return Prefs.RecordingReminder.Sound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notificationSoundChanged(filename: String, title: String) {
|
||||||
|
switch selectedSound {
|
||||||
|
case .Restart:
|
||||||
|
restartSound.detailTextLabel?.text = title
|
||||||
|
PrefsShared.RestartReminder.Sound = filename
|
||||||
|
case .Recording:
|
||||||
|
recordingSound.detailTextLabel?.text = title
|
||||||
|
Prefs.RecordingReminder.Sound = filename
|
||||||
|
if Prefs.RecordingReminder.Enabled {
|
||||||
|
PushNotification.scheduleRecordingReminder(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,59 +2,69 @@ import UIKit
|
|||||||
|
|
||||||
class TVCSettings: UITableViewController {
|
class TVCSettings: UITableViewController {
|
||||||
|
|
||||||
private let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
|
||||||
@IBOutlet var vpnToggle: UISwitch!
|
@IBOutlet var vpnToggle: UISwitch!
|
||||||
@IBOutlet var cellDomainsIgnored: UITableViewCell!
|
@IBOutlet var cellDomainsIgnored: UITableViewCell!
|
||||||
@IBOutlet var cellDomainsBlocked: UITableViewCell!
|
@IBOutlet var cellDomainsBlocked: UITableViewCell!
|
||||||
|
@IBOutlet var cellPrivacyAutoDelete: UITableViewCell!
|
||||||
|
@IBOutlet var cellNotificationReminder: UITableViewCell!
|
||||||
|
@IBOutlet var cellNotificationConnectionAlert: UITableViewCell!
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
NotifyVPNStateChanged.observe(call: #selector(vpnStateChanged(_:)), on: self)
|
reloadVPNState()
|
||||||
changedState(currentVPNState)
|
reloadLoggingFilterUI()
|
||||||
NotifyDNSFilterChanged.observe(call: #selector(reloadDataSource), on: self)
|
reloadPrivacyUI()
|
||||||
reloadDataSource()
|
NotifyVPNStateChanged.observe(call: #selector(reloadVPNState), on: self)
|
||||||
|
NotifyDNSFilterChanged.observe(call: #selector(reloadLoggingFilterUI), on: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func reloadDataSource() {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
let (blocked, ignored) = DomainFilter.counts()
|
super.viewWillAppear(animated)
|
||||||
|
reloadNotificationState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
// FIXME: there is a lag between tap and open when run on device
|
||||||
|
if let cell = tableView.cellForRow(at: indexPath), cell === cellPrivacyAutoDelete {
|
||||||
|
openAutoDeletePicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openRestartVPNSettings() { scrollToSection(0, animated: false) }
|
||||||
|
func openNotificationSettings() { scrollToSection(2, animated: false) }
|
||||||
|
private func scrollToSection(_ section: Int, animated: Bool) {
|
||||||
|
tableView.scrollToRow(at: .init(row: 0, section: section), at: .top, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - VPN Proxy Settings
|
||||||
|
|
||||||
|
extension TVCSettings {
|
||||||
|
@objc private func reloadVPNState() {
|
||||||
|
vpnToggle.isOn = (GlassVPN.state != .off)
|
||||||
|
vpnToggle.onTintColor = (GlassVPN.state == .inbetween ? .systemYellow : nil)
|
||||||
|
UIApplication.shared.applicationIconBadgeNumber =
|
||||||
|
!vpnToggle.isOn &&
|
||||||
|
PrefsShared.RestartReminder.Enabled &&
|
||||||
|
PrefsShared.RestartReminder.WithBadge ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction private func toggleVPNProxy(_ sender: UISwitch) {
|
||||||
|
GlassVPN.setEnabled(sender.isOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Logging Filter
|
||||||
|
|
||||||
|
extension TVCSettings {
|
||||||
|
@objc private func reloadLoggingFilterUI() {
|
||||||
|
let (blocked, ignored, _, _) = DomainFilter.counts()
|
||||||
cellDomainsIgnored.detailTextLabel?.text = "\(ignored) Domains"
|
cellDomainsIgnored.detailTextLabel?.text = "\(ignored) Domains"
|
||||||
cellDomainsBlocked.detailTextLabel?.text = "\(blocked) Domains"
|
cellDomainsBlocked.detailTextLabel?.text = "\(blocked) Domains"
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleVPNProxy(_ sender: UISwitch) {
|
|
||||||
appDelegate.setProxyEnabled(sender.isOn)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func exportDB(_ sender: Any) {
|
|
||||||
let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil)
|
|
||||||
self.present(sheet, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func resetTutorialAlerts(_ sender: UIButton) {
|
|
||||||
Pref.DidShowTutorial.Welcome = false
|
|
||||||
Pref.DidShowTutorial.Recordings = false
|
|
||||||
Alert(title: sender.titleLabel?.text,
|
|
||||||
text: "\nDone.\n\nYou may need to restart the application.").presentIn(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func clearDatabaseResults(_ sender: Any) {
|
|
||||||
AskAlert(title: "Clear results?", text:
|
|
||||||
"You are about to delete all results that have been logged in the past. " +
|
|
||||||
"Your preferences for blocked and ignored domains are preserved.\n" +
|
|
||||||
"Continue?", buttonText: "Delete", buttonStyle: .destructive) { _ in
|
|
||||||
TheGreatDestroyer.deleteAllLogs()
|
|
||||||
}.presentIn(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func vpnStateChanged(_ notification: Notification) {
|
|
||||||
changedState(notification.object as! VPNState)
|
|
||||||
}
|
|
||||||
|
|
||||||
func changedState(_ newState: VPNState) {
|
|
||||||
vpnToggle.isOn = (newState != .off)
|
|
||||||
vpnToggle.onTintColor = (newState == .inbetween ? .systemYellow : nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
|
||||||
let t:String, d: String
|
let t:String, d: String
|
||||||
switch tableView.cellForRow(at: indexPath)?.reuseIdentifier {
|
switch tableView.cellForRow(at: indexPath)?.reuseIdentifier {
|
||||||
@@ -83,3 +93,115 @@ class TVCSettings: UITableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Privacy
|
||||||
|
|
||||||
|
extension TVCSettings {
|
||||||
|
private func reloadPrivacyUI() {
|
||||||
|
let (num, unit) = getAutoDeleteSelection([1, 7, 31])
|
||||||
|
let str: String
|
||||||
|
switch num {
|
||||||
|
case 0: str = "Never"
|
||||||
|
case 1: str = "1 \(["Day", "Week", "Month"][unit])"
|
||||||
|
default: str = "\(num) \(["Days", "Weeks", "Months"][unit])"
|
||||||
|
}
|
||||||
|
cellPrivacyAutoDelete.detailTextLabel?.text = str
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAutoDeleteSelection(_ multiplier: [Int]) -> (Int, Int) {
|
||||||
|
let current = PrefsShared.AutoDeleteLogsDays
|
||||||
|
let snd = multiplier.lastIndex { current % $0 == 0 }! // make sure 1 is in list
|
||||||
|
return (current / multiplier[snd], snd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openAutoDeletePicker() {
|
||||||
|
let multiplier = [1, 7, 31]
|
||||||
|
let (one, two) = getAutoDeleteSelection(multiplier)
|
||||||
|
|
||||||
|
let picker = DurationPickerAlert(
|
||||||
|
title: "Auto-delete logs",
|
||||||
|
detail: "Warning: Logs older than the selected interval are deleted immediately! " +
|
||||||
|
"Logs are also deleted on each app launch, and periodically in the background as long as the VPN is running.",
|
||||||
|
options: [(0...30).map{"\($0)"}, ["Days", "Weeks", "Months"]],
|
||||||
|
widths: [0.4, 0.6])
|
||||||
|
picker.pickerView.setSelection([min(30, one), two])
|
||||||
|
picker.present(in: self) { _, idx in
|
||||||
|
let asDays = idx[0] * multiplier[idx[1]]
|
||||||
|
PrefsShared.AutoDeleteLogsDays = asDays
|
||||||
|
self.reloadPrivacyUI()
|
||||||
|
if !GlassVPN.send(.autoDelete(after: asDays)) {
|
||||||
|
// if VPN isn't active, fallback to immediate local delete
|
||||||
|
TheGreatDestroyer.deleteLogs(olderThan: asDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Notification Settings
|
||||||
|
|
||||||
|
extension TVCSettings {
|
||||||
|
private func reloadNotificationState() {
|
||||||
|
let lbl1 = cellNotificationReminder.detailTextLabel
|
||||||
|
let lbl2 = cellNotificationConnectionAlert.detailTextLabel
|
||||||
|
readNotificationState { (realAllowed, provisional) in
|
||||||
|
lbl1?.text = provisional ? "Enabled" : "Disabled"
|
||||||
|
lbl2?.text = realAllowed ? "Enabled" : "Disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readNotificationState(_ closure: @escaping (_ all: Bool, _ prov: Bool) -> Void) {
|
||||||
|
let en1 = PrefsShared.ConnectionAlerts.Enabled
|
||||||
|
let en2 = Prefs.RecordingReminder.Enabled || PrefsShared.RestartReminder.Enabled
|
||||||
|
closure(en1, en2)
|
||||||
|
guard en1 || en2 else { return }
|
||||||
|
PushNotification.allowed { state in
|
||||||
|
switch state {
|
||||||
|
case .NotDetermined, .Denied: closure(false, false)
|
||||||
|
case .Authorized: closure(en1, en2)
|
||||||
|
case .Provisional: closure(false, en2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Reset Settings
|
||||||
|
|
||||||
|
extension TVCSettings {
|
||||||
|
@IBAction private func resetTutorialAlerts(_ sender: UIButton) {
|
||||||
|
Prefs.DidShowTutorial.Welcome = false
|
||||||
|
Prefs.DidShowTutorial.Recordings = false
|
||||||
|
Alert(title: sender.titleLabel?.text,
|
||||||
|
text: "\nDone.\n\nYou may need to restart the application.").presentIn(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction private func clearDatabaseResults() {
|
||||||
|
AskAlert(title: "Clear results?", text:
|
||||||
|
"You are about to delete all results that have been logged in the past. " +
|
||||||
|
"Your preferences for blocked and ignored domains are preserved.\n" +
|
||||||
|
"Continue?", buttonText: "Delete", buttonStyle: .destructive) { _ in
|
||||||
|
TheGreatDestroyer.deleteAllLogs()
|
||||||
|
}.presentIn(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Advanced
|
||||||
|
|
||||||
|
extension TVCSettings {
|
||||||
|
@IBAction private func exportDB() {
|
||||||
|
AppDB?.vacuum()
|
||||||
|
let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil)
|
||||||
|
self.present(sheet, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||||
|
if section == tableView.numberOfSections - 1 {
|
||||||
|
let fs = FileManager.default.readableSizeOf(path: URL.internalDB().relativePath)
|
||||||
|
return "Database size: \(fs ?? "0 MB")"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,15 +5,34 @@ class TBCMain: UITabBarController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
NotifyVPNStateChanged.observe(call: #selector(vpnStateChanged(_:)), on: self)
|
reloadTabBarBadge()
|
||||||
changedState(currentVPNState)
|
NotifyVPNStateChanged.observe(call: #selector(reloadTabBarBadge), on: self)
|
||||||
|
|
||||||
if !Pref.DidShowTutorial.Welcome {
|
if !Prefs.DidShowTutorial.Welcome {
|
||||||
self.perform(#selector(showWelcomeMessage), with: nil, afterDelay: 0.5)
|
self.perform(#selector(showWelcomeMessage), with: nil, afterDelay: 0.5)
|
||||||
}
|
}
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
initNotifications()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func showWelcomeMessage() {
|
@objc private func reloadTabBarBadge() {
|
||||||
|
let stateView = self.tabBar.items?.last
|
||||||
|
switch GlassVPN.state {
|
||||||
|
case .on: stateView?.badgeValue = "✓"
|
||||||
|
case .inbetween: stateView?.badgeValue = "⋯"
|
||||||
|
case .off: stateView?.badgeValue = "✗"
|
||||||
|
}
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
switch GlassVPN.state {
|
||||||
|
case .on: stateView?.badgeColor = .systemGreen
|
||||||
|
case .inbetween: stateView?.badgeColor = .systemYellow
|
||||||
|
case .off: stateView?.badgeColor = .systemRed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func showWelcomeMessage() {
|
||||||
let x = TutorialSheet()
|
let x = TutorialSheet()
|
||||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||||
.h1("Welcome\n")
|
.h1("Welcome\n")
|
||||||
@@ -37,27 +56,83 @@ class TBCMain: UITabBarController {
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
x.present {
|
x.present {
|
||||||
Pref.DidShowTutorial.Welcome = true
|
Prefs.DidShowTutorial.Welcome = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@objc func vpnStateChanged(_ notification: Notification) {
|
|
||||||
changedState(notification.object as! VPNState)
|
extension TBCMain {
|
||||||
}
|
@discardableResult func openTab(_ index: Int) -> UIViewController? {
|
||||||
|
selectedIndex = index
|
||||||
func changedState(_ newState: VPNState) {
|
guard let nav = selectedViewController as? UINavigationController else {
|
||||||
let stateView = self.tabBar.items?.last
|
return selectedViewController
|
||||||
switch newState {
|
}
|
||||||
case .on: stateView?.badgeValue = "✓"
|
nav.popToRootViewController(animated: false)
|
||||||
case .inbetween: stateView?.badgeValue = "⋯"
|
return nav.topViewController
|
||||||
case .off: stateView?.badgeValue = "✗"
|
}
|
||||||
}
|
}
|
||||||
if #available(iOS 10.0, *) {
|
|
||||||
switch newState {
|
// MARK: - Push Notifications
|
||||||
case .on: stateView?.badgeColor = .systemGreen
|
|
||||||
case .inbetween: stateView?.badgeColor = .systemYellow
|
@available(iOS 10.0, *)
|
||||||
case .off: stateView?.badgeColor = .systemRed
|
extension TBCMain: UNUserNotificationCenterDelegate {
|
||||||
}
|
|
||||||
|
func initNotifications() {
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
guard Prefs.RecordingReminder.Enabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PushNotification.allowed {
|
||||||
|
switch $0 {
|
||||||
|
case .NotDetermined:
|
||||||
|
PushNotification.requestProvisionalOrDoNothing { success in
|
||||||
|
guard success else { return }
|
||||||
|
PushNotification.scheduleRecordingReminder(force: false)
|
||||||
|
}
|
||||||
|
case .Denied:
|
||||||
|
break
|
||||||
|
case .Authorized, .Provisional:
|
||||||
|
PushNotification.scheduleRecordingReminder(force: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
completionHandler([.alert, .badge, .sound]) // in-app notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
defer { completionHandler() }
|
||||||
|
if isFrontmostModal() {
|
||||||
|
return // dont intervene user actions
|
||||||
|
}
|
||||||
|
switch response.notification.request.identifier {
|
||||||
|
case PushNotification.Identifier.YouShallRecordMoreReminder.rawValue:
|
||||||
|
selectedIndex = 1 // open recordings tab
|
||||||
|
case PushNotification.Identifier.CantStopMeNowReminder.rawValue:
|
||||||
|
(openTab(2) as! TVCSettings).openRestartVPNSettings()
|
||||||
|
//case PushNotification.Identifier.RestInPeaceTombstoneReminder // only badge
|
||||||
|
case let x: // domain notification
|
||||||
|
(openTab(0) as! TVCDomains).pushOpen(domain: x) // open requests tab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 12.0, *)
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||||
|
(openTab(2) as! TVCSettings).openNotificationSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFrontmostModal() -> Bool {
|
||||||
|
var x = selectedViewController!
|
||||||
|
while let tmp = x.presentedViewController {
|
||||||
|
x = tmp
|
||||||
|
}
|
||||||
|
if x is UIAlertController {
|
||||||
|
return true
|
||||||
|
} else if #available(iOS 13.0, *) {
|
||||||
|
return x.isModalInPresentation
|
||||||
|
} else {
|
||||||
|
return x.modalPresentationStyle == .custom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||